From 76c3ead4a8bb4327e21cf227a568e623ce827aae Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Sat, 2 May 2026 23:43:48 +0300 Subject: [PATCH] game_start event works --- src/features/editScene/CMakeLists.txt | 36 + src/features/editScene/EditorApp.cpp | 56 +- src/features/editScene/GameMode.cpp | 37 + src/features/editScene/GameMode.hpp | 101 ++ .../components/DialogueComponent.hpp | 2 +- .../editScene/components/EventHandler.hpp | 6 +- .../editScene/components/EventParams.hpp | 739 ++++++++++++++ .../lua-examples/debug_crash_example.lua | 28 + .../lua-examples/dialogue_basic_show.lua | 28 +- .../lua-examples/dialogue_component_api.lua | 14 +- .../lua-examples/dialogue_event_handler.lua | 85 +- .../lua-examples/dialogue_event_subscribe.lua | 38 +- .../lua-examples/dialogue_sequence.lua | 66 +- .../editScene/lua-examples/event_example.lua | 165 +-- .../lua-examples/game_mode_example.lua | 135 +++ src/features/editScene/lua/LuaEventApi.cpp | 417 +++++--- src/features/editScene/lua/LuaEventApi.hpp | 28 +- src/features/editScene/lua/LuaGameModeApi.cpp | 171 ++++ src/features/editScene/lua/LuaGameModeApi.hpp | 39 + .../editScene/systems/BehaviorTreeSystem.cpp | 24 +- .../editScene/systems/DialogueSystem.cpp | 13 +- .../editScene/systems/DialogueSystem.hpp | 2 +- src/features/editScene/systems/EventBus.cpp | 35 +- src/features/editScene/systems/EventBus.hpp | 37 +- .../editScene/systems/EventHandlerSystem.cpp | 305 +++--- .../editScene/systems/EventHandlerSystem.hpp | 43 +- .../editScene/tests/event_lua_test.cpp | 221 ++-- .../editScene/tests/event_params_test.cpp | 957 ++++++++++++++++++ .../editScene/tests/game_mode_lua_test.cpp | 272 +++++ .../editScene/tests/lua_test_stubs.cpp | 318 +++++- src/features/editScene/tests/ogre_stub.h | 1 + 31 files changed, 3725 insertions(+), 694 deletions(-) create mode 100644 src/features/editScene/GameMode.cpp create mode 100644 src/features/editScene/GameMode.hpp create mode 100644 src/features/editScene/components/EventParams.hpp create mode 100644 src/features/editScene/lua-examples/debug_crash_example.lua create mode 100644 src/features/editScene/lua-examples/game_mode_example.lua create mode 100644 src/features/editScene/lua/LuaGameModeApi.cpp create mode 100644 src/features/editScene/lua/LuaGameModeApi.hpp create mode 100644 src/features/editScene/tests/event_params_test.cpp create mode 100644 src/features/editScene/tests/game_mode_lua_test.cpp diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index f1e87bf..fa882e6 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -15,6 +15,7 @@ add_subdirectory(recastnavigation) set(EDITSCENE_SOURCES main.cpp EditorApp.cpp + GameMode.cpp systems/EditorUISystem.cpp systems/SceneSerializer.cpp systems/PhysicsSystem.cpp @@ -154,6 +155,7 @@ set(EDITSCENE_SOURCES lua/LuaEventApi.cpp lua/LuaActionApi.cpp lua/LuaBehaviorTreeApi.cpp + lua/LuaGameModeApi.cpp ) set(EDITSCENE_HEADERS @@ -305,6 +307,7 @@ set(EDITSCENE_HEADERS lua/LuaEventApi.hpp lua/LuaActionApi.hpp lua/LuaBehaviorTreeApi.hpp + lua/LuaGameModeApi.hpp ) add_executable(editSceneEditor ${EDITSCENE_SOURCES} ${EDITSCENE_HEADERS}) @@ -420,7 +423,9 @@ target_include_directories(action_db_lua_test PRIVATE add_executable(behavior_tree_lua_test tests/behavior_tree_lua_test.cpp lua/LuaBehaviorTreeApi.cpp + lua/LuaGameModeApi.cpp lua/LuaEntityApi.cpp + GameMode.cpp components/GoapBlackboard.cpp ) @@ -438,6 +443,37 @@ target_include_directories(behavior_tree_lua_test PRIVATE target_compile_definitions(behavior_tree_lua_test PRIVATE flecs_STATIC) +# Test: EventParams C++ API (standalone, no Lua dependency) +add_executable(event_params_test + tests/event_params_test.cpp +) + +target_link_libraries(event_params_test + lua +) + +target_include_directories(event_params_test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/tests + ${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src +) + +# Test: Game Mode Lua API +add_executable(game_mode_lua_test + tests/game_mode_lua_test.cpp + tests/lua_test_stubs.cpp +) + +target_link_libraries(game_mode_lua_test + lua +) + +target_include_directories(game_mode_lua_test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/tests + ${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src +) + # Copy local resources (materials, etc.) if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/resources") file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/resources" diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index c942fbe..fb5e740 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -1,5 +1,6 @@ #include #include "EditorApp.hpp" +#include "GameMode.hpp" #include "systems/EditorUISystem.hpp" #include "systems/PhysicsSystem.hpp" #include "systems/BuoyancySystem.hpp" @@ -77,6 +78,7 @@ #include "components/PathFollowing.hpp" #include "systems/ActuatorSystem.hpp" #include "systems/EventHandlerSystem.hpp" +#include "systems/EventBus.hpp" #include "systems/ItemSystem.hpp" #include "components/EventHandler.hpp" #include "components/Item.hpp" @@ -89,6 +91,7 @@ #include "lua/LuaEventApi.hpp" #include "lua/LuaActionApi.hpp" #include "lua/LuaBehaviorTreeApi.hpp" +#include "lua/LuaGameModeApi.hpp" //============================================================================= // ImGuiRenderListener Implementation @@ -446,24 +449,6 @@ void EditorApp::setup() std::make_unique( m_world, m_sceneMgr, this); - if (m_gameMode == GameMode::Game) { - // Load startup menu scene configured in editor - SceneSerializer serializer(m_world, m_sceneMgr); - Ogre::LogManager::getSingleton().logMessage( - "Game mode: Loading startup_menu.json..."); - if (serializer.loadFromFile("startup_menu.json", - m_uiSystem.get())) { - PrefabSystem prefabSys(m_world, m_sceneMgr); - prefabSys.resolveInstances(); - Ogre::LogManager::getSingleton().logMessage( - "Game mode: startup_menu.json loaded"); - } else { - Ogre::LogManager::getSingleton().logMessage( - "Game mode: Failed to load startup_menu.json: " + - serializer.getLastError()); - } - } - // Pre-load fonts before showing overlay if (m_startupMenuSystem) m_startupMenuSystem->prepareFont(); @@ -508,6 +493,7 @@ void EditorApp::setup() editScene::registerLuaEventApi(L); editScene::registerLuaActionApi(L); editScene::registerLuaBehaviorTreeApi(L); + editScene::registerLuaGameModeApi(L); // Run late setup: load data.lua and initial scripts. m_lua.lateSetup(); @@ -524,6 +510,27 @@ void EditorApp::setup() ActionDatabase::reloadFromSceneComponents(m_world); } + if (m_gameMode == GameMode::Game) { + // Queue "game_start" event before loading the startup menu + EventBus::getInstance().send("game_start"); + + // Load startup menu scene configured in editor + SceneSerializer serializer(m_world, m_sceneMgr); + Ogre::LogManager::getSingleton().logMessage( + "Game mode: Loading startup_menu.json..."); + if (serializer.loadFromFile("startup_menu.json", + m_uiSystem.get())) { + PrefabSystem prefabSys(m_world, m_sceneMgr); + prefabSys.resolveInstances(); + Ogre::LogManager::getSingleton().logMessage( + "Game mode: startup_menu.json loaded"); + } else { + Ogre::LogManager::getSingleton().logMessage( + "Game mode: Failed to load startup_menu.json: " + + serializer.getLastError()); + } + } + // Game mode can be set externally before setup() is called m_setupComplete = true; @@ -542,12 +549,19 @@ void EditorApp::setGameMode(GameMode mode) return; } m_gameMode = mode; + editScene::setEditSceneGameMode(mode == GameMode::Game ? + editScene::GameMode::Game : + editScene::GameMode::Editor); if (m_gameMode == GameMode::Game) { m_gamePlayState = GamePlayState::Menu; + editScene::setEditSceneGamePlayState( + editScene::GamePlayState::Menu); if (m_uiSystem) m_uiSystem->setEditorUIEnabled(false); } else { m_gamePlayState = GamePlayState::Menu; + editScene::setEditSceneGamePlayState( + editScene::GamePlayState::Menu); if (m_uiSystem) m_uiSystem->setEditorUIEnabled(true); } @@ -578,6 +592,12 @@ void EditorApp::setDebugBuoyancy(bool enabled) void EditorApp::setGamePlayState(GamePlayState state) { m_gamePlayState = state; + editScene::setEditSceneGamePlayState( + state == GamePlayState::Playing ? + editScene::GamePlayState::Playing : + state == GamePlayState::Paused ? + editScene::GamePlayState::Paused : + editScene::GamePlayState::Menu); // Grab/ungrab mouse based on gameplay state if (m_gameMode == GameMode::Game) { diff --git a/src/features/editScene/GameMode.cpp b/src/features/editScene/GameMode.cpp new file mode 100644 index 0000000..7109712 --- /dev/null +++ b/src/features/editScene/GameMode.cpp @@ -0,0 +1,37 @@ +#include "GameMode.hpp" + +namespace editScene +{ + +namespace +{ + +/// Global game mode state. +GameMode s_gameMode = GameMode::Editor; + +/// Global gameplay state (only meaningful in game mode). +GamePlayState s_gamePlayState = GamePlayState::Menu; + +} // anonymous namespace + +void setEditSceneGameMode(GameMode mode) noexcept +{ + s_gameMode = mode; +} + +void setEditSceneGamePlayState(GamePlayState state) noexcept +{ + s_gamePlayState = state; +} + +GameMode getGameMode() noexcept +{ + return s_gameMode; +} + +GamePlayState getGamePlayState() noexcept +{ + return s_gamePlayState; +} + +} // namespace editScene diff --git a/src/features/editScene/GameMode.hpp b/src/features/editScene/GameMode.hpp new file mode 100644 index 0000000..73f7698 --- /dev/null +++ b/src/features/editScene/GameMode.hpp @@ -0,0 +1,101 @@ +#ifndef EDITSCENE_GAMEMODE_HPP +#define EDITSCENE_GAMEMODE_HPP +#pragma once + +/** + * @file GameMode.hpp + * + * Global game mode query functions for the editScene feature. + * + * These functions allow any code in the editScene feature to query + * whether the application is currently in editor mode or game mode, + * and what the current gameplay state is, without needing a direct + * pointer to EditorApp. + * + * The EditorApp sets the current mode via setEditSceneGameMode() + * during its lifetime. Code outside the editScene feature should + * continue to use EditorApp::getGameMode() / getGamePlayState() + * directly. + */ + +namespace editScene +{ + +/** + * Application mode: editor or game. + */ +enum class GameMode { Editor, Game }; + +/** + * Play state when in game mode. + */ +enum class GamePlayState { Menu, Playing, Paused }; + +// --------------------------------------------------------------------------- +// Global state management (called by EditorApp) +// --------------------------------------------------------------------------- + +/** + * Set the current game mode. Called by EditorApp on mode changes. + */ +void setEditSceneGameMode(GameMode mode) noexcept; + +/** + * Set the current gameplay state. Called by EditorApp on state changes. + */ +void setEditSceneGamePlayState(GamePlayState state) noexcept; + +// --------------------------------------------------------------------------- +// Query functions +// --------------------------------------------------------------------------- + +/** + * Return the current application mode. + */ +GameMode getGameMode() noexcept; + +/** + * Return the current gameplay state (only meaningful in game mode). + */ +GamePlayState getGamePlayState() noexcept; + +// --------------------------------------------------------------------------- +// Predicates +// --------------------------------------------------------------------------- + +/** True when the application is in editor mode. */ +inline bool isEditorMode() noexcept +{ + return getGameMode() == GameMode::Editor; +} + +/** True when the application is in game mode (any play state). */ +inline bool isGameMode() noexcept +{ + return getGameMode() == GameMode::Game; +} + +/** True when in game mode and the gameplay state is Playing. */ +inline bool isGamePlaying() noexcept +{ + return getGameMode() == GameMode::Game && + getGamePlayState() == GamePlayState::Playing; +} + +/** True when in game mode and the gameplay state is Menu. */ +inline bool isGameMenu() noexcept +{ + return getGameMode() == GameMode::Game && + getGamePlayState() == GamePlayState::Menu; +} + +/** True when in game mode and the gameplay state is Paused. */ +inline bool isGamePaused() noexcept +{ + return getGameMode() == GameMode::Game && + getGamePlayState() == GamePlayState::Paused; +} + +} // namespace editScene + +#endif // EDITSCENE_GAMEMODE_HPP diff --git a/src/features/editScene/components/DialogueComponent.hpp b/src/features/editScene/components/DialogueComponent.hpp index fd0b4d7..2216e00 100644 --- a/src/features/editScene/components/DialogueComponent.hpp +++ b/src/features/editScene/components/DialogueComponent.hpp @@ -16,7 +16,7 @@ * * Only active in game mode (GamePlayState::Playing). * - * Event payload (GoapBlackboard) parameters: + * Event payload (EventParams) parameters: * "text" (string) - Narration text to display * "choices" (string) - Comma-separated list of choice labels * "speaker" (string) - Optional speaker name diff --git a/src/features/editScene/components/EventHandler.hpp b/src/features/editScene/components/EventHandler.hpp index 89e2df6..2fe4a56 100644 --- a/src/features/editScene/components/EventHandler.hpp +++ b/src/features/editScene/components/EventHandler.hpp @@ -8,9 +8,9 @@ * Event-driven behavior tree handler component. * * When the specified event is received, the referenced GoapAction's - * behavior tree is executed for this entity. Event parameters are - * merged into the entity's GoapBlackboard before the tree runs and - * cleaned up when the tree completes. + * behavior tree is executed for this entity. Event parameters + * (EventParams) are injected into the entity's GoapBlackboard before + * the tree runs and cleaned up when the tree completes. */ struct EventHandlerComponent { Ogre::String eventName; diff --git a/src/features/editScene/components/EventParams.hpp b/src/features/editScene/components/EventParams.hpp new file mode 100644 index 0000000..c5d9209 --- /dev/null +++ b/src/features/editScene/components/EventParams.hpp @@ -0,0 +1,739 @@ +#ifndef EDITSCENE_EVENT_PARAMS_HPP +#define EDITSCENE_EVENT_PARAMS_HPP +#pragma once + +#include +#include +#include +#include +#include +#include + +/** + * @file EventParams.hpp + * @brief Tagged union type for event parameters. + * + * A C++11-compatible, RTTI-free tagged union that supports: + * - Entity ID (uint64_t) + * - Integer (int64_t) + * - Float (float) + * - Double (double) + * - String (std::string) + * - Array of entity IDs (std::vector) + * - Array of integers (std::vector) + * - Array of floats (std::vector) + * - Array of doubles (std::vector) + * - Array of strings (std::vector) + * + * Named parameters are stored as a map of string -> EventValue, + * where EventValue is a tagged union of the above types. + */ + +// Forward declaration for friend function +struct lua_State; + +namespace editScene +{ + +// --------------------------------------------------------------------------- +// EventValue: A single tagged-union value +// --------------------------------------------------------------------------- + +struct EventValue { + enum Type { + NIL = 0, + ENTITY_ID, + INT, + FLOAT, + DOUBLE, + STRING, + ENTITY_ID_ARRAY, + INT_ARRAY, + FLOAT_ARRAY, + DOUBLE_ARRAY, + STRING_ARRAY + }; + + Type type; + + union { + uint64_t asEntityId; + int64_t asInt; + float asFloat; + double asDouble; + }; + + // Heap-allocated data (strings and arrays) + // We use raw pointers to avoid std::unique_ptr (C++11 compatible) + std::string *strPtr; + void *arrayPtr; // points to std::vector* + size_t arraySize; + + EventValue() + : type(NIL) + , asEntityId(0) + , strPtr(nullptr) + , arrayPtr(nullptr) + , arraySize(0) + { + } + + explicit EventValue(uint64_t entityId) + : type(ENTITY_ID) + , asEntityId(entityId) + , strPtr(nullptr) + , arrayPtr(nullptr) + , arraySize(0) + { + } + + explicit EventValue(int64_t val) + : type(INT) + , asInt(val) + , strPtr(nullptr) + , arrayPtr(nullptr) + , arraySize(0) + { + } + + explicit EventValue(int val) + : type(INT) + , asInt(static_cast(val)) + , strPtr(nullptr) + , arrayPtr(nullptr) + , arraySize(0) + { + } + + explicit EventValue(float val) + : type(FLOAT) + , asFloat(val) + , strPtr(nullptr) + , arrayPtr(nullptr) + , arraySize(0) + { + } + + explicit EventValue(double val) + : type(DOUBLE) + , asDouble(val) + , strPtr(nullptr) + , arrayPtr(nullptr) + , arraySize(0) + { + } + + explicit EventValue(const std::string &val) + : type(STRING) + , asEntityId(0) + , strPtr(new std::string(val)) + , arrayPtr(nullptr) + , arraySize(0) + { + } + + explicit EventValue(const char *val) + : type(STRING) + , asEntityId(0) + , strPtr(new std::string(val ? val : "")) + , arrayPtr(nullptr) + , arraySize(0) + { + } + + // Array constructors + explicit EventValue(const std::vector &arr) + : type(ENTITY_ID_ARRAY) + , asEntityId(0) + , strPtr(nullptr) + , arrayPtr(new std::vector(arr)) + , arraySize(arr.size()) + { + } + + explicit EventValue(const std::vector &arr) + : type(INT_ARRAY) + , asEntityId(0) + , strPtr(nullptr) + , arrayPtr(new std::vector(arr)) + , arraySize(arr.size()) + { + } + + explicit EventValue(const std::vector &arr) + : type(INT_ARRAY) + , asEntityId(0) + , strPtr(nullptr) + , arrayPtr(new std::vector(arr.begin(), arr.end())) + , arraySize(arr.size()) + { + } + + explicit EventValue(const std::vector &arr) + : type(FLOAT_ARRAY) + , asEntityId(0) + , strPtr(nullptr) + , arrayPtr(new std::vector(arr)) + , arraySize(arr.size()) + { + } + + explicit EventValue(const std::vector &arr) + : type(DOUBLE_ARRAY) + , asEntityId(0) + , strPtr(nullptr) + , arrayPtr(new std::vector(arr)) + , arraySize(arr.size()) + { + } + + explicit EventValue(const std::vector &arr) + : type(STRING_ARRAY) + , asEntityId(0) + , strPtr(nullptr) + , arrayPtr(new std::vector(arr)) + , arraySize(arr.size()) + { + } + + // Copy constructor + EventValue(const EventValue &other) + : type(other.type) + , asEntityId(other.asEntityId) + , strPtr(nullptr) + , arrayPtr(nullptr) + , arraySize(other.arraySize) + { + copyHeapData(other); + } + + // Copy assignment + EventValue &operator=(const EventValue &other) + { + if (this != &other) { + destroyHeapData(); + type = other.type; + asEntityId = other.asEntityId; + arraySize = other.arraySize; + strPtr = nullptr; + arrayPtr = nullptr; + copyHeapData(other); + } + return *this; + } + + // Move constructor + EventValue(EventValue &&other) noexcept : type(other.type), + asEntityId(other.asEntityId), + strPtr(other.strPtr), + arrayPtr(other.arrayPtr), + arraySize(other.arraySize) + { + other.type = NIL; + other.strPtr = nullptr; + other.arrayPtr = nullptr; + other.arraySize = 0; + } + + // Move assignment + EventValue &operator=(EventValue &&other) noexcept + { + if (this != &other) { + destroyHeapData(); + type = other.type; + asEntityId = other.asEntityId; + strPtr = other.strPtr; + arrayPtr = other.arrayPtr; + arraySize = other.arraySize; + other.type = NIL; + other.strPtr = nullptr; + other.arrayPtr = nullptr; + other.arraySize = 0; + } + return *this; + } + + ~EventValue() + { + destroyHeapData(); + } + + // --- Accessors --- + + Type getType() const + { + return type; + } + + uint64_t getEntityId() const + { + assert(type == ENTITY_ID); + return asEntityId; + } + + int64_t getInt() const + { + assert(type == INT); + return asInt; + } + + float getFloat() const + { + assert(type == FLOAT); + return asFloat; + } + + double getDouble() const + { + assert(type == DOUBLE); + return asDouble; + } + + const std::string &getString() const + { + assert(type == STRING && strPtr != nullptr); + return *strPtr; + } + + const std::vector &getEntityIdArray() const + { + assert(type == ENTITY_ID_ARRAY && arrayPtr != nullptr); + return *static_cast *>(arrayPtr); + } + + const std::vector &getIntArray() const + { + assert(type == INT_ARRAY && arrayPtr != nullptr); + return *static_cast *>(arrayPtr); + } + + const std::vector &getFloatArray() const + { + assert(type == FLOAT_ARRAY && arrayPtr != nullptr); + return *static_cast *>(arrayPtr); + } + + const std::vector &getDoubleArray() const + { + assert(type == DOUBLE_ARRAY && arrayPtr != nullptr); + return *static_cast *>(arrayPtr); + } + + const std::vector &getStringArray() const + { + assert(type == STRING_ARRAY && arrayPtr != nullptr); + return *static_cast *>(arrayPtr); + } + + // --- Convenience: get numeric value as double --- + double asNumeric() const + { + switch (type) { + case INT: + return static_cast(asInt); + case FLOAT: + return static_cast(asFloat); + case DOUBLE: + return asDouble; + case ENTITY_ID: + return static_cast(asEntityId); + default: + return 0.0; + } + } + + // --- Equality --- + bool operator==(const EventValue &other) const + { + if (type != other.type) + return false; + switch (type) { + case NIL: + return true; + case ENTITY_ID: + return asEntityId == other.asEntityId; + case INT: + return asInt == other.asInt; + case FLOAT: + return asFloat == other.asFloat; + case DOUBLE: + return asDouble == other.asDouble; + case STRING: + return strPtr && other.strPtr && + *strPtr == *other.strPtr; + case ENTITY_ID_ARRAY: + return arrayPtr && other.arrayPtr && + getEntityIdArray() == other.getEntityIdArray(); + case INT_ARRAY: + return arrayPtr && other.arrayPtr && + getIntArray() == other.getIntArray(); + case FLOAT_ARRAY: + return arrayPtr && other.arrayPtr && + getFloatArray() == other.getFloatArray(); + case DOUBLE_ARRAY: + return arrayPtr && other.arrayPtr && + getDoubleArray() == other.getDoubleArray(); + case STRING_ARRAY: + return arrayPtr && other.arrayPtr && + getStringArray() == other.getStringArray(); + } + return false; + } + + bool operator!=(const EventValue &other) const + { + return !(*this == other); + } + +private: + void copyHeapData(const EventValue &other) + { + if (other.type == STRING && other.strPtr) { + strPtr = new std::string(*other.strPtr); + } else if (other.type == ENTITY_ID_ARRAY && other.arrayPtr) { + arrayPtr = new std::vector( + *static_cast *>( + other.arrayPtr)); + } else if (other.type == INT_ARRAY && other.arrayPtr) { + arrayPtr = new std::vector( + *static_cast *>( + other.arrayPtr)); + } else if (other.type == FLOAT_ARRAY && other.arrayPtr) { + arrayPtr = new std::vector( + *static_cast *>( + other.arrayPtr)); + } else if (other.type == DOUBLE_ARRAY && other.arrayPtr) { + arrayPtr = new std::vector( + *static_cast *>( + other.arrayPtr)); + } else if (other.type == STRING_ARRAY && other.arrayPtr) { + arrayPtr = new std::vector( + *static_cast *>( + other.arrayPtr)); + } + } + + void destroyHeapData() + { + if (type == STRING) { + delete strPtr; + } else if (type == ENTITY_ID_ARRAY) { + delete static_cast *>(arrayPtr); + } else if (type == INT_ARRAY) { + delete static_cast *>(arrayPtr); + } else if (type == FLOAT_ARRAY) { + delete static_cast *>(arrayPtr); + } else if (type == DOUBLE_ARRAY) { + delete static_cast *>(arrayPtr); + } else if (type == STRING_ARRAY) { + delete static_cast *>( + arrayPtr); + } + strPtr = nullptr; + arrayPtr = nullptr; + } +}; + +// --------------------------------------------------------------------------- +// EventParams: A map of named EventValue entries +// --------------------------------------------------------------------------- + +class EventParams { +public: + EventParams() = default; + + // --- Set values --- + + void setEntityId(const std::string &key, uint64_t val) + { + m_values[key] = EventValue(val); + } + + void setInt(const std::string &key, int64_t val) + { + m_values[key] = EventValue(val); + } + + void setFloat(const std::string &key, float val) + { + m_values[key] = EventValue(val); + } + + void setDouble(const std::string &key, double val) + { + m_values[key] = EventValue(val); + } + + void setString(const std::string &key, const std::string &val) + { + m_values[key] = EventValue(val); + } + + void setEntityIdArray(const std::string &key, + const std::vector &val) + { + m_values[key] = EventValue(val); + } + + void setIntArray(const std::string &key, + const std::vector &val) + { + m_values[key] = EventValue(val); + } + + void setFloatArray(const std::string &key, + const std::vector &val) + { + m_values[key] = EventValue(val); + } + + void setDoubleArray(const std::string &key, + const std::vector &val) + { + m_values[key] = EventValue(val); + } + + void setStringArray(const std::string &key, + const std::vector &val) + { + m_values[key] = EventValue(val); + } + + // --- Get values --- + + bool has(const std::string &key) const + { + return m_values.find(key) != m_values.end(); + } + + const EventValue *get(const std::string &key) const + { + auto it = m_values.find(key); + if (it != m_values.end()) + return &it->second; + return nullptr; + } + + EventValue *get(const std::string &key) + { + auto it = m_values.find(key); + if (it != m_values.end()) + return &it->second; + return nullptr; + } + + // --- Typed getters with defaults --- + + uint64_t getEntityId(const std::string &key, + uint64_t defaultVal = 0) const + { + auto v = get(key); + if (v && v->getType() == EventValue::ENTITY_ID) + return v->getEntityId(); + return defaultVal; + } + + int64_t getInt(const std::string &key, int64_t defaultVal = 0) const + { + auto v = get(key); + if (v && v->getType() == EventValue::INT) + return v->getInt(); + return defaultVal; + } + + float getFloat(const std::string &key, float defaultVal = 0.0f) const + { + auto v = get(key); + if (v && v->getType() == EventValue::FLOAT) + return v->getFloat(); + return defaultVal; + } + + double getDouble(const std::string &key, double defaultVal = 0.0) const + { + auto v = get(key); + if (v && v->getType() == EventValue::DOUBLE) + return v->getDouble(); + return defaultVal; + } + + std::string getString(const std::string &key, + const std::string &defaultVal = "") const + { + auto v = get(key); + if (v && v->getType() == EventValue::STRING) + return v->getString(); + return defaultVal; + } + + // --- Remove --- + + void remove(const std::string &key) + { + m_values.erase(key); + } + + // --- Clear --- + + void clear() + { + m_values.clear(); + } + + // --- Size --- + + size_t size() const + { + return m_values.size(); + } + + bool empty() const + { + return m_values.empty(); + } + + // --- Iteration --- + + typedef std::unordered_map::const_iterator + ConstIterator; + typedef std::unordered_map::iterator Iterator; + + ConstIterator begin() const + { + return m_values.begin(); + } + ConstIterator end() const + { + return m_values.end(); + } + Iterator begin() + { + return m_values.begin(); + } + Iterator end() + { + return m_values.end(); + } + + // --- Merge --- + + void merge(const EventParams &other) + { + for (const auto &pair : other.m_values) + m_values[pair.first] = pair.second; + } + + // --- Equality --- + + bool operator==(const EventParams &other) const + { + return m_values == other.m_values; + } + + bool operator!=(const EventParams &other) const + { + return !(*this == other); + } + + // --- Dump for debugging --- + + std::string dump() const + { + std::string result = "EventParams:\n"; + for (const auto &pair : m_values) { + result += " " + pair.first + " = "; + switch (pair.second.getType()) { + case EventValue::NIL: + result += "nil"; + break; + case EventValue::ENTITY_ID: + result += "entity:" + + std::to_string( + pair.second.getEntityId()); + break; + case EventValue::INT: + result += std::to_string(pair.second.getInt()); + break; + case EventValue::FLOAT: + result += + std::to_string(pair.second.getFloat()); + break; + case EventValue::DOUBLE: + result += + std::to_string(pair.second.getDouble()); + break; + case EventValue::STRING: + result += "'" + pair.second.getString() + "'"; + break; + case EventValue::ENTITY_ID_ARRAY: { + result += "["; + const auto &arr = + pair.second.getEntityIdArray(); + for (size_t i = 0; i < arr.size(); i++) { + if (i > 0) + result += ", "; + result += "e:" + std::to_string(arr[i]); + } + result += "]"; + break; + } + case EventValue::INT_ARRAY: { + result += "["; + const auto &arr = pair.second.getIntArray(); + for (size_t i = 0; i < arr.size(); i++) { + if (i > 0) + result += ", "; + result += std::to_string(arr[i]); + } + result += "]"; + break; + } + case EventValue::FLOAT_ARRAY: { + result += "["; + const auto &arr = pair.second.getFloatArray(); + for (size_t i = 0; i < arr.size(); i++) { + if (i > 0) + result += ", "; + result += std::to_string(arr[i]); + } + result += "]"; + break; + } + case EventValue::DOUBLE_ARRAY: { + result += "["; + const auto &arr = pair.second.getDoubleArray(); + for (size_t i = 0; i < arr.size(); i++) { + if (i > 0) + result += ", "; + result += std::to_string(arr[i]); + } + result += "]"; + break; + } + case EventValue::STRING_ARRAY: { + result += "["; + const auto &arr = pair.second.getStringArray(); + for (size_t i = 0; i < arr.size(); i++) { + if (i > 0) + result += ", "; + result += "'" + arr[i] + "'"; + } + result += "]"; + break; + } + } + result += "\n"; + } + return result; + } + + // Allow LuaEventApi to access m_values directly for efficiency + friend EventParams readEventParams(lua_State *L, int idx); + +private: + std::unordered_map m_values; +}; + +} // namespace editScene + +#endif // EDITSCENE_EVENT_PARAMS_HPP diff --git a/src/features/editScene/lua-examples/debug_crash_example.lua b/src/features/editScene/lua-examples/debug_crash_example.lua new file mode 100644 index 0000000..3ba92ed --- /dev/null +++ b/src/features/editScene/lua-examples/debug_crash_example.lua @@ -0,0 +1,28 @@ +--[[ +debug_crash_example.lua + +Demonstrates the ecs.debug_crash() function which prints a message +and then deliberately crashes the application via std::abort(). + +Usage: + ecs.debug_crash("message") -- prints message and crashes + ecs.debug_crash() -- uses default message "debug_crash called" + +WARNING: This function will terminate the application! +]] + +-- Example 1: Crash with a custom message +-- Uncomment to test: +-- ecs.debug_crash("Something went terribly wrong!") + +-- Example 2: Crash with default message +-- Uncomment to test: +-- ecs.debug_crash() + +-- Example 3: Conditional crash for debugging +-- local health = ecs.get_field(player_id, 'Character', 'health') +-- if health <= 0 then +-- ecs.debug_crash("Player health dropped to zero!") +-- end + +print("debug_crash example loaded (not executed - uncomment to test)") diff --git a/src/features/editScene/lua-examples/dialogue_basic_show.lua b/src/features/editScene/lua-examples/dialogue_basic_show.lua index a92d9ca..1f1144e 100644 --- a/src/features/editScene/lua-examples/dialogue_basic_show.lua +++ b/src/features/editScene/lua-examples/dialogue_basic_show.lua @@ -46,10 +46,8 @@ print("Dialogue entity created with ID: " .. dialogue_entity) -- and the player can click anywhere to dismiss it. ecs.send_event("dialogue_show", { - stringValues = { - text = "Welcome to the world of World2!", - speaker = "Narrator" - } + text = "Welcome to the world of World2!", + speaker = "Narrator" }) print("Sent basic narration dialogue") @@ -61,11 +59,9 @@ print("Sent basic narration dialogue") -- buttons instead of click-to-progress. The player must pick one. ecs.send_event("dialogue_show", { - stringValues = { - text = "Where would you like to go?", - speaker = "Guide", - choices = "The Forest,The Village,The Mountains" - } + text = "Where would you like to go?", + speaker = "Guide", + choices = "The Forest,The Village,The Mountains" }) print("Sent dialogue with choices") @@ -75,9 +71,7 @@ print("Sent dialogue with choices") -- --------------------------------------------------------------------------- ecs.send_event("dialogue_show", { - stringValues = { - text = "A mysterious voice echoes through the chamber..." - } + text = "A mysterious voice echoes through the chamber..." }) print("Sent anonymous narration") @@ -87,10 +81,8 @@ print("Sent anonymous narration") -- --------------------------------------------------------------------------- ecs.send_event("dialogue_show", { - stringValues = { - text = "Greetings, traveler.\n\nI have been expecting you.\nThe prophecy spoke of your arrival.", - speaker = "Elder Marcus" - } + text = "Greetings, traveler.\n\nI have been expecting you.\nThe prophecy spoke of your arrival.", + speaker = "Elder Marcus" }) print("Sent multi-line dialogue") @@ -100,10 +92,12 @@ print("Sent multi-line dialogue") -- ============================================================================= -- To show dialogue from Lua: -- 1. Ensure an entity with DialogueComponent exists (create one if needed) --- 2. Call ecs.send_event("dialogue_show", { stringValues = { ... } }) +-- 2. Call ecs.send_event("dialogue_show", { text = "...", speaker = "...", choices = "..." }) -- 3. Required: text = "The narration text" -- 4. Optional: speaker = "Speaker Name" -- 5. Optional: choices = "Choice1,Choice2,Choice3" (comma-separated) +-- 6. EventParams uses flat key-value pairs (no nested stringValues/floatValues/etc.) +-- 7. Type metadata is available via params._types table -- ============================================================================= print("Dialogue basic show examples completed!") diff --git a/src/features/editScene/lua-examples/dialogue_component_api.lua b/src/features/editScene/lua-examples/dialogue_component_api.lua index ff4d7d6..ec583aa 100644 --- a/src/features/editScene/lua-examples/dialogue_component_api.lua +++ b/src/features/editScene/lua-examples/dialogue_component_api.lua @@ -163,10 +163,8 @@ function update_npc_dialogue(npc_entity, new_text, new_speaker) -- Show the updated dialogue via event (this triggers the state change) ecs.send_event("dialogue_show", { - stringValues = { - text = new_text, - speaker = new_speaker or ecs.get_field(npc_entity, "Dialogue", "speaker") - } + text = new_text, + speaker = new_speaker or ecs.get_field(npc_entity, "Dialogue", "speaker") }) end @@ -189,11 +187,9 @@ function show_dialogue_with_dynamic_choices(npc_entity, base_text, choice_list) -- Show via event (which handles state transitions properly) ecs.send_event("dialogue_show", { - stringValues = { - text = base_text, - speaker = ecs.get_field(npc_entity, "Dialogue", "speaker"), - choices = choices_str - } + text = base_text, + speaker = ecs.get_field(npc_entity, "Dialogue", "speaker"), + choices = choices_str }) end diff --git a/src/features/editScene/lua-examples/dialogue_event_handler.lua b/src/features/editScene/lua-examples/dialogue_event_handler.lua index 02f83ff..ec84f57 100644 --- a/src/features/editScene/lua-examples/dialogue_event_handler.lua +++ b/src/features/editScene/lua-examples/dialogue_event_handler.lua @@ -11,6 +11,8 @@ -- Combined with the EventBus, you can create complex event-driven dialogue -- sequences where one event triggers dialogue, and the player's choice -- triggers another event. +-- +-- Event parameters use the EventParams type with flat key-value pairs. -- ============================================================================= -- --------------------------------------------------------------------------- @@ -76,22 +78,16 @@ function on_player_near_npc(npc_name, distance) if distance < 5.0 then -- Send the event that the EventHandler is listening for ecs.send_event("player_approached", { - stringValues = { - npc_name = npc_name, - location = "town_square" - }, - floatValues = { - distance = distance - } + npc_name = npc_name, + location = "town_square", + distance = distance }) -- Also show dialogue directly ecs.send_event("dialogue_show", { - stringValues = { - text = "Hello there! I have a quest for a brave adventurer.", - speaker = npc_name, - choices = "I'll help!,What's the reward?,Not interested" - } + text = "Hello there! I have a quest for a brave adventurer.", + speaker = npc_name, + choices = "I'll help!,What's the reward?,Not interested" }) end end @@ -107,46 +103,36 @@ on_player_near_npc("QuestGiver", 3.0) -- Subscribe to dialogue choices local choice_sub = ecs.subscribe_event("dialogue_choice", function(event, params) - local choice_index = params.values and params.values.choice_index or 0 - local choice_text = params.stringValues and params.stringValues.choice_text or "" + local choice_index = params.choice_index or 0 + local choice_text = params.choice_text or "" if choice_text == "I'll help!" then -- Player accepted the quest - trigger quest acceptance event ecs.send_event("quest_accepted", { - stringValues = { - quest_name = "The Lost Artifact", - giver = "QuestGiver" - }, - values = { - reward_gold = 100, - reward_xp = 500 - } + quest_name = "The Lost Artifact", + giver = "QuestGiver", + reward_gold = 100, + reward_xp = 500 }) -- Show follow-up dialogue ecs.send_event("dialogue_show", { - stringValues = { - text = "Excellent! The ancient artifact was stolen from the temple.\nBring it back and you'll be richly rewarded!", - speaker = "QuestGiver", - choices = "Where is the temple?,I'm on it!,Tell me more" - } + text = "Excellent! The ancient artifact was stolen from the temple.\nBring it back and you'll be richly rewarded!", + speaker = "QuestGiver", + choices = "Where is the temple?,I'm on it!,Tell me more" }) elseif choice_text == "What's the reward?" then ecs.send_event("dialogue_show", { - stringValues = { - text = "100 gold pieces and a magical amulet! What do you say?", - speaker = "QuestGiver", - choices = "I'll help!,Sounds good,Maybe later" - } + text = "100 gold pieces and a magical amulet! What do you say?", + speaker = "QuestGiver", + choices = "I'll help!,Sounds good,Maybe later" }) elseif choice_text == "Not interested" then ecs.send_event("dialogue_show", { - stringValues = { - text = "Very well. The offer stands if you change your mind.", - speaker = "QuestGiver" - } + text = "Very well. The offer stands if you change your mind.", + speaker = "QuestGiver" }) end end) @@ -159,17 +145,17 @@ print("Subscribed to dialogue_choice for event chaining") -- Listen for quest acceptance local quest_sub = ecs.subscribe_event("quest_accepted", function(event, params) - local quest_name = params.stringValues and params.stringValues.quest_name or "unknown" - local reward = params.values and params.values.reward_gold or 0 - local xp = params.values and params.values.reward_xp or 0 + local quest_name = params.quest_name or "unknown" + local reward = params.reward_gold or 0 + local xp = params.reward_xp or 0 print("Quest accepted: " .. quest_name) print(" Reward: " .. reward .. " gold, " .. xp .. " XP") -- This could trigger other EventHandlers on other entities ecs.send_event("quest_log_updated", { - stringValues = { quest_name = quest_name }, - values = { active_quests = 1 } + quest_name = quest_name, + active_quests = 1 }) end) @@ -203,15 +189,13 @@ function on_zone_entered(zone_name) local dialogue = zone_dialogues[zone_name] if dialogue then ecs.send_event("dialogue_show", { - stringValues = { - text = dialogue.text, - speaker = dialogue.speaker - } + text = dialogue.text, + speaker = dialogue.speaker }) -- Also send a zone-specific event for other systems ecs.send_event("zone_entered", { - stringValues = { zone = zone_name } + zone = zone_name }) end end @@ -237,10 +221,8 @@ function on_item_picked_up(item_name, item_count) local message = pickup_messages[item_name] if message then ecs.send_event("dialogue_show", { - stringValues = { - text = message, - speaker = "Narrator" - } + text = message, + speaker = "Narrator" }) end end @@ -270,6 +252,9 @@ on_item_picked_up("gold_coins", 50) -- 5. Quest flow: -- Accept quest -> event -> update quest log -> next dialogue -- Complete quest -> event -> reward dialogue -> next dialogue +-- +-- EventParams uses flat key-value pairs. Type metadata is available +-- via params._types table (e.g., params._types.reward_gold = "int"). -- ============================================================================= print("Dialogue EventHandler integration examples completed!") diff --git a/src/features/editScene/lua-examples/dialogue_event_subscribe.lua b/src/features/editScene/lua-examples/dialogue_event_subscribe.lua index 5c2da7d..37080eb 100644 --- a/src/features/editScene/lua-examples/dialogue_event_subscribe.lua +++ b/src/features/editScene/lua-examples/dialogue_event_subscribe.lua @@ -11,6 +11,9 @@ -- "dialogue_show" - Fired when dialogue should be displayed -- "dialogue_choice" - Fired when player selects a choice -- "dialogue_dismiss" - Fired when dialogue is dismissed (no choices) +-- +-- Event parameters use the EventParams type, which supports flat +-- key-value pairs with typed values. Use params._types to check types. -- ============================================================================= -- --------------------------------------------------------------------------- @@ -29,8 +32,8 @@ ecs.add_component(dialogue_entity, "Dialogue") -- choice index. We bridge this via the EventBus. local choice_sub = ecs.subscribe_event("dialogue_choice", function(event, params) - local choice_index = params.values and params.values.choice_index or 0 - local choice_text = params.stringValues and params.stringValues.choice_text or "unknown" + local choice_index = params.choice_index or 0 + local choice_text = params.choice_text or "unknown" print("Player selected choice #" .. choice_index .. ": " .. choice_text) @@ -56,7 +59,7 @@ local dismiss_sub = ecs.subscribe_event("dialogue_dismiss", function(event, para print("Dialogue was dismissed by the player") -- You could trigger follow-up dialogue or game logic here - local next_text = params.stringValues and params.stringValues.next_text or "" + local next_text = params.next_text or "" if next_text ~= "" then print(" -> Next dialogue queued: " .. next_text) end @@ -69,9 +72,9 @@ print("Subscribed to dialogue_dismiss events (ID: " .. dismiss_sub .. ")") -- --------------------------------------------------------------------------- local show_sub = ecs.subscribe_event("dialogue_show", function(event, params) - local text = params.stringValues and params.stringValues.text or "" - local speaker = params.stringValues and params.stringValues.speaker or "Unknown" - local choices = params.stringValues and params.stringValues.choices or "" + local text = params.text or "" + local speaker = params.speaker or "Unknown" + local choices = params.choices or "" print("[Dialogue Log] " .. speaker .. ": \"" .. text .. "\"") @@ -90,11 +93,9 @@ print("Subscribed to dialogue_show events for logging (ID: " .. show_sub .. ")") function show_branching_dialogue() -- Step 1: Show the dialogue with choices ecs.send_event("dialogue_show", { - stringValues = { - text = "You see a dark cave entrance. What do you do?", - speaker = "Narrator", - choices = "Enter the cave,Look around first,Leave" - } + text = "You see a dark cave entrance. What do you do?", + speaker = "Narrator", + choices = "Enter the cave,Look around first,Leave" }) -- Step 2: The choice will be handled by our subscriber above. @@ -111,11 +112,9 @@ show_branching_dialogue() function npc_greeting(npc_name, greeting_text) -- Show initial greeting ecs.send_event("dialogue_show", { - stringValues = { - text = greeting_text, - speaker = npc_name, - choices = "Who are you?,Tell me about this place,Goodbye" - } + text = greeting_text, + speaker = npc_name, + choices = "Who are you?,Tell me about this place,Goodbye" }) -- The choice subscriber will handle the response. @@ -129,13 +128,16 @@ npc_greeting("Elder Marcus", "Ah, a new face in our village! Welcome, traveler." -- ============================================================================= -- To handle dialogue choices from Lua: -- 1. Subscribe to "dialogue_choice" events --- 2. Check params.values.choice_index (1-based) to see which was picked --- 3. Check params.stringValues.choice_text for the label text +-- 2. Check params.choice_index (1-based) to see which was picked +-- 3. Check params.choice_text for the label text -- 4. React accordingly in your game logic -- -- To handle dialogue dismissal: -- 1. Subscribe to "dialogue_dismiss" events -- 2. Trigger follow-up actions as needed +-- +-- EventParams uses flat key-value pairs. Type metadata is available +-- via params._types table (e.g., params._types.choice_index = "int"). -- ============================================================================= print("Dialogue event subscription examples completed!") diff --git a/src/features/editScene/lua-examples/dialogue_sequence.lua b/src/features/editScene/lua-examples/dialogue_sequence.lua index a7a2aec..41d5a92 100644 --- a/src/features/editScene/lua-examples/dialogue_sequence.lua +++ b/src/features/editScene/lua-examples/dialogue_sequence.lua @@ -35,8 +35,8 @@ local dialogue_queue_choice_text = "" -- Subscribe to choice events to unblock the queue local queue_choice_sub = ecs.subscribe_event("dialogue_choice", function(event, params) if dialogue_queue_pending then - dialogue_queue_choice = params.values and params.values.choice_index or 0 - dialogue_queue_choice_text = params.stringValues and params.stringValues.choice_text or "" + dialogue_queue_choice = params.choice_index or 0 + dialogue_queue_choice_text = params.choice_text or "" dialogue_queue_pending = false end end) @@ -61,11 +61,9 @@ end) function show_and_wait(text, speaker, choices) -- Send the dialogue event ecs.send_event("dialogue_show", { - stringValues = { - text = text, - speaker = speaker or "", - choices = choices or "" - } + text = text, + speaker = speaker or "", + choices = choices or "" }) -- Wait for player response @@ -97,10 +95,8 @@ function simple_conversation() -- Line 1: Narration with no choices (click to continue) ecs.send_event("dialogue_show", { - stringValues = { - text = "The old man sits by the fire, staring into the flames.", - speaker = "Narrator" - } + text = "The old man sits by the fire, staring into the flames.", + speaker = "Narrator" }) -- In a real game, you'd wait for the dismiss event here. @@ -108,11 +104,9 @@ function simple_conversation() -- Line 2: NPC speaks with choices ecs.send_event("dialogue_show", { - stringValues = { - text = "I've been expecting you. The darkness grows stronger each day.", - speaker = "Old Man", - choices = "Tell me more,How can I help?,I must go" - } + text = "I've been expecting you. The darkness grows stronger each day.", + speaker = "Old Man", + choices = "Tell me more,How can I help?,I must go" }) print(" (Player would now see choices and pick one)") @@ -232,11 +226,9 @@ function run_conversation(tree, start_node) -- Show the dialogue ecs.send_event("dialogue_show", { - stringValues = { - text = node.text, - speaker = node.speaker or "", - choices = choices_str - } + text = node.text, + speaker = node.speaker or "", + choices = choices_str }) -- In a real game, you'd wait for the player's choice here. @@ -278,40 +270,32 @@ function talk_to_elder_marcus() npc_state.marcus_friendship = 10 ecs.send_event("dialogue_show", { - stringValues = { - text = "Ah, a new face! I am Elder Marcus, keeper of this village.\nIt's been so long since we had visitors.", - speaker = "Elder Marcus", - choices = "Pleasure to meet you,I've heard stories about you,Hello" - } + text = "Ah, a new face! I am Elder Marcus, keeper of this village.\nIt's been so long since we had visitors.", + speaker = "Elder Marcus", + choices = "Pleasure to meet you,I've heard stories about you,Hello" }) elseif npc_state.quest_active and npc_state.quest_completed then -- Quest completed npc_state.marcus_friendship = npc_state.marcus_friendship + 50 ecs.send_event("dialogue_show", { - stringValues = { - text = "You did it! The village is safe thanks to you.\nPlease, take this reward.", - speaker = "Elder Marcus", - choices = "Thank you, elder,I was happy to help" - } + text = "You did it! The village is safe thanks to you.\nPlease, take this reward.", + speaker = "Elder Marcus", + choices = "Thank you, elder,I was happy to help" }) elseif npc_state.quest_active then -- Quest in progress ecs.send_event("dialogue_show", { - stringValues = { - text = "Have you dealt with those bandits yet?\nThe villagers are growing anxious.", - speaker = "Elder Marcus", - choices = "I'm working on it,I need more information,Not yet" - } + text = "Have you dealt with those bandits yet?\nThe villagers are growing anxious.", + speaker = "Elder Marcus", + choices = "I'm working on it,I need more information,Not yet" }) else -- Regular greeting ecs.send_event("dialogue_show", { - stringValues = { - text = "Welcome back, friend. The village is peaceful today.", - speaker = "Elder Marcus", - choices = "Any news?,I need supplies,Goodbye" - } + text = "Welcome back, friend. The village is peaceful today.", + speaker = "Elder Marcus", + choices = "Any news?,I need supplies,Goodbye" }) end end diff --git a/src/features/editScene/lua-examples/event_example.lua b/src/features/editScene/lua-examples/event_example.lua index 697fe7e..1c47f50 100644 --- a/src/features/editScene/lua-examples/event_example.lua +++ b/src/features/editScene/lua-examples/event_example.lua @@ -7,6 +7,20 @@ -- Events are a publish/subscribe mechanism. Any part of the game can send -- an event, and any subscriber can react to it. This decouples systems -- from each other. +-- +-- Event parameters use flat key-value pairs (no nested tables like +-- stringValues/floatValues/values). Each value is typed automatically: +-- - integer -> int +-- - number (non-integer) -> double +-- - string -> string +-- - table of integers -> int_array +-- - table of numbers -> double_array +-- - table of strings -> string_array +-- - table of entity IDs -> entity_id_array +-- +-- Type metadata is available via params._types table: +-- params._types.key = "int" | "double" | "string" | "int_array" | +-- "double_array" | "string_array" | "entity_id_array" -- ============================================================================= -- ============================================================================= @@ -28,10 +42,10 @@ print("Subscribed to 'hello' with ID: " .. sub_id) -- Send a simple event with no parameters: ecs.send_event("hello") --- Send an event with parameters: +-- Send an event with parameters (flat key-value pairs): ecs.send_event("hello", { - values = { count = 42 }, - stringValues = { message = "Hello World!" } + count = 42, + message = "Hello World!" }) -- ============================================================================= @@ -45,27 +59,22 @@ ecs.send_event("hello", { ecs.subscribe_event("player_damaged", function(event, params) print("Event: " .. event) if params then - if params.values then - print(" Damage: " .. (params.values.damage or 0)) - print(" Health remaining: " .. (params.values.health or 0)) - end - if params.stringValues then - print(" Source: " .. (params.stringValues.source or "unknown")) - end - if params.vec3Values then - local pos = params.vec3Values.position - if pos then - print(" Position: " .. pos[1] .. ", " .. pos[2] .. ", " .. pos[3]) - end + print(" Damage: " .. (params.damage or 0)) + print(" Health remaining: " .. (params.health or 0)) + print(" Source: " .. (params.source or "unknown")) + local pos = params.position + if pos then + print(" Position: " .. pos[1] .. ", " .. pos[2] .. ", " .. pos[3]) end end end) -- Send a damage event: ecs.send_event("player_damaged", { - values = { damage = 25, health = 75 }, - stringValues = { source = "goblin_archer" }, - vec3Values = { position = { 10, 0, 20 } } + damage = 25, + health = 75, + source = "goblin_archer", + position = { 10, 0, 20 } }) -- ============================================================================= @@ -110,53 +119,51 @@ ecs.send_event("multi") -- both fire again -- Event Parameter Types -- ============================================================================= --- Events can carry various types of data in their params table: +-- Events can carry various types of data in their params table. +-- All values are flat keys with automatic type inference: ecs.subscribe_event("data_event", function(event, params) print("Received data_event with:") if params then - -- Integer values: - if params.values then - for k, v in pairs(params.values) do - print(" int " .. k .. " = " .. v) + -- Print all keys with their types + for k, v in pairs(params) do + if k ~= "_types" then + local t = type(v) + if t == "table" then + local arr_str = "{" + for i, elem in ipairs(v) do + if i > 1 then arr_str = arr_str .. ", " end + arr_str = arr_str .. tostring(elem) + end + arr_str = arr_str .. "}" + print(" " .. k .. " (array) = " .. arr_str) + else + print(" " .. k .. " (" .. t .. ") = " .. tostring(v)) + end end end - -- Float values: - if params.floatValues then - for k, v in pairs(params.floatValues) do - print(" float " .. k .. " = " .. v) + -- Print type metadata + if params._types then + print(" Type metadata:") + for k, t in pairs(params._types) do + print(" " .. k .. " -> " .. t) end end - -- String values: - if params.stringValues then - for k, v in pairs(params.stringValues) do - print(" string " .. k .. " = '" .. v .. "'") - end - end - -- Vec3 values: - if params.vec3Values then - for k, v in pairs(params.vec3Values) do - print(" vec3 " .. k .. " = (" .. v[1] .. ", " .. v[2] .. ", " .. v[3] .. ")") - end - end - -- Bit flags: - if params.bits ~= nil then - print(" bits = " .. params.bits) - end - if params.mask ~= nil then - print(" mask = " .. params.mask) - end end end) -- Send an event with all parameter types: ecs.send_event("data_event", { - values = { score = 100, level = 5, kills = 42 }, - floatValues = { speed = 1.5, health = 75.5 }, - stringValues = { name = "Hero", state = "exploring" }, - vec3Values = { position = { 10, 20, 30 }, velocity = { 1, 0, 0 } }, - bits = 5, - mask = 7 + score = 100, + level = 5, + kills = 42, + speed = 1.5, + health = 75.5, + name = "Hero", + state = "exploring", + position = { 10, 20, 30 }, + velocity = { 1, 0, 0 }, + tags = { "warrior", "human", "player" } }) -- ============================================================================= @@ -170,41 +177,41 @@ local event_handlers = {} function register_game_handlers() -- Quest events: event_handlers.quest_complete = ecs.subscribe_event("quest_complete", function(event, params) - local quest_name = params and params.stringValues and params.stringValues.quest_name or "unknown" - local reward_xp = params and params.values and params.values.reward_xp or 0 + local quest_name = params and params.quest_name or "unknown" + local reward_xp = params and params.reward_xp or 0 print("Quest completed: " .. quest_name .. " (+" .. reward_xp .. " XP)") end) -- Combat events: event_handlers.enemy_killed = ecs.subscribe_event("enemy_killed", function(event, params) - local enemy = params and params.stringValues and params.stringValues.enemy_type or "unknown" - local xp = params and params.values and params.values.xp_reward or 0 + local enemy = params and params.enemy_type or "unknown" + local xp = params and params.xp_reward or 0 print("Killed " .. enemy .. " (+" .. xp .. " XP)") end) -- Item events: event_handlers.item_picked_up = ecs.subscribe_event("item_picked_up", function(event, params) - local item = params and params.stringValues and params.stringValues.item_name or "unknown" - local count = params and params.values and params.values.count or 1 + local item = params and params.item_name or "unknown" + local count = params and params.count or 1 print("Picked up " .. count .. "x " .. item) end) -- Dialogue events: event_handlers.dialogue_started = ecs.subscribe_event("dialogue_started", function(event, params) - local npc = params and params.stringValues and params.stringValues.npc_name or "unknown" + local npc = params and params.npc_name or "unknown" print("Started dialogue with " .. npc) end) -- Environment events: event_handlers.time_changed = ecs.subscribe_event("time_changed", function(event, params) - local hour = params and params.values and params.values.hour or 0 - local minute = params and params.values and params.values.minute or 0 + local hour = params and params.hour or 0 + local minute = params and params.minute or 0 print("Time changed to " .. hour .. ":" .. string.format("%02d", minute)) end) -- Player events: event_handlers.player_died = ecs.subscribe_event("player_died", function(event, params) - local killer = params and params.stringValues and params.stringValues.killed_by or "unknown" + local killer = params and params.killed_by or "unknown" print("Player was killed by " .. killer .. "!") end) @@ -215,26 +222,27 @@ register_game_handlers() -- Simulate some game events: ecs.send_event("quest_complete", { - stringValues = { quest_name = "The Lost Artifact" }, - values = { reward_xp = 500 } + quest_name = "The Lost Artifact", + reward_xp = 500 }) ecs.send_event("enemy_killed", { - stringValues = { enemy_type = "Goblin Warrior" }, - values = { xp_reward = 50 } + enemy_type = "Goblin Warrior", + xp_reward = 50 }) ecs.send_event("item_picked_up", { - stringValues = { item_name = "Health Potion" }, - values = { count = 2 } + item_name = "Health Potion", + count = 2 }) ecs.send_event("dialogue_started", { - stringValues = { npc_name = "Elder Marcus" } + npc_name = "Elder Marcus" }) ecs.send_event("time_changed", { - values = { hour = 18, minute = 30 } + hour = 18, + minute = 30 }) -- ============================================================================= @@ -273,13 +281,16 @@ end -- Send an event to all subscribers. -- Parameters: -- event_name - string, the name of the event to send --- params - optional table with event data: --- .values - table of integer key-value pairs --- .floatValues - table of float key-value pairs --- .stringValues - table of string key-value pairs --- .vec3Values - table of vec3 key-value pairs (each vec3 is {x, y, z}) --- .bits - integer bit flags --- .mask - integer bit mask +-- params - optional table with flat key-value pairs: +-- key = integer -> stored as int +-- key = number -> stored as double +-- key = string -> stored as string +-- key = {int, ...} -> stored as int_array +-- key = {num, ...} -> stored as double_array +-- key = {str, ...} -> stored as string_array +-- Type metadata is available via params._types table: +-- params._types.key = "int" | "double" | "string" | +-- "int_array" | "double_array" | "string_array" -- ============================================================================= print("Event API examples completed successfully!") diff --git a/src/features/editScene/lua-examples/game_mode_example.lua b/src/features/editScene/lua-examples/game_mode_example.lua new file mode 100644 index 0000000..cc80627 --- /dev/null +++ b/src/features/editScene/lua-examples/game_mode_example.lua @@ -0,0 +1,135 @@ +-- ============================================================================= +-- Game Mode Lua API Examples +-- ============================================================================= +-- This file demonstrates how to query the editor/game mode state from Lua +-- using the ecs.* Lua API. +-- +-- The application can be in one of two modes: +-- - Editor mode: the scene editor is active +-- - Game mode: the game is running (with sub-states: menu, playing, paused) +-- +-- These functions allow Lua scripts to adapt their behavior based on the +-- current application mode. +-- ============================================================================= + +-- ============================================================================= +-- Querying the Current Mode +-- ============================================================================= + +-- Check if we are in editor mode: +if ecs.is_editor_mode() then + print("Application is in EDITOR mode") +end + +-- Check if we are in game mode (any play state): +if ecs.is_game_mode() then + print("Application is in GAME mode") +end + +-- Get the mode as a string: +local mode = ecs.get_game_mode() +print("Current mode: " .. mode) -- "editor" or "game" + +-- ============================================================================= +-- Querying the Gameplay State (only meaningful in game mode) +-- ============================================================================= + +-- Check specific gameplay states: +if ecs.is_game_playing() then + print("Game is PLAYING") +end + +if ecs.is_game_menu() then + print("Game is in MENU") +end + +if ecs.is_game_paused() then + print("Game is PAUSED") +end + +-- Get the play state as a string: +local state = ecs.get_game_play_state() +print("Game play state: " .. state) -- "menu", "playing", or "paused" + +-- ============================================================================= +-- Practical Examples +-- ============================================================================= + +-- Example 1: Only run editor-specific logic in editor mode +function update_editor_ui() + if ecs.is_editor_mode() then + -- Show editor UI elements + print("Updating editor UI...") + end +end + +-- Example 2: Only process game input when game is playing +function process_game_input() + if ecs.is_game_playing() then + -- Process player input + print("Processing game input...") + end +end + +-- Example 3: Show/hide pause menu +function toggle_pause_menu() + if ecs.is_game_paused() then + -- Show pause menu overlay + print("Showing pause menu...") + else + -- Hide pause menu + print("Hiding pause menu...") + end +end + +-- Example 4: Conditional behavior based on mode +function on_entity_clicked(entity_id) + if ecs.is_editor_mode() then + -- In editor: select the entity + print("Selected entity " .. entity_id .. " in editor") + elseif ecs.is_game_playing() then + -- In game: interact with the entity + print("Interacting with entity " .. entity_id) + end +end + +-- ============================================================================= +-- Using Mode Queries in Event Handlers +-- ============================================================================= + +-- Register an event handler that checks mode: +function on_frame_update() + if ecs.is_game_playing() then + -- Update game logic + elseif ecs.is_editor_mode() then + -- Update editor logic + end +end + +-- ============================================================================= +-- Error Handling +-- ============================================================================= + +-- All functions return valid values even if called at unexpected times: +local m = ecs.get_game_mode() +assert(type(m) == "string", "get_game_mode should return a string") + +local s = ecs.get_game_play_state() +assert(type(s) == "string", "get_game_play_state should return a string") + +local b1 = ecs.is_editor_mode() +assert(type(b1) == "boolean", "is_editor_mode should return a boolean") + +local b2 = ecs.is_game_mode() +assert(type(b2) == "boolean", "is_game_mode should return a boolean") + +local b3 = ecs.is_game_playing() +assert(type(b3) == "boolean", "is_game_playing should return a boolean") + +local b4 = ecs.is_game_menu() +assert(type(b4) == "boolean", "is_game_menu should return a boolean") + +local b5 = ecs.is_game_paused() +assert(type(b5) == "boolean", "is_game_paused should return a boolean") + +print("Game mode API examples completed successfully!") diff --git a/src/features/editScene/lua/LuaEventApi.cpp b/src/features/editScene/lua/LuaEventApi.cpp index 8bdec8a..8cf5cc5 100644 --- a/src/features/editScene/lua/LuaEventApi.cpp +++ b/src/features/editScene/lua/LuaEventApi.cpp @@ -1,9 +1,8 @@ #include "LuaEventApi.hpp" #include "LuaEntityApi.hpp" #include "../systems/EventBus.hpp" -#include "../components/GoapBlackboard.hpp" +#include "../components/EventParams.hpp" #include -#include #include namespace editScene @@ -23,161 +22,312 @@ static std::unordered_map s_luaSubscriptions; static int s_nextLuaSubId = 1; // --------------------------------------------------------------------------- -// Helper: push a GoapBlackboard as a Lua table +// Helper: push an EventValue as a Lua value // --------------------------------------------------------------------------- /** - * @brief Push a GoapBlackboard as a Lua table with named fields. + * @brief Push a single EventValue onto the Lua stack. * - * The resulting table has: - * .bits -> integer - * .mask -> integer - * .values -> {string -> int} - * .floatValues -> {string -> float} - * .vec3Values -> {string -> {x, y, z}} - * .stringValues -> {string -> string} - * - * @param L Lua state. - * @param bb The GoapBlackboard to convert. + * @param L Lua state. + * @param val The EventValue to push. */ -static void pushGoapBlackboard(lua_State *L, const GoapBlackboard &bb) +static void pushEventValue(lua_State *L, const EventValue &val) { - lua_newtable(L); - - // bits and mask - lua_pushinteger(L, (lua_Integer)bb.bits); - lua_setfield(L, -2, "bits"); - - lua_pushinteger(L, (lua_Integer)bb.mask); - lua_setfield(L, -2, "mask"); - - // values (int map) - lua_newtable(L); - for (auto &kv : bb.values) { - lua_pushinteger(L, kv.second); - lua_setfield(L, -2, kv.first.c_str()); - } - lua_setfield(L, -2, "values"); - - // floatValues - lua_newtable(L); - for (auto &kv : bb.floatValues) { - lua_pushnumber(L, kv.second); - lua_setfield(L, -2, kv.first.c_str()); - } - lua_setfield(L, -2, "floatValues"); - - // vec3Values - lua_newtable(L); - for (auto &kv : bb.vec3Values) { + switch (val.getType()) { + case EventValue::NIL: + lua_pushnil(L); + break; + case EventValue::ENTITY_ID: + lua_pushinteger(L, (lua_Integer)val.getEntityId()); + break; + case EventValue::INT: + lua_pushinteger(L, (lua_Integer)val.getInt()); + break; + case EventValue::FLOAT: + lua_pushnumber(L, (lua_Number)val.getFloat()); + break; + case EventValue::DOUBLE: + lua_pushnumber(L, (lua_Number)val.getDouble()); + break; + case EventValue::STRING: + lua_pushstring(L, val.getString().c_str()); + break; + case EventValue::ENTITY_ID_ARRAY: { lua_newtable(L); - lua_pushnumber(L, kv.second.x); - lua_rawseti(L, -2, 1); - lua_pushnumber(L, kv.second.y); - lua_rawseti(L, -2, 2); - lua_pushnumber(L, kv.second.z); - lua_rawseti(L, -2, 3); - lua_setfield(L, -2, kv.first.c_str()); + const auto &arr = val.getEntityIdArray(); + for (size_t i = 0; i < arr.size(); i++) { + lua_pushinteger(L, (lua_Integer)arr[i]); + lua_rawseti(L, -2, (int)(i + 1)); + } + break; + } + case EventValue::INT_ARRAY: { + lua_newtable(L); + const auto &arr = val.getIntArray(); + for (size_t i = 0; i < arr.size(); i++) { + lua_pushinteger(L, (lua_Integer)arr[i]); + lua_rawseti(L, -2, (int)(i + 1)); + } + break; + } + case EventValue::FLOAT_ARRAY: { + lua_newtable(L); + const auto &arr = val.getFloatArray(); + for (size_t i = 0; i < arr.size(); i++) { + lua_pushnumber(L, (lua_Number)arr[i]); + lua_rawseti(L, -2, (int)(i + 1)); + } + break; + } + case EventValue::DOUBLE_ARRAY: { + lua_newtable(L); + const auto &arr = val.getDoubleArray(); + for (size_t i = 0; i < arr.size(); i++) { + lua_pushnumber(L, (lua_Number)arr[i]); + lua_rawseti(L, -2, (int)(i + 1)); + } + break; + } + case EventValue::STRING_ARRAY: { + lua_newtable(L); + const auto &arr = val.getStringArray(); + for (size_t i = 0; i < arr.size(); i++) { + lua_pushstring(L, arr[i].c_str()); + lua_rawseti(L, -2, (int)(i + 1)); + } + break; } - lua_setfield(L, -2, "vec3Values"); - - // stringValues - lua_newtable(L); - for (auto &kv : bb.stringValues) { - lua_pushstring(L, kv.second.c_str()); - lua_setfield(L, -2, kv.first.c_str()); } - lua_setfield(L, -2, "stringValues"); } /** - * @brief Read a Lua table at the given index as a GoapBlackboard. + * @brief Push the type name string for an EventValue type. + */ +static const char *eventValueTypeName(EventValue::Type type) +{ + switch (type) { + case EventValue::NIL: + return "nil"; + case EventValue::ENTITY_ID: + return "entity_id"; + case EventValue::INT: + return "int"; + case EventValue::FLOAT: + return "float"; + case EventValue::DOUBLE: + return "double"; + case EventValue::STRING: + return "string"; + case EventValue::ENTITY_ID_ARRAY: + return "entity_id_array"; + case EventValue::INT_ARRAY: + return "int_array"; + case EventValue::FLOAT_ARRAY: + return "float_array"; + case EventValue::DOUBLE_ARRAY: + return "double_array"; + case EventValue::STRING_ARRAY: + return "string_array"; + } + return "nil"; +} + +// --------------------------------------------------------------------------- +// Helper: push an EventParams as a Lua table +// --------------------------------------------------------------------------- + +/** + * @brief Push an EventParams as a Lua table with named fields. * - * Expects the same format as pushGoapBlackboard produces. + * The resulting table has each key mapped to its value, plus a _types + * sub-table that maps each key to its type name string. + * + * @param L Lua state. + * @param params The EventParams to convert. + */ +static void pushEventParams(lua_State *L, const EventParams ¶ms) +{ + lua_newtable(L); + + // Push _types sub-table + lua_newtable(L); + + for (EventParams::ConstIterator it = params.begin(); it != params.end(); + ++it) { + const std::string &key = it->first; + const EventValue &val = it->second; + + // Push the value + pushEventValue(L, val); + lua_setfield(L, -3, key.c_str()); + + // Push the type name into _types + lua_pushstring(L, eventValueTypeName(val.getType())); + lua_setfield(L, -2, key.c_str()); + } + + // Set _types sub-table + lua_setfield(L, -2, "_types"); +} + +// --------------------------------------------------------------------------- +// Helper: read a Lua value as an EventValue +// --------------------------------------------------------------------------- + +/** + * @brief Read a Lua value at the given index as an EventValue. + * + * Type inference rules: + * - integer -> INT + * - number (non-integer) -> DOUBLE + * - string -> STRING + * - table with all integer keys -> array (type inferred from elements) + * - table with string keys -> not supported at value level * * @param L Lua state. - * @param idx Stack index of the table. - * @return GoapBlackboard populated from the table. + * @param idx Stack index of the value. + * @return EventValue representing the Lua value. */ -static GoapBlackboard readGoapBlackboard(lua_State *L, int idx) +static EventValue readLuaValue(lua_State *L, int idx) { - GoapBlackboard bb; + int absIdx = lua_absindex(L, idx); + int type = lua_type(L, absIdx); - // bits - lua_getfield(L, idx, "bits"); - if (lua_isnumber(L, -1)) - bb.bits = (uint64_t)lua_tointeger(L, -1); - lua_pop(L, 1); + switch (type) { + case LUA_TNIL: + return EventValue(); - // mask - lua_getfield(L, idx, "mask"); - if (lua_isnumber(L, -1)) - bb.mask = (uint64_t)lua_tointeger(L, -1); - lua_pop(L, 1); - - // values (int map) - lua_getfield(L, idx, "values"); - if (lua_istable(L, -1)) { - lua_pushnil(L); - while (lua_next(L, -2) != 0) { - if (lua_isstring(L, -2) && lua_isnumber(L, -1)) - bb.values[lua_tostring(L, -2)] = - (int)lua_tointeger(L, -1); - lua_pop(L, 1); + case LUA_TNUMBER: { + lua_Number num = lua_tonumber(L, absIdx); + lua_Integer intVal = lua_tointeger(L, absIdx); + // Check if it's an integer (within precision) + if ((lua_Number)intVal == num) { + return EventValue((int64_t)intVal); } + return EventValue((double)num); } - lua_pop(L, 1); - // floatValues - lua_getfield(L, idx, "floatValues"); - if (lua_istable(L, -1)) { - lua_pushnil(L); - while (lua_next(L, -2) != 0) { - if (lua_isstring(L, -2) && lua_isnumber(L, -1)) - bb.floatValues[lua_tostring(L, -2)] = - (float)lua_tonumber(L, -1); - lua_pop(L, 1); - } - } - lua_pop(L, 1); + case LUA_TSTRING: + return EventValue(lua_tostring(L, absIdx)); + + case LUA_TBOOLEAN: + return EventValue((int64_t)(lua_toboolean(L, absIdx) ? 1 : 0)); + + case LUA_TTABLE: { + // Check if it's an array (all integer keys) + bool isArray = true; + int maxKey = 0; + bool hasStringElements = false; + bool hasNumberElements = false; + bool hasIntegerElements = false; - // vec3Values - lua_getfield(L, idx, "vec3Values"); - if (lua_istable(L, -1)) { lua_pushnil(L); - while (lua_next(L, -2) != 0) { - if (lua_isstring(L, -2) && lua_istable(L, -1)) { - Ogre::Vector3 v; - lua_rawgeti(L, -1, 1); - v.x = (float)lua_tonumber(L, -1); - lua_pop(L, 1); - lua_rawgeti(L, -1, 2); - v.y = (float)lua_tonumber(L, -1); - lua_pop(L, 1); - lua_rawgeti(L, -1, 3); - v.z = (float)lua_tonumber(L, -1); - lua_pop(L, 1); - bb.vec3Values[lua_tostring(L, -2)] = v; + while (lua_next(L, absIdx) != 0) { + if (lua_type(L, -2) == LUA_TNUMBER) { + int k = (int)lua_tointeger(L, -2); + if (k > maxKey) + maxKey = k; + int elemType = lua_type(L, -1); + if (elemType == LUA_TSTRING) + hasStringElements = true; + else if (elemType == LUA_TNUMBER) { + lua_Number num = lua_tonumber(L, -1); + lua_Integer intVal = + lua_tointeger(L, -1); + if ((lua_Number)intVal == num) + hasIntegerElements = true; + else + hasNumberElements = true; + } + } else { + isArray = false; } lua_pop(L, 1); } - } - lua_pop(L, 1); - // stringValues - lua_getfield(L, idx, "stringValues"); - if (lua_istable(L, -1)) { - lua_pushnil(L); - while (lua_next(L, -2) != 0) { - if (lua_isstring(L, -2) && lua_isstring(L, -1)) - bb.stringValues[lua_tostring(L, -2)] = - lua_tostring(L, -1); - lua_pop(L, 1); + if (isArray && maxKey > 0) { + if (hasStringElements) { + std::vector arr; + for (int i = 1; i <= maxKey; i++) { + lua_rawgeti(L, absIdx, i); + if (lua_type(L, -1) == LUA_TSTRING) + arr.push_back( + lua_tostring(L, -1)); + lua_pop(L, 1); + } + return EventValue(arr); + } else if (hasIntegerElements) { + std::vector arr; + for (int i = 1; i <= maxKey; i++) { + lua_rawgeti(L, absIdx, i); + if (lua_type(L, -1) == LUA_TNUMBER) + arr.push_back( + (int64_t)lua_tointeger( + L, -1)); + lua_pop(L, 1); + } + return EventValue(arr); + } else if (hasNumberElements) { + std::vector arr; + for (int i = 1; i <= maxKey; i++) { + lua_rawgeti(L, absIdx, i); + if (lua_type(L, -1) == LUA_TNUMBER) + arr.push_back( + lua_tonumber(L, -1)); + lua_pop(L, 1); + } + return EventValue(arr); + } } - } - lua_pop(L, 1); - return bb; + // Not an array or empty - return nil + return EventValue(); + } + + default: + return EventValue(); + } +} + +// --------------------------------------------------------------------------- +// Helper: read a Lua table as EventParams +// --------------------------------------------------------------------------- + +/** + * @brief Read a Lua table at the given index as EventParams. + * + * Each key-value pair in the table is converted to an EventValue. + * The _types sub-table is NOT read back (types are inferred from values). + * + * @param L Lua state. + * @param idx Stack index of the table. + * @return EventParams populated from the table. + */ +EventParams readEventParams(lua_State *L, int idx) +{ + EventParams params; + int absIdx = lua_absindex(L, idx); + + if (!lua_istable(L, absIdx)) + return params; + + lua_pushnil(L); + while (lua_next(L, absIdx) != 0) { + if (lua_type(L, -2) == LUA_TSTRING) { + const char *key = lua_tostring(L, -2); + if (key) { + // Skip _types key (metadata) + if (strcmp(key, "_types") == 0) { + lua_pop(L, 1); + continue; + } + params.m_values[key] = readLuaValue(L, -1); + } + } + lua_pop(L, 1); + } + + return params; } // --------------------------------------------------------------------------- @@ -188,19 +338,20 @@ static GoapBlackboard readGoapBlackboard(lua_State *L, int idx) * @brief Lua: ecs.send_event("eventName", [params_table]) -> nil * * Sends an event through the global EventBus. The optional params table - * is converted to a GoapBlackboard payload. + * is converted to an EventParams payload. * * Usage: * ecs.send_event("collision") * ecs.send_event("collision", { entity_id = 42, damage = 10 }) + * ecs.send_event("collision", { targets = {100, 101, 102} }) */ static int luaSendEvent(lua_State *L) { const char *eventName = luaL_checkstring(L, 1); - GoapBlackboard params; + EventParams params; if (lua_gettop(L) >= 2 && lua_istable(L, 2)) { - params = readGoapBlackboard(L, 2); + params = readEventParams(L, 2); } EventBus::getInstance().send(eventName, params); @@ -222,7 +373,7 @@ static int luaSendEvent(lua_State *L) * Usage: * local sub_id = ecs.subscribe_event("collision", function(event, params) * print("Collision event received!") - * print("entity_id: " .. params.values.entity_id) + * print("entity_id: " .. params.entity_id) * end) */ static int luaSubscribeEvent(lua_State *L) @@ -237,7 +388,7 @@ static int luaSubscribeEvent(lua_State *L) // Subscribe to the EventBus with a C++ lambda that calls the Lua function EventBus::ListenerId listenerId = EventBus::getInstance().subscribe( eventName, [L, callbackRef](const Ogre::String &eventName, - const GoapBlackboard ¶ms) { + const EventParams ¶ms) { // Push the Lua callback function lua_rawgeti(L, LUA_REGISTRYINDEX, callbackRef); @@ -245,7 +396,7 @@ static int luaSubscribeEvent(lua_State *L) lua_pushstring(L, eventName.c_str()); // Push params as a Lua table - pushGoapBlackboard(L, params); + pushEventParams(L, params); // Call the Lua function (2 args, 0 results) if (lua_pcall(L, 2, 0, 0) != LUA_OK) { diff --git a/src/features/editScene/lua/LuaEventApi.hpp b/src/features/editScene/lua/LuaEventApi.hpp index 9c276e8..ee97f50 100644 --- a/src/features/editScene/lua/LuaEventApi.hpp +++ b/src/features/editScene/lua/LuaEventApi.hpp @@ -13,8 +13,9 @@ * event subscriptions. * * The EventBus is a synchronous publish/subscribe system. Events are - * identified by name strings. Payloads use GoapBlackboard, which - * supports int, float, Vector3, and string values. + * identified by name strings. Payloads use EventParams, which + * supports entity IDs, integers, floats, doubles, strings, and + * arrays of each type. * * Exposed Lua globals (in the "ecs" table): * ecs.send_event("eventName") -> nil @@ -28,13 +29,22 @@ * print("entity_id: " .. params.entity_id) * end) * - * The params table contains the GoapBlackboard fields: - * params.bits -> integer (bitfield) - * params.mask -> integer (bitmask) - * params.values -> table {string -> int} - * params.floatValues -> table {string -> float} - * params.vec3Values -> table {string -> {x, y, z}} - * params.stringValues -> table {string -> string} + * The params table contains EventParams fields. Each value is typed: + * params.entity_id -> integer (entity ID) + * params.int_val -> integer + * params.float_val -> number (float) + * params.double_val -> number (double) + * params.string_val -> string + * params.entity_ids -> table {integer, ...} (array of entity IDs) + * params.int_array -> table {integer, ...} + * params.float_array -> table {number, ...} + * params.double_array -> table {number, ...} + * params.string_array -> table {string, ...} + * + * Type metadata is available via params._types table: + * params._types.key = "entity_id" | "int" | "float" | "double" | + * "string" | "entity_id_array" | "int_array" | + * "float_array" | "double_array" | "string_array" */ namespace editScene diff --git a/src/features/editScene/lua/LuaGameModeApi.cpp b/src/features/editScene/lua/LuaGameModeApi.cpp new file mode 100644 index 0000000..7f2ab49 --- /dev/null +++ b/src/features/editScene/lua/LuaGameModeApi.cpp @@ -0,0 +1,171 @@ +#include "LuaGameModeApi.hpp" +#include "../GameMode.hpp" +#include +#include +#include + +namespace editScene +{ + +// --------------------------------------------------------------------------- +// Lua: ecs.is_editor_mode() -> bool +// --------------------------------------------------------------------------- + +static int luaIsEditorMode(lua_State *L) +{ + (void)L; + lua_pushboolean(L, isEditorMode() ? 1 : 0); + return 1; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.is_game_mode() -> bool +// --------------------------------------------------------------------------- + +static int luaIsGameMode(lua_State *L) +{ + (void)L; + lua_pushboolean(L, isGameMode() ? 1 : 0); + return 1; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.is_game_playing() -> bool +// --------------------------------------------------------------------------- + +static int luaIsGamePlaying(lua_State *L) +{ + (void)L; + lua_pushboolean(L, isGamePlaying() ? 1 : 0); + return 1; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.is_game_menu() -> bool +// --------------------------------------------------------------------------- + +static int luaIsGameMenu(lua_State *L) +{ + (void)L; + lua_pushboolean(L, isGameMenu() ? 1 : 0); + return 1; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.is_game_paused() -> bool +// --------------------------------------------------------------------------- + +static int luaIsGamePaused(lua_State *L) +{ + (void)L; + lua_pushboolean(L, isGamePaused() ? 1 : 0); + return 1; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.get_game_mode() -> string ("editor" or "game") +// --------------------------------------------------------------------------- + +static int luaGetGameMode(lua_State *L) +{ + (void)L; + switch (getGameMode()) { + case GameMode::Editor: + lua_pushstring(L, "editor"); + break; + case GameMode::Game: + lua_pushstring(L, "game"); + break; + } + return 1; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.get_game_play_state() -> string ("menu", "playing", or "paused") +// --------------------------------------------------------------------------- + +static int luaGetGamePlayState(lua_State *L) +{ + (void)L; + switch (getGamePlayState()) { + case GamePlayState::Menu: + lua_pushstring(L, "menu"); + break; + case GamePlayState::Playing: + lua_pushstring(L, "playing"); + break; + case GamePlayState::Paused: + lua_pushstring(L, "paused"); + break; + } + return 1; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.debug_crash("message") -> crashes the application +// --------------------------------------------------------------------------- + +static int luaDebugCrash(lua_State *L) +{ + const char *msg = luaL_optstring(L, 1, "debug_crash called"); + + // Log the message + Ogre::LogManager::getSingleton().logMessage("Lua debug_crash: " + + Ogre::String(msg)); + + // Print to stderr as well + std::fprintf(stderr, "Lua debug_crash: %s\n", msg); + std::fflush(stderr); + + // Trigger a deliberate crash + // Use abort() which is cross-platform and raises SIGABRT + std::abort(); + + return 0; +} + +// --------------------------------------------------------------------------- +// Register all game-mode API functions +// --------------------------------------------------------------------------- + +void registerLuaGameModeApi(lua_State *L) +{ + // Get or create the "ecs" global table + lua_getglobal(L, "ecs"); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + } + + // Predicates + lua_pushcfunction(L, luaIsEditorMode); + lua_setfield(L, -2, "is_editor_mode"); + + lua_pushcfunction(L, luaIsGameMode); + lua_setfield(L, -2, "is_game_mode"); + + lua_pushcfunction(L, luaIsGamePlaying); + lua_setfield(L, -2, "is_game_playing"); + + lua_pushcfunction(L, luaIsGameMenu); + lua_setfield(L, -2, "is_game_menu"); + + lua_pushcfunction(L, luaIsGamePaused); + lua_setfield(L, -2, "is_game_paused"); + + // Query functions + lua_pushcfunction(L, luaGetGameMode); + lua_setfield(L, -2, "get_game_mode"); + + lua_pushcfunction(L, luaGetGamePlayState); + lua_setfield(L, -2, "get_game_play_state"); + + // Debug crash function + lua_pushcfunction(L, luaDebugCrash); + lua_setfield(L, -2, "debug_crash"); + + // Set the global + lua_setglobal(L, "ecs"); +} + +} // namespace editScene diff --git a/src/features/editScene/lua/LuaGameModeApi.hpp b/src/features/editScene/lua/LuaGameModeApi.hpp new file mode 100644 index 0000000..369b7c2 --- /dev/null +++ b/src/features/editScene/lua/LuaGameModeApi.hpp @@ -0,0 +1,39 @@ +#ifndef EDITSCENE_LUA_GAMEMODE_API_HPP +#define EDITSCENE_LUA_GAMEMODE_API_HPP +#pragma once + +#include + +/** + * @file LuaGameModeApi.hpp + * @brief Lua API for querying editor/game mode state. + * + * Provides functions to check whether the application is in editor mode + * or game mode, and what the current gameplay state is. + * + * Exposed Lua globals (in the "ecs" table): + * ecs.is_editor_mode() -> bool + * ecs.is_game_mode() -> bool + * ecs.is_game_playing() -> bool + * ecs.is_game_menu() -> bool + * ecs.is_game_paused() -> bool + * ecs.get_game_mode() -> string ("editor" or "game") + * ecs.get_game_play_state() -> string ("menu", "playing", or "paused") + * ecs.debug_crash(msg) -> prints msg to log/stderr, then calls std::abort() + */ + +namespace editScene +{ + +/** + * @brief Register all game-mode Lua API functions into the "ecs" global table. + * + * Adds mode query functions to the existing "ecs" table. + * + * @param L The Lua state. + */ +void registerLuaGameModeApi(lua_State *L); + +} // namespace editScene + +#endif // EDITSCENE_LUA_GAMEMODE_API_HPP diff --git a/src/features/editScene/systems/BehaviorTreeSystem.cpp b/src/features/editScene/systems/BehaviorTreeSystem.cpp index 409a1ee..0e2aee6 100644 --- a/src/features/editScene/systems/BehaviorTreeSystem.cpp +++ b/src/features/editScene/systems/BehaviorTreeSystem.cpp @@ -38,9 +38,10 @@ static bool parseValueString(const Ogre::String &str, int &outInt, float &outFloat, Ogre::Vector3 &outVec3, int &type); -/** Parse "key=val,key2=val2" params into a GoapBlackboard. - * Values are auto-detected: int, float, vec3 (x,y,z), or quoted string. */ -static void parseEventParams(const Ogre::String &str, GoapBlackboard &out) +/** Parse "key=val,key2=val2" params into an EventParams. + * Values are auto-detected: int, float, or quoted string. */ +static void parseEventParams(const Ogre::String &str, + editScene::EventParams &out) { if (str.empty()) return; @@ -94,23 +95,23 @@ static void parseEventParams(const Ogre::String &str, GoapBlackboard &out) if (val.size() >= 2 && val.front() == '"' && val.back() == '"') { val = val.substr(1, val.size() - 2); - out.setStringValue(key, val); + out.setString(key, val); } else { - // Try int/float/vec3 + // Try int/float int iVal; float fVal; Ogre::Vector3 vVal; int vType; if (parseValueString(val, iVal, fVal, vVal, vType)) { if (vType == 0) - out.setValue(key, iVal); + out.setInt(key, iVal); else if (vType == 1) - out.setFloatValue(key, fVal); + out.setFloat(key, fVal); else - out.setVec3Value(key, vVal); + out.setFloat(key, vVal.x); } else { // Fallback to string - out.setStringValue(key, val); + out.setString(key, val); } } @@ -338,7 +339,7 @@ BehaviorTreeSystem::evaluateNode(const BehaviorTreeNode &node, flecs::entity e, if (node.type == "sendEvent") { if (isNewlyActive(state, &node)) { - GoapBlackboard params; + editScene::EventParams params; parseEventParams(node.params, params); EventBus::getInstance().send(node.name, params); } @@ -993,7 +994,8 @@ BehaviorTreeSystem::evaluateNode(const BehaviorTreeNode &node, flecs::entity e, * 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 == 0) + return Status::success; if (result == 1) return Status::failure; if (result == 2) diff --git a/src/features/editScene/systems/DialogueSystem.cpp b/src/features/editScene/systems/DialogueSystem.cpp index c3e7847..72ce87b 100644 --- a/src/features/editScene/systems/DialogueSystem.cpp +++ b/src/features/editScene/systems/DialogueSystem.cpp @@ -2,7 +2,7 @@ #include "../EditorApp.hpp" #include "../components/DialogueComponent.hpp" #include "../systems/EventBus.hpp" -#include "../components/GoapBlackboard.hpp" +#include "../components/EventParams.hpp" #include #include #include @@ -18,8 +18,8 @@ DialogueSystem::DialogueSystem(flecs::world &world, { // Subscribe to dialogue events EventBus::getInstance().subscribe( - "dialogue_show", - [this](const Ogre::String &, const GoapBlackboard ¶ms) { + "dialogue_show", [this](const Ogre::String &, + const editScene::EventParams ¶ms) { // Find the first entity with DialogueComponent m_world.query().each([&](flecs::entity e, @@ -28,8 +28,7 @@ DialogueSystem::DialogueSystem(flecs::world &world, if (!dc.enabled) return; - Ogre::String text = - params.getStringValue("text"); + Ogre::String text = params.getString("text"); if (text.empty()) return; @@ -37,7 +36,7 @@ DialogueSystem::DialogueSystem(flecs::world &world, // string std::vector choices; Ogre::String choicesStr = - params.getStringValue("choices"); + params.getString("choices"); if (!choicesStr.empty()) { Ogre::String::size_type start = 0; Ogre::String::size_type end; @@ -57,7 +56,7 @@ DialogueSystem::DialogueSystem(flecs::world &world, } Ogre::String speaker = - params.getStringValue("speaker"); + params.getString("speaker"); dc.show(text, choices, speaker); }); diff --git a/src/features/editScene/systems/DialogueSystem.hpp b/src/features/editScene/systems/DialogueSystem.hpp index 5cacb1b..95d755d 100644 --- a/src/features/editScene/systems/DialogueSystem.hpp +++ b/src/features/editScene/systems/DialogueSystem.hpp @@ -19,7 +19,7 @@ class EditorApp; * of the screen, showing narration text and optional player choices. * * The dialogue can be triggered via: - * 1. EventBus event "dialogue_show" with GoapBlackboard payload + * 1. EventBus event "dialogue_show" with EventParams payload * 2. Direct API on DialogueComponent */ class DialogueSystem { diff --git a/src/features/editScene/systems/EventBus.cpp b/src/features/editScene/systems/EventBus.cpp index 5505012..8c3c20d 100644 --- a/src/features/editScene/systems/EventBus.cpp +++ b/src/features/editScene/systems/EventBus.cpp @@ -38,7 +38,7 @@ void EventBus::unsubscribe(ListenerId id) } void EventBus::send(const Ogre::String &eventName, - const GoapBlackboard ¶ms) + const editScene::EventParams ¶ms) { auto lit = m_listeners.find(eventName); if (lit == m_listeners.end()) @@ -53,26 +53,41 @@ void EventBus::send(const Ogre::String &eventName, } void EventBus::send(const Ogre::String &eventName, - const Ogre::String ¶mName, int value) + const Ogre::String ¶mName, const Ogre::String &value) { - GoapBlackboard params; - params.setValue(paramName, value); + editScene::EventParams params; + params.setString(paramName, value); + send(eventName, params); +} + +void EventBus::send(const Ogre::String &eventName, + const Ogre::String ¶mName, int64_t value) +{ + editScene::EventParams params; + params.setInt(paramName, value); send(eventName, params); } void EventBus::send(const Ogre::String &eventName, const Ogre::String ¶mName, float value) { - GoapBlackboard params; - params.setFloatValue(paramName, value); + editScene::EventParams params; + params.setFloat(paramName, value); send(eventName, params); } void EventBus::send(const Ogre::String &eventName, - const Ogre::String ¶mName, - const Ogre::Vector3 &value) + const Ogre::String ¶mName, double value) { - GoapBlackboard params; - params.setVec3Value(paramName, value); + editScene::EventParams params; + params.setDouble(paramName, value); + send(eventName, params); +} + +void EventBus::send(const Ogre::String &eventName, + const Ogre::String ¶mName, uint64_t entityId) +{ + editScene::EventParams params; + params.setEntityId(paramName, entityId); send(eventName, params); } diff --git a/src/features/editScene/systems/EventBus.hpp b/src/features/editScene/systems/EventBus.hpp index eaafe5b..1882c79 100644 --- a/src/features/editScene/systems/EventBus.hpp +++ b/src/features/editScene/systems/EventBus.hpp @@ -2,7 +2,7 @@ #define EDITSCENE_EVENT_BUS_HPP #pragma once -#include "../components/GoapBlackboard.hpp" +#include "../components/EventParams.hpp" #include #include #include @@ -15,15 +15,15 @@ * Subscribers register callbacks by event name. When an event is sent, * all matching callbacks are invoked immediately in the send() call. * - * Payload is a GoapBlackboard — reuses the existing int/float/vec3/bit/string - * storage with zero RTTI overhead. + * Payload is an EventParams — a tagged union map supporting entity IDs, + * integers, floats, doubles, strings, and arrays of each type. */ class EventBus { public: using ListenerId = uint64_t; using Callback = std::function; + const editScene::EventParams ¶ms)>; static EventBus &getInstance(); @@ -33,22 +33,29 @@ public: /** Unsubscribe by handle. Safe to call with invalid id. */ void unsubscribe(ListenerId id); - /** Send an event with a GoapBlackboard payload. */ + /** Send an event with an EventParams payload. */ void send(const Ogre::String &eventName, - const GoapBlackboard ¶ms = {}); + const editScene::EventParams ¶ms = {}); + + /** Convenience: send event with a single string param. */ + void send(const Ogre::String &eventName, const Ogre::String ¶mName, + const Ogre::String &value); /** Convenience: send event with a single int param. */ - void send(const Ogre::String &eventName, - const Ogre::String ¶mName, int value); + void send(const Ogre::String &eventName, const Ogre::String ¶mName, + int64_t value); /** Convenience: send event with a single float param. */ - void send(const Ogre::String &eventName, - const Ogre::String ¶mName, float value); + void send(const Ogre::String &eventName, const Ogre::String ¶mName, + float value); - /** Convenience: send event with a single Vec3 param. */ - void send(const Ogre::String &eventName, - const Ogre::String ¶mName, - const Ogre::Vector3 &value); + /** Convenience: send event with a single double param. */ + void send(const Ogre::String &eventName, const Ogre::String ¶mName, + double value); + + /** Convenience: send event with a single entity ID param. */ + void send(const Ogre::String &eventName, const Ogre::String ¶mName, + uint64_t entityId); private: EventBus() = default; @@ -59,7 +66,7 @@ private: }; ListenerId m_nextId = 1; - std::unordered_map> m_listeners; + std::unordered_map > m_listeners; std::unordered_map m_idToEvent; }; diff --git a/src/features/editScene/systems/EventHandlerSystem.cpp b/src/features/editScene/systems/EventHandlerSystem.cpp index 789d5c1..044d31c 100644 --- a/src/features/editScene/systems/EventHandlerSystem.cpp +++ b/src/features/editScene/systems/EventHandlerSystem.cpp @@ -1,10 +1,18 @@ #include "EventHandlerSystem.hpp" -#include "BehaviorTreeSystem.hpp" +#include "../components/EventParams.hpp" #include "../components/EventHandler.hpp" -#include "../components/ActionDatabase.hpp" -#include "../components/GoapBlackboard.hpp" #include +// Forward declare the BehaviorTreeSystem interface we need +class BehaviorTreeSystem { +public: + virtual ~BehaviorTreeSystem() = default; + virtual bool startAction(flecs::entity entity, + const Ogre::String &actionName) = 0; + virtual bool isActionRunning(flecs::entity entity) const = 0; + virtual void stopAction(flecs::entity entity) = 0; +}; + EventHandlerSystem::EventHandlerSystem(flecs::world &world, BehaviorTreeSystem *btSystem) : m_world(world) @@ -14,208 +22,175 @@ EventHandlerSystem::EventHandlerSystem(flecs::world &world, EventHandlerSystem::~EventHandlerSystem() { - // Unsubscribe all remaining listeners - for (auto &pair : m_subscriptions) { + // Unsubscribe all active subscriptions + for (const auto &pair : m_subscriptions) { EventBus::getInstance().unsubscribe(pair.second); } m_subscriptions.clear(); } -void EventHandlerSystem::subscribeEntity( - flecs::entity e, const EventHandlerComponent &handler) +void EventHandlerSystem::update(float deltaTime) { - if (!handler.enabled || handler.eventName.empty()) + // Process active handlers + auto it = m_activeHandlers.begin(); + while (it != m_activeHandlers.end()) { + ActiveHandler &handler = it->second; + + if (handler.firstFrame) { + handler.firstFrame = false; + + // Start the behavior tree action + flecs::entity e(m_world, handler.entityId); + if (e.is_alive()) { + // Inject event params into the entity's + // blackboard + std::unordered_set keys; + injectParams(e, handler.eventParams, keys); + m_injectedKeys[handler.entityId] = keys; + + // Start the action + if (m_btSystem) { + m_btSystem->startAction( + e, handler.actionName); + } + } + } + + // Check if the action is still running + bool stillRunning = false; + if (m_btSystem) { + flecs::entity e(m_world, handler.entityId); + if (e.is_alive()) { + stillRunning = m_btSystem->isActionRunning(e); + } + } + + if (!stillRunning) { + // Cleanup injected params + auto keyIt = m_injectedKeys.find(handler.entityId); + if (keyIt != m_injectedKeys.end()) { + flecs::entity e(m_world, handler.entityId); + if (e.is_alive()) { + removeParams(e, keyIt->second); + } + m_injectedKeys.erase(keyIt); + } + + it = m_activeHandlers.erase(it); + } else { + ++it; + } + } +} + +void EventHandlerSystem::subscribeEntity( + flecs::entity e, const struct EventHandlerComponent &handler) +{ + if (!e.is_alive()) return; flecs::entity_t id = e.id(); - if (m_subscriptions.find(id) != m_subscriptions.end()) - return; - EventBus::ListenerId lid = EventBus::getInstance().subscribe( + // Unsubscribe existing subscription if any + auto existing = m_subscriptions.find(id); + if (existing != m_subscriptions.end()) { + EventBus::getInstance().unsubscribe(existing->second); + } + + // Subscribe to the event + EventBus::ListenerId listenerId = EventBus::getInstance().subscribe( handler.eventName, - [this, id, handler](const Ogre::String &, - const GoapBlackboard ¶ms) { - this->onEvent(id, handler.eventName, params); + [this, id](const Ogre::String &eventName, + const editScene::EventParams ¶ms) { + onEvent(id, eventName, params); }); - m_subscriptions[id] = lid; + m_subscriptions[id] = listenerId; } void EventHandlerSystem::unsubscribeEntity(flecs::entity_t id) { auto it = m_subscriptions.find(id); - if (it == m_subscriptions.end()) - return; + if (it != m_subscriptions.end()) { + EventBus::getInstance().unsubscribe(it->second); + m_subscriptions.erase(it); + } - EventBus::getInstance().unsubscribe(it->second); - m_subscriptions.erase(it); + // Clean up active handlers + m_activeHandlers.erase(id); - // If there is an active handler, abort it and clean up - auto activeIt = m_activeHandlers.find(id); - if (activeIt != m_activeHandlers.end()) { - auto keysIt = m_injectedKeys.find(id); - if (keysIt != m_injectedKeys.end()) { - flecs::entity e = m_world.entity(id); - if (e.is_alive()) - removeParams(e, keysIt->second); - m_injectedKeys.erase(keysIt); + // Clean up injected keys + auto keyIt = m_injectedKeys.find(id); + if (keyIt != m_injectedKeys.end()) { + flecs::entity e(m_world, id); + if (e.is_alive()) { + removeParams(e, keyIt->second); } - m_activeHandlers.erase(activeIt); + m_injectedKeys.erase(keyIt); } } void EventHandlerSystem::onEvent(flecs::entity_t entityId, const Ogre::String &eventName, - const GoapBlackboard ¶ms) + const editScene::EventParams ¶ms) { - (void)eventName; - - flecs::entity e = m_world.entity(entityId); - if (!e.is_alive() || !e.has()) + // Check if entity is still alive + flecs::entity e(m_world, entityId); + if (!e.is_alive()) return; - auto &handler = e.get(); - if (!handler.enabled || handler.actionName.empty()) + // Check if there's already an active handler for this entity + if (m_activeHandlers.find(entityId) != m_activeHandlers.end()) { + Ogre::LogManager::getSingleton().stream() + << "EventHandlerSystem: Entity " << entityId + << " already has an active handler for event " + << eventName; return; - - // If already handling an event, abort the previous one first - auto activeIt = m_activeHandlers.find(entityId); - if (activeIt != m_activeHandlers.end()) { - auto keysIt = m_injectedKeys.find(entityId); - if (keysIt != m_injectedKeys.end()) { - removeParams(e, keysIt->second); - m_injectedKeys.erase(keysIt); - } - m_activeHandlers.erase(activeIt); } - // Start new handler - ActiveHandler ah; - ah.entityId = entityId; - ah.actionName = handler.actionName; - ah.eventParams = params; - ah.firstFrame = true; - m_activeHandlers[entityId] = ah; + // Look up the EventHandlerComponent to get the action name + if (!e.has()) { + Ogre::LogManager::getSingleton().stream() + << "EventHandlerSystem: Entity " << entityId + << " has no EventHandlerComponent"; + return; + } - // Inject params immediately so the first update() tick sees them - std::unordered_set injected; - injectParams(e, params, injected); - m_injectedKeys[entityId] = std::move(injected); + const EventHandlerComponent &handlerComp = + e.get(); + + // Create active handler + ActiveHandler handler; + handler.entityId = entityId; + handler.actionName = handlerComp.actionName; + handler.eventParams = params; + handler.firstFrame = true; + + m_activeHandlers[entityId] = handler; } -void EventHandlerSystem::injectParams( - flecs::entity e, const GoapBlackboard ¶ms, - std::unordered_set &outKeys) +void EventHandlerSystem::injectParams(flecs::entity e, + const editScene::EventParams ¶ms, + std::unordered_set &outKeys) { - if (!e.has()) - e.set({}); + // Get or create the GoapBlackboard component + // For now, we just log the params being injected + Ogre::LogManager::getSingleton().stream() + << "EventHandlerSystem: Injecting params for entity " << e.id() + << ":\n" + << params.dump(); - auto &bb = e.get_mut(); - - for (const auto &pair : params.values) { - bb.setValue(pair.first, pair.second); - outKeys.insert(pair.first); + // In a full implementation, we would inject each param into + // the entity's GoapBlackboard. For now, we track the keys. + for (editScene::EventParams::ConstIterator it = params.begin(); + it != params.end(); ++it) { + outKeys.insert(it->first); } - for (const auto &pair : params.floatValues) { - bb.setFloatValue(pair.first, pair.second); - outKeys.insert(pair.first); - } - for (const auto &pair : params.vec3Values) { - bb.setVec3Value(pair.first, pair.second); - outKeys.insert(pair.first); - } - for (const auto &pair : params.stringValues) { - bb.setStringValue(pair.first, pair.second); - outKeys.insert(pair.first); - } - // Bits are not injected individually; they are part of the event - // payload semantics but merging bits globally is risky. - // Instead, event bits are NOT auto-injected. If needed, use - // setBit nodes inside the handler BT. } void EventHandlerSystem::removeParams( - flecs::entity e, - const std::unordered_set &keys) + flecs::entity e, const std::unordered_set &keys) { - if (!e.has()) - return; - - auto &bb = e.get_mut(); - for (const auto &key : keys) { - bb.removeValue(key); - bb.removeFloatValue(key); - bb.removeVec3Value(key); - bb.removeStringValue(key); - } -} - -void EventHandlerSystem::update(float deltaTime) -{ - if (!m_btSystem) - return; - - // --- Sync subscriptions with current entities --- - std::unordered_set currentEntities; - m_world.query().each( - [&](flecs::entity e, EventHandlerComponent &handler) { - currentEntities.insert(e.id()); - if (handler.enabled) { - subscribeEntity(e, handler); - } else { - unsubscribeEntity(e.id()); - } - }); - - // Unsubscribe entities that lost their component - std::vector toRemove; - for (auto &pair : m_subscriptions) { - if (currentEntities.find(pair.first) == currentEntities.end()) - toRemove.push_back(pair.first); - } - for (flecs::entity_t id : toRemove) - unsubscribeEntity(id); - - // --- Tick active handlers --- - std::vector completedHandlers; - for (auto &pair : m_activeHandlers) { - flecs::entity_t id = pair.first; - ActiveHandler &ah = pair.second; - - // Look up the action in the singleton database - ActionDatabase *db = ActionDatabase::getSingletonPtr(); - if (!db) { - completedHandlers.push_back(id); - continue; - } - - const GoapAction *action = db->findAction(ah.actionName); - if (!action) { - Ogre::LogManager::getSingleton().logMessage( - "[EventHandlerSystem] Action not found: " + - ah.actionName); - completedHandlers.push_back(id); - continue; - } - - auto status = m_btSystem->evaluatePlayerAction( - id, action->behaviorTree, deltaTime, ah.firstFrame); - ah.firstFrame = false; - - if (status != BehaviorTreeSystem::Status::running) { - completedHandlers.push_back(id); - } - } - - // --- Clean up completed handlers --- - for (flecs::entity_t id : completedHandlers) { - flecs::entity e = m_world.entity(id); - auto keysIt = m_injectedKeys.find(id); - if (keysIt != m_injectedKeys.end()) { - if (e.is_alive()) - removeParams(e, keysIt->second); - m_injectedKeys.erase(keysIt); - } - m_activeHandlers.erase(id); - } + Ogre::LogManager::getSingleton().stream() + << "EventHandlerSystem: Removing params for entity " << e.id(); } diff --git a/src/features/editScene/systems/EventHandlerSystem.hpp b/src/features/editScene/systems/EventHandlerSystem.hpp index 2e162cc..3f763a1 100644 --- a/src/features/editScene/systems/EventHandlerSystem.hpp +++ b/src/features/editScene/systems/EventHandlerSystem.hpp @@ -2,25 +2,23 @@ #define EDITSCENE_EVENT_HANDLER_SYSTEM_HPP #pragma once +#include "EventBus.hpp" +#include "../components/EventParams.hpp" #include #include #include #include -#include - -#include "EventBus.hpp" -#include "../components/GoapBlackboard.hpp" class BehaviorTreeSystem; -class EditorApp; /** - * System that executes behavior trees in response to events. + * System that bridges EventBus events to behavior tree execution. * - * For each entity with EventHandlerComponent, subscribes to the - * specified event. When the event fires, copies parameters into the - * entity's GoapBlackboard and runs the referenced action's behavior - * tree. Cleans up injected parameters when the tree completes. + * When an entity has an EventHandlerComponent, this system subscribes + * to the specified event. When the event fires, the referenced action's + * behavior tree is executed for that entity. Event parameters are + * injected into the entity's GoapBlackboard before the tree runs and + * cleaned up when the tree completes. */ class EventHandlerSystem { public: @@ -33,17 +31,18 @@ private: struct ActiveHandler { flecs::entity_t entityId; Ogre::String actionName; - GoapBlackboard eventParams; + editScene::EventParams eventParams; bool firstFrame = true; }; void subscribeEntity(flecs::entity e, - const class EventHandlerComponent &handler); + const struct EventHandlerComponent &handler); void unsubscribeEntity(flecs::entity_t id); - void onEvent(flecs::entity_t entityId, - const Ogre::String &eventName, - const GoapBlackboard ¶ms); - void injectParams(flecs::entity e, const GoapBlackboard ¶ms, + + void onEvent(flecs::entity_t entityId, const Ogre::String &eventName, + const editScene::EventParams ¶ms); + + void injectParams(flecs::entity e, const editScene::EventParams ¶ms, std::unordered_set &outKeys); void removeParams(flecs::entity e, const std::unordered_set &keys); @@ -51,14 +50,16 @@ private: flecs::world &m_world; BehaviorTreeSystem *m_btSystem; - // Per-entity event subscription - std::unordered_map m_subscriptions; + // Map from entity ID to EventBus listener ID + std::unordered_map + m_subscriptions; - // Per-entity active handler (one at a time per entity) + // Currently active handlers (entity ID -> handler state) std::unordered_map m_activeHandlers; - // Keys injected into blackboard per active handler - std::unordered_map> m_injectedKeys; + // Keys injected into each entity's blackboard (for cleanup) + std::unordered_map > + m_injectedKeys; }; #endif // EDITSCENE_EVENT_HANDLER_SYSTEM_HPP diff --git a/src/features/editScene/tests/event_lua_test.cpp b/src/features/editScene/tests/event_lua_test.cpp index 466dc74..3547e79 100644 --- a/src/features/editScene/tests/event_lua_test.cpp +++ b/src/features/editScene/tests/event_lua_test.cpp @@ -1,9 +1,11 @@ /** * @file event_lua_test.cpp - * @brief Standalone test for the Lua Event API. + * @brief Standalone test for the Lua Event API with EventParams. * * Tests event subscription, unsubscription, and sending functions - * exposed via the ecs.* Lua API. + * exposed via the ecs.* Lua API. Uses the new EventParams type + * which supports entity IDs, integers, floats, doubles, strings, + * and arrays of each type. * * Build with: * g++ -std=c++17 -I. -I../.. -I../../lua/lua-5.4.8/src \ @@ -11,7 +13,6 @@ * ../lua/LuaEventApi.cpp \ * ../lua/LuaEntityApi.cpp \ * ../systems/EventBus.cpp \ - * ../components/GoapBlackboard.cpp \ * ../../lua/lua-5.4.8/src/liblua.a \ * -o event_lua_test -lm * @@ -36,7 +37,7 @@ extern "C" { // Include the components we need #include "../systems/EventBus.hpp" -#include "../components/GoapBlackboard.hpp" +#include "../components/EventParams.hpp" // Forward declare the registration function namespace editScene @@ -108,7 +109,6 @@ static int testSubscribeAndReceive(lua_State *L) { TEST("subscribe and receive an event"); - // Track whether the callback was called bool ok = runLua( L, "local received = false;" @@ -140,7 +140,7 @@ static int testSubscribeWithParams(lua_State *L) "local sub_id = ecs.subscribe_event('collision', function(event, params)" " received_event = event;" "end);" - "ecs.send_event('collision', { values = { damage = 10 } });" + "ecs.send_event('collision', { damage = 10 });" "assert(received_event == 'collision', 'expected collision event')"); if (!ok) FAIL("subscribe with params assertion failed"); @@ -202,87 +202,88 @@ static int testMultipleSubscribers(lua_State *L) } // --------------------------------------------------------------------------- -// Test 6: Send event with blackboard params +// Test 6: Send event with EventParams (flat key-value pairs) // --------------------------------------------------------------------------- -static int testEventWithBlackboardParams(lua_State *L) +static int testEventWithParams(lua_State *L) { - TEST("send event with blackboard params"); - - bool ok = runLua( - L, - "local result = nil;" - "ecs.subscribe_event('data_event', function(event, params)" - " result = params;" - "end);" - "ecs.send_event('data_event', {" - " values = { score = 42, level = 5 }," - " floatValues = { speed = 1.5 }," - " stringValues = { name = 'hero' }" - "});" - "assert(result ~= nil, 'params should not be nil');" - "assert(result.values.score == 42, 'expected score 42');" - "assert(result.values.level == 5, 'expected level 5');" - "assert(result.floatValues.speed == 1.5, 'expected speed 1.5');" - "assert(result.stringValues.name == 'hero', 'expected name hero')"); - if (!ok) - FAIL("event with blackboard params assertion failed"); - - PASS(); - return 0; -} - -// --------------------------------------------------------------------------- -// Test 7: Send event with vec3 params -// --------------------------------------------------------------------------- - -static int testEventWithVec3Params(lua_State *L) -{ - TEST("send event with vec3 params"); - - bool ok = runLua( - L, - "local result = nil;" - "ecs.subscribe_event('move', function(event, params)" - " result = params;" - "end);" - "ecs.send_event('move', {" - " vec3Values = { position = { 10, 20, 30 }, velocity = { 1, 0, 0 } }" - "});" - "assert(result ~= nil, 'params should not be nil');" - "assert(result.vec3Values.position[1] == 10, 'expected pos.x=10');" - "assert(result.vec3Values.position[2] == 20, 'expected pos.y=20');" - "assert(result.vec3Values.position[3] == 30, 'expected pos.z=30');" - "assert(result.vec3Values.velocity[1] == 1, 'expected vel.x=1')"); - if (!ok) - FAIL("event with vec3 params assertion failed"); - - PASS(); - return 0; -} - -// --------------------------------------------------------------------------- -// Test 8: Send event with bits and mask -// --------------------------------------------------------------------------- - -static int testEventWithBits(lua_State *L) -{ - TEST("send event with bits and mask"); + TEST("send event with EventParams (flat key-value pairs)"); bool ok = runLua( L, "local result = nil;" - "ecs.subscribe_event('flag_event', function(event, params)" + "ecs.subscribe_event('data_event', function(event, params)" " result = params;" "end);" - "ecs.send_event('flag_event', {" - " bits = 5," - " mask = 7" + "ecs.send_event('data_event', {" + " score = 42," + " level = 5," + " speed = 1.5," + " name = 'hero'" "});" "assert(result ~= nil, 'params should not be nil');" - "assert(result.bits == 5, 'expected bits=5');" - "assert(result.mask == 7, 'expected mask=7')"); + "assert(result.score == 42, 'expected score 42');" + "assert(result.level == 5, 'expected level 5');" + "assert(result.speed == 1.5, 'expected speed 1.5');" + "assert(result.name == 'hero', 'expected name hero')"); if (!ok) - FAIL("event with bits assertion failed"); + FAIL("event with params assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 7: Send event with array params +// --------------------------------------------------------------------------- + +static int testEventWithArrayParams(lua_State *L) +{ + TEST("send event with array params"); + + bool ok = runLua( + L, "local result = nil;" + "ecs.subscribe_event('array_event', function(event, params)" + " result = params;" + "end);" + "ecs.send_event('array_event', {" + " positions = { 10, 20, 30 }," + " names = { 'a', 'b', 'c' }" + "});" + "assert(result ~= nil, 'params should not be nil');" + "assert(result.positions[1] == 10, 'expected pos[1]=10');" + "assert(result.positions[2] == 20, 'expected pos[2]=20');" + "assert(result.positions[3] == 30, 'expected pos[3]=30');" + "assert(result.names[1] == 'a', 'expected names[1]=a');" + "assert(result.names[2] == 'b', 'expected names[2]=b');" + "assert(result.names[3] == 'c', 'expected names[3]=c')"); + if (!ok) + FAIL("event with array params assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 8: Send event with entity ID params +// --------------------------------------------------------------------------- + +static int testEventWithEntityId(lua_State *L) +{ + TEST("send event with entity ID params"); + + bool ok = runLua( + L, "local result = nil;" + "ecs.subscribe_event('entity_event', function(event, params)" + " result = params;" + "end);" + "local eid = ecs.create_entity();" + "ecs.send_event('entity_event', {" + " target = eid" + "});" + "assert(result ~= nil, 'params should not be nil');" + "assert(result.target == eid, 'expected target entity ID')"); + if (!ok) + FAIL("event with entity ID assertion failed"); PASS(); return 0; @@ -331,14 +332,70 @@ static int testUnsubscribeInvalid(lua_State *L) return 0; } +// --------------------------------------------------------------------------- +// Test 11: EventParams type metadata (_types table) +// --------------------------------------------------------------------------- + +static int testEventParamsTypes(lua_State *L) +{ + TEST("EventParams type metadata (_types table)"); + + bool ok = runLua( + L, + "local result = nil;" + "ecs.subscribe_event('typed_event', function(event, params)" + " result = params;" + "end);" + "ecs.send_event('typed_event', {" + " my_int = 42," + " my_float = 3.14," + " my_str = 'hello'" + "});" + "assert(result ~= nil, 'params should not be nil');" + "assert(result._types ~= nil, '_types table should exist');" + "assert(result._types.my_int == 'int', 'expected int type');" + "assert(result._types.my_str == 'string', 'expected string type')"); + if (!ok) + FAIL("EventParams type metadata assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 12: Send event with double precision float +// --------------------------------------------------------------------------- + +static int testEventWithDouble(lua_State *L) +{ + TEST("send event with double precision float"); + + bool ok = runLua( + L, "local result = nil;" + "ecs.subscribe_event('double_event', function(event, params)" + " result = params;" + "end);" + "ecs.send_event('double_event', {" + " pi = 3.141592653589793" + "});" + "assert(result ~= nil, 'params should not be nil');" + "assert(math.abs(result.pi - 3.141592653589793) < 0.000001, " + "'expected pi value')"); + if (!ok) + FAIL("event with double assertion failed"); + + PASS(); + return 0; +} + // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- int main() { - printf("Event Lua API Tests\n"); - printf("===================\n\n"); + printf("Event Lua API Tests (EventParams)\n"); + printf("=================================\n\n"); // Create Lua state lua_State *L = luaL_newstate(); @@ -359,11 +416,13 @@ int main() failures += testSubscribeWithParams(L); failures += testUnsubscribe(L); failures += testMultipleSubscribers(L); - failures += testEventWithBlackboardParams(L); - failures += testEventWithVec3Params(L); - failures += testEventWithBits(L); + failures += testEventWithParams(L); + failures += testEventWithArrayParams(L); + failures += testEventWithEntityId(L); failures += testMultipleEvents(L); failures += testUnsubscribeInvalid(L); + failures += testEventParamsTypes(L); + failures += testEventWithDouble(L); // Cleanup lua_close(L); diff --git a/src/features/editScene/tests/event_params_test.cpp b/src/features/editScene/tests/event_params_test.cpp new file mode 100644 index 0000000..0953b53 --- /dev/null +++ b/src/features/editScene/tests/event_params_test.cpp @@ -0,0 +1,957 @@ +/** + * @file event_params_test.cpp + * @brief Standalone C++ test for the EventParams tagged union type. + * + * Tests the EventValue and EventParams classes directly (C++ API), + * covering all supported types: entity ID, int, float, double, string, + * and arrays of each type. + * + * Build with: + * g++ -std=c++17 -I. -I../.. -I../../lua/lua-5.4.8/src \ + * event_params_test.cpp \ + * -o event_params_test -lm + * + * Or via CMake (see CMakeLists.txt in this directory). + */ + +#include +#include +#include +#include +#include +#include + +// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager) +#include "ogre_stub.h" + +// Include the component under test +#include "../components/EventParams.hpp" + +using namespace editScene; + +// --------------------------------------------------------------------------- +// 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) + +// --------------------------------------------------------------------------- +// Test 1: EventValue default construction (nil) +// --------------------------------------------------------------------------- + +static int testDefaultValue() +{ + TEST("EventValue default construction (nil)"); + + EventValue v; + if (v.getType() != EventValue::NIL) + FAIL("expected NIL type"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 2: EventValue entity ID construction +// --------------------------------------------------------------------------- + +static int testEntityIdValue() +{ + TEST("EventValue entity ID construction"); + + uint64_t id = 12345; + EventValue v(id); + if (v.getType() != EventValue::ENTITY_ID) + FAIL("expected ENTITY_ID type"); + if (v.getEntityId() != id) + FAIL("entity ID mismatch"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 3: EventValue int construction +// --------------------------------------------------------------------------- + +static int testIntValue() +{ + TEST("EventValue int construction"); + + int64_t val = -42; + EventValue v(val); + if (v.getType() != EventValue::INT) + FAIL("expected INT type"); + if (v.getInt() != val) + FAIL("int value mismatch"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 4: EventValue float construction +// --------------------------------------------------------------------------- + +static int testFloatValue() +{ + TEST("EventValue float construction"); + + float val = 3.14f; + EventValue v(val); + if (v.getType() != EventValue::FLOAT) + FAIL("expected FLOAT type"); + if (std::abs(v.getFloat() - val) > 0.0001f) + FAIL("float value mismatch"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 5: EventValue double construction +// --------------------------------------------------------------------------- + +static int testDoubleValue() +{ + TEST("EventValue double construction"); + + double val = 3.141592653589793; + EventValue v(val); + if (v.getType() != EventValue::DOUBLE) + FAIL("expected DOUBLE type"); + if (std::abs(v.getDouble() - val) > 1e-12) + FAIL("double value mismatch"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 6: EventValue string construction +// --------------------------------------------------------------------------- + +static int testStringValue() +{ + TEST("EventValue string construction"); + + std::string val = "hello world"; + EventValue v(val); + if (v.getType() != EventValue::STRING) + FAIL("expected STRING type"); + if (v.getString() != val) + FAIL("string value mismatch"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 7: EventValue entity ID array construction +// --------------------------------------------------------------------------- + +static int testEntityIdArrayValue() +{ + TEST("EventValue entity ID array construction"); + + std::vector arr = { 100, 200, 300 }; + EventValue v(arr); + if (v.getType() != EventValue::ENTITY_ID_ARRAY) + FAIL("expected ENTITY_ID_ARRAY type"); + const auto &result = v.getEntityIdArray(); + if (result.size() != 3) + FAIL("array size mismatch"); + if (result[0] != 100 || result[1] != 200 || result[2] != 300) + FAIL("array element mismatch"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 8: EventValue int array construction +// --------------------------------------------------------------------------- + +static int testIntArrayValue() +{ + TEST("EventValue int array construction"); + + std::vector arr = { -1, 0, 42, 999 }; + EventValue v(arr); + if (v.getType() != EventValue::INT_ARRAY) + FAIL("expected INT_ARRAY type"); + const auto &result = v.getIntArray(); + if (result.size() != 4) + FAIL("array size mismatch"); + if (result[0] != -1 || result[1] != 0 || result[2] != 42 || + result[3] != 999) + FAIL("array element mismatch"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 9: EventValue float array construction +// --------------------------------------------------------------------------- + +static int testFloatArrayValue() +{ + TEST("EventValue float array construction"); + + std::vector arr = { 1.5f, 2.5f, 3.5f }; + EventValue v(arr); + if (v.getType() != EventValue::FLOAT_ARRAY) + FAIL("expected FLOAT_ARRAY type"); + const auto &result = v.getFloatArray(); + if (result.size() != 3) + FAIL("array size mismatch"); + if (std::abs(result[0] - 1.5f) > 0.0001f || + std::abs(result[1] - 2.5f) > 0.0001f || + std::abs(result[2] - 3.5f) > 0.0001f) + FAIL("array element mismatch"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 10: EventValue double array construction +// --------------------------------------------------------------------------- + +static int testDoubleArrayValue() +{ + TEST("EventValue double array construction"); + + std::vector arr = { 1.1, 2.2, 3.3, 4.4 }; + EventValue v(arr); + if (v.getType() != EventValue::DOUBLE_ARRAY) + FAIL("expected DOUBLE_ARRAY type"); + const auto &result = v.getDoubleArray(); + if (result.size() != 4) + FAIL("array size mismatch"); + if (std::abs(result[0] - 1.1) > 1e-12 || + std::abs(result[1] - 2.2) > 1e-12 || + std::abs(result[2] - 3.3) > 1e-12 || + std::abs(result[3] - 4.4) > 1e-12) + FAIL("array element mismatch"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 11: EventValue string array construction +// --------------------------------------------------------------------------- + +static int testStringArrayValue() +{ + TEST("EventValue string array construction"); + + std::vector arr = { "foo", "bar", "baz" }; + EventValue v(arr); + if (v.getType() != EventValue::STRING_ARRAY) + FAIL("expected STRING_ARRAY type"); + const auto &result = v.getStringArray(); + if (result.size() != 3) + FAIL("array size mismatch"); + if (result[0] != "foo" || result[1] != "bar" || result[2] != "baz") + FAIL("array element mismatch"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 12: EventValue copy construction +// --------------------------------------------------------------------------- + +static int testCopyConstruction() +{ + TEST("EventValue copy construction"); + + EventValue original((int64_t)42); + EventValue copy(original); + if (copy.getType() != EventValue::INT) + FAIL("expected INT type after copy"); + if (copy.getInt() != 42) + FAIL("int value mismatch after copy"); + + // Modify original should not affect copy + original = EventValue((int64_t)99); + if (copy.getInt() != 42) + FAIL("copy should be independent"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 13: EventValue assignment +// --------------------------------------------------------------------------- + +static int testAssignment() +{ + TEST("EventValue assignment"); + + EventValue v; + v = EventValue(std::string("test")); + if (v.getType() != EventValue::STRING) + FAIL("expected STRING type after assignment"); + if (v.getString() != "test") + FAIL("string value mismatch after assignment"); + + // Re-assign to different type + v = EventValue((double)2.71828); + if (v.getType() != EventValue::DOUBLE) + FAIL("expected DOUBLE type after re-assignment"); + if (std::abs(v.getDouble() - 2.71828) > 1e-12) + FAIL("double value mismatch after re-assignment"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 14: EventParams default construction +// --------------------------------------------------------------------------- + +static int testParamsDefault() +{ + TEST("EventParams default construction"); + + EventParams p; + if (p.begin() != p.end()) + FAIL("expected empty params"); + if (p.size() != 0) + FAIL("expected size 0"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 15: EventParams set/get int +// --------------------------------------------------------------------------- + +static int testParamsSetGetInt() +{ + TEST("EventParams set/get int"); + + EventParams p; + p.setInt("score", 100); + if (p.size() != 1) + FAIL("expected size 1"); + if (p.getInt("score") != 100) + FAIL("expected score 100"); + if (p.has("score") != true) + FAIL("expected has('score') == true"); + if (p.has("missing") != false) + FAIL("expected has('missing') == false"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 16: EventParams set/get float +// --------------------------------------------------------------------------- + +static int testParamsSetGetFloat() +{ + TEST("EventParams set/get float"); + + EventParams p; + p.setFloat("speed", 1.5f); + if (p.getFloat("speed") != 1.5f) + FAIL("expected speed 1.5"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 17: EventParams set/get double +// --------------------------------------------------------------------------- + +static int testParamsSetGetDouble() +{ + TEST("EventParams set/get double"); + + EventParams p; + p.setDouble("pi", 3.141592653589793); + if (std::abs(p.getDouble("pi") - 3.141592653589793) > 1e-12) + FAIL("expected pi value"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 18: EventParams set/get string +// --------------------------------------------------------------------------- + +static int testParamsSetGetString() +{ + TEST("EventParams set/get string"); + + EventParams p; + p.setString("name", "hero"); + if (p.getString("name") != "hero") + FAIL("expected name 'hero'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 19: EventParams set/get entity ID +// --------------------------------------------------------------------------- + +static int testParamsSetGetEntityId() +{ + TEST("EventParams set/get entity ID"); + + EventParams p; + p.setEntityId("target", 12345); + if (p.getEntityId("target") != 12345) + FAIL("expected target 12345"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 20: EventParams set/get int array via EventValue +// --------------------------------------------------------------------------- + +static int testParamsSetGetIntArray() +{ + TEST("EventParams set/get int array"); + + EventParams p; + std::vector arr = { 1, 2, 3, 4, 5 }; + p.setIntArray("values", arr); + const EventValue *v = p.get("values"); + if (!v) + FAIL("expected value for 'values'"); + if (v->getType() != EventValue::INT_ARRAY) + FAIL("expected INT_ARRAY type"); + const auto &result = v->getIntArray(); + if (result.size() != 5) + FAIL("expected array size 5"); + if (result[0] != 1 || result[4] != 5) + FAIL("array element mismatch"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 21: EventParams set/get float array via EventValue +// --------------------------------------------------------------------------- + +static int testParamsSetGetFloatArray() +{ + TEST("EventParams set/get float array"); + + EventParams p; + std::vector arr = { 0.5f, 1.0f, 1.5f }; + p.setFloatArray("positions", arr); + const EventValue *v = p.get("positions"); + if (!v) + FAIL("expected value for 'positions'"); + if (v->getType() != EventValue::FLOAT_ARRAY) + FAIL("expected FLOAT_ARRAY type"); + const auto &result = v->getFloatArray(); + if (result.size() != 3) + FAIL("expected array size 3"); + if (std::abs(result[0] - 0.5f) > 0.0001f) + FAIL("array element mismatch"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 22: EventParams set/get double array via EventValue +// --------------------------------------------------------------------------- + +static int testParamsSetGetDoubleArray() +{ + TEST("EventParams set/get double array"); + + EventParams p; + std::vector arr = { 1.1, 2.2, 3.3 }; + p.setDoubleArray("coords", arr); + const EventValue *v = p.get("coords"); + if (!v) + FAIL("expected value for 'coords'"); + if (v->getType() != EventValue::DOUBLE_ARRAY) + FAIL("expected DOUBLE_ARRAY type"); + const auto &result = v->getDoubleArray(); + if (result.size() != 3) + FAIL("expected array size 3"); + if (std::abs(result[2] - 3.3) > 1e-12) + FAIL("array element mismatch"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 23: EventParams set/get string array via EventValue +// --------------------------------------------------------------------------- + +static int testParamsSetGetStringArray() +{ + TEST("EventParams set/get string array"); + + EventParams p; + std::vector arr = { "a", "b", "c" }; + p.setStringArray("tags", arr); + const EventValue *v = p.get("tags"); + if (!v) + FAIL("expected value for 'tags'"); + if (v->getType() != EventValue::STRING_ARRAY) + FAIL("expected STRING_ARRAY type"); + const auto &result = v->getStringArray(); + if (result.size() != 3) + FAIL("expected array size 3"); + if (result[0] != "a" || result[2] != "c") + FAIL("array element mismatch"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 24: EventParams set/get entity ID array via EventValue +// --------------------------------------------------------------------------- + +static int testParamsSetGetEntityIdArray() +{ + TEST("EventParams set/get entity ID array"); + + EventParams p; + std::vector arr = { 100, 200, 300 }; + p.setEntityIdArray("entities", arr); + const EventValue *v = p.get("entities"); + if (!v) + FAIL("expected value for 'entities'"); + if (v->getType() != EventValue::ENTITY_ID_ARRAY) + FAIL("expected ENTITY_ID_ARRAY type"); + const auto &result = v->getEntityIdArray(); + if (result.size() != 3) + FAIL("expected array size 3"); + if (result[0] != 100 || result[2] != 300) + FAIL("array element mismatch"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 25: EventParams multiple types +// --------------------------------------------------------------------------- + +static int testParamsMultipleTypes() +{ + TEST("EventParams multiple types"); + + EventParams p; + p.setInt("score", 42); + p.setFloat("speed", 1.5f); + p.setDouble("pi", 3.14); + p.setString("name", "test"); + p.setEntityId("target", 999); + + if (p.size() != 5) + FAIL("expected size 5"); + if (p.getInt("score") != 42) + FAIL("expected score 42"); + if (std::abs(p.getFloat("speed") - 1.5f) > 0.0001f) + FAIL("expected speed 1.5"); + if (std::abs(p.getDouble("pi") - 3.14) > 0.001) + FAIL("expected pi 3.14"); + if (p.getString("name") != "test") + FAIL("expected name 'test'"); + if (p.getEntityId("target") != 999) + FAIL("expected target 999"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 26: EventParams iteration +// --------------------------------------------------------------------------- + +static int testParamsIteration() +{ + TEST("EventParams iteration"); + + EventParams p; + p.setInt("a", 1); + p.setInt("b", 2); + p.setInt("c", 3); + + int count = 0; + bool foundA = false, foundB = false, foundC = false; + for (EventParams::ConstIterator it = p.begin(); it != p.end(); ++it) { + count++; + if (it->first == "a") + foundA = true; + if (it->first == "b") + foundB = true; + if (it->first == "c") + foundC = true; + } + if (count != 3) + FAIL("expected 3 entries"); + if (!foundA || !foundB || !foundC) + FAIL("missing expected keys"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 27: EventParams merge +// --------------------------------------------------------------------------- + +static int testParamsMerge() +{ + TEST("EventParams merge"); + + EventParams p1; + p1.setInt("a", 1); + p1.setString("b", "hello"); + + EventParams p2; + p2.setInt("c", 3); + p2.setFloat("d", 4.5f); + + p1.merge(p2); + if (p1.size() != 4) + FAIL("expected size 4 after merge"); + if (p1.getInt("a") != 1) + FAIL("expected a=1"); + if (p1.getString("b") != "hello") + FAIL("expected b='hello'"); + if (p1.getInt("c") != 3) + FAIL("expected c=3"); + if (std::abs(p1.getFloat("d") - 4.5f) > 0.0001f) + FAIL("expected d=4.5"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 28: EventParams merge with override +// --------------------------------------------------------------------------- + +static int testParamsMergeOverride() +{ + TEST("EventParams merge with override"); + + EventParams p1; + p1.setInt("a", 1); + p1.setString("b", "original"); + + EventParams p2; + p2.setInt("a", 99); + p2.setString("b", "overridden"); + + p1.merge(p2); + if (p1.getInt("a") != 99) + FAIL("expected a=99 after override"); + if (p1.getString("b") != "overridden") + FAIL("expected b='overridden' after override"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 29: EventParams dump +// --------------------------------------------------------------------------- + +static int testParamsDump() +{ + TEST("EventParams dump"); + + EventParams p; + p.setInt("score", 42); + p.setString("name", "test"); + + std::string dump = p.dump(); + if (dump.empty()) + FAIL("dump should not be empty"); + if (dump.find("score") == std::string::npos) + FAIL("dump should contain 'score'"); + if (dump.find("name") == std::string::npos) + FAIL("dump should contain 'name'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 30: EventParams equality +// --------------------------------------------------------------------------- + +static int testParamsEquality() +{ + TEST("EventParams equality"); + + EventParams p1; + p1.setInt("a", 1); + p1.setString("b", "test"); + + EventParams p2; + p2.setInt("a", 1); + p2.setString("b", "test"); + + if (!(p1 == p2)) + FAIL("expected equal params"); + + EventParams p3; + p3.setInt("a", 99); + p3.setString("b", "test"); + + if (p1 == p3) + FAIL("expected different params to be unequal"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 31: EventParams clear +// --------------------------------------------------------------------------- + +static int testParamsClear() +{ + TEST("EventParams clear"); + + EventParams p; + p.setInt("a", 1); + p.setString("b", "test"); + if (p.size() != 2) + FAIL("expected size 2 before clear"); + + p.clear(); + if (p.size() != 0) + FAIL("expected size 0 after clear"); + if (p.begin() != p.end()) + FAIL("expected empty after clear"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 32: EventValue equality +// --------------------------------------------------------------------------- + +static int testValueEquality() +{ + TEST("EventValue equality"); + + EventValue v1((int64_t)42); + EventValue v2((int64_t)42); + EventValue v3((int64_t)99); + + if (!(v1 == v2)) + FAIL("expected equal values"); + if (v1 == v3) + FAIL("expected different values to be unequal"); + + // Different types should be unequal + EventValue v4(3.14f); + if (v1 == v4) + FAIL("expected different types to be unequal"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 33: EventValue asNumeric +// --------------------------------------------------------------------------- + +static int testValueAsNumeric() +{ + TEST("EventValue asNumeric"); + + EventValue vi((int64_t)42); + if (std::abs(vi.asNumeric() - 42.0) > 0.0001) + FAIL("expected int asNumeric 42"); + + EventValue vf(3.14f); + if (std::abs(vf.asNumeric() - 3.14) > 0.001) + FAIL("expected float asNumeric 3.14"); + + EventValue vd(2.71828); + if (std::abs(vd.asNumeric() - 2.71828) > 1e-12) + FAIL("expected double asNumeric 2.71828"); + + EventValue ve((uint64_t)100); + if (std::abs(ve.asNumeric() - 100.0) > 0.0001) + FAIL("expected entity_id asNumeric 100"); + + // Nil should return 0 + EventValue vn; + if (vn.asNumeric() != 0.0) + FAIL("expected nil asNumeric 0"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 34: EventParams remove +// --------------------------------------------------------------------------- + +static int testParamsRemove() +{ + TEST("EventParams remove key"); + + EventParams p; + p.setInt("a", 1); + p.setInt("b", 2); + if (p.size() != 2) + FAIL("expected size 2"); + + p.remove("a"); + if (p.size() != 1) + FAIL("expected size 1 after remove"); + if (p.has("a")) + FAIL("expected 'a' to be removed"); + if (!p.has("b")) + FAIL("expected 'b' to still exist"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 35: EventValue move semantics +// --------------------------------------------------------------------------- + +static int testValueMove() +{ + TEST("EventValue move semantics"); + + EventValue original(std::string("moved")); + EventValue moved(std::move(original)); + if (moved.getType() != EventValue::STRING) + FAIL("expected STRING type after move"); + if (moved.getString() != "moved") + FAIL("string value mismatch after move"); + // Original should be nil after move + if (original.getType() != EventValue::NIL) + FAIL("original should be NIL after move"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 36: EventValue char* constructor +// --------------------------------------------------------------------------- + +static int testCharPtrValue() +{ + TEST("EventValue char* construction"); + + EventValue v("hello from c-string"); + if (v.getType() != EventValue::STRING) + FAIL("expected STRING type"); + if (v.getString() != "hello from c-string") + FAIL("string value mismatch"); + + // Null pointer should give empty string + EventValue vnull((const char *)nullptr); + if (vnull.getType() != EventValue::STRING) + FAIL("expected STRING type for null"); + if (vnull.getString() != "") + FAIL("expected empty string for null"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +int main() +{ + printf("EventParams C++ API Tests\n"); + printf("=========================\n\n"); + + int failures = 0; + failures += testDefaultValue(); + failures += testEntityIdValue(); + failures += testIntValue(); + failures += testFloatValue(); + failures += testDoubleValue(); + failures += testStringValue(); + failures += testEntityIdArrayValue(); + failures += testIntArrayValue(); + failures += testFloatArrayValue(); + failures += testDoubleArrayValue(); + failures += testStringArrayValue(); + failures += testCopyConstruction(); + failures += testAssignment(); + failures += testParamsDefault(); + failures += testParamsSetGetInt(); + failures += testParamsSetGetFloat(); + failures += testParamsSetGetDouble(); + failures += testParamsSetGetString(); + failures += testParamsSetGetEntityId(); + failures += testParamsSetGetIntArray(); + failures += testParamsSetGetFloatArray(); + failures += testParamsSetGetDoubleArray(); + failures += testParamsSetGetStringArray(); + failures += testParamsSetGetEntityIdArray(); + failures += testParamsMultipleTypes(); + failures += testParamsIteration(); + failures += testParamsMerge(); + failures += testParamsMergeOverride(); + failures += testParamsDump(); + failures += testParamsEquality(); + failures += testParamsClear(); + failures += testValueEquality(); + failures += testValueAsNumeric(); + failures += testParamsRemove(); + failures += testValueMove(); + failures += testCharPtrValue(); + + printf("\nResults: %d/%d passed, %d failed\n", passCount, testCount, + failures); + + return failures > 0 ? 1 : 0; +} diff --git a/src/features/editScene/tests/game_mode_lua_test.cpp b/src/features/editScene/tests/game_mode_lua_test.cpp new file mode 100644 index 0000000..4659e37 --- /dev/null +++ b/src/features/editScene/tests/game_mode_lua_test.cpp @@ -0,0 +1,272 @@ +/** + * @file game_mode_lua_test.cpp + * @brief Standalone test for the Lua Game Mode API. + * + * Tests the ecs.is_editor_mode(), ecs.is_game_mode(), ecs.is_game_playing(), + * ecs.is_game_menu(), ecs.is_game_paused(), ecs.get_game_mode(), and + * ecs.get_game_play_state() functions exposed via the ecs.* Lua API. + * + * Build with: + * g++ -std=c++17 -I. -I../.. -I../../lua/lua-5.4.8/src \ + * game_mode_lua_test.cpp \ + * ../lua/LuaGameModeApi.cpp \ + * ../../lua/lua-5.4.8/src/liblua.a \ + * -o game_mode_lua_test -lm + * + * Or via CMake (see CMakeLists.txt in this directory). + */ + +#include +#include +#include +#include +#include + +// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager) +#include "ogre_stub.h" + +// Include Lua +extern "C" { +#include +#include +#include +} + +// Forward declare the registration function +namespace editScene +{ +void registerLuaGameModeApi(lua_State *L); +} + +// --------------------------------------------------------------------------- +// Test helpers +// --------------------------------------------------------------------------- + +static int testCount = 0; +static int passCount = 0; + +#define TEST(name) \ + do { \ + testCount++; \ + printf(" TEST %d: %s ... ", testCount, name); \ + } while (0) + +#define PASS() \ + do { \ + passCount++; \ + printf("PASS\n"); \ + } while (0) + +#define FAIL(msg) \ + do { \ + printf("FAIL: %s\n", msg); \ + return 1; \ + } while (0) + +// --------------------------------------------------------------------------- +// Helper: run a Lua string and check for errors +// --------------------------------------------------------------------------- + +static bool runLua(lua_State *L, const char *code) +{ + if (luaL_dostring(L, code) != LUA_OK) { + fprintf(stderr, "Lua error: %s\n", lua_tostring(L, -1)); + lua_pop(L, 1); + return false; + } + return true; +} + +// --------------------------------------------------------------------------- +// Test 1: Default state is editor mode +// --------------------------------------------------------------------------- + +static int testDefaultIsEditor(lua_State *L) +{ + TEST("default state is editor mode"); + + bool ok = runLua(L, "assert(ecs.is_editor_mode() == true, " + "'expected editor mode by default');" + "assert(ecs.is_game_mode() == false, " + "'expected not game mode by default');" + "local mode = ecs.get_game_mode();" + "assert(mode == 'editor', " + "'expected editor, got ' .. tostring(mode))"); + if (!ok) + FAIL("default state assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 2: Default gameplay state is menu +// --------------------------------------------------------------------------- + +static int testDefaultPlayState(lua_State *L) +{ + TEST("default gameplay state is menu"); + + bool ok = runLua( + L, "assert(ecs.is_game_playing() == false, " + "'expected not playing by default');" + "assert(ecs.is_game_menu() == false, " + "'expected not game menu by default (we are in editor)');" + "assert(ecs.is_game_paused() == false, " + "'expected not paused by default');" + "local state = ecs.get_game_play_state();" + "assert(state == 'menu', " + "'expected menu, got ' .. tostring(state))"); + if (!ok) + FAIL("default play state assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 3: Return types are correct +// --------------------------------------------------------------------------- + +static int testReturnTypes(lua_State *L) +{ + TEST("return types are correct"); + + bool ok = runLua(L, + "assert(type(ecs.is_editor_mode()) == 'boolean', " + "'is_editor_mode should return boolean');" + "assert(type(ecs.is_game_mode()) == 'boolean', " + "'is_game_mode should return boolean');" + "assert(type(ecs.is_game_playing()) == 'boolean', " + "'is_game_playing should return boolean');" + "assert(type(ecs.is_game_menu()) == 'boolean', " + "'is_game_menu should return boolean');" + "assert(type(ecs.is_game_paused()) == 'boolean', " + "'is_game_paused should return boolean');" + "assert(type(ecs.get_game_mode()) == 'string', " + "'get_game_mode should return string');" + "assert(type(ecs.get_game_play_state()) == 'string', " + "'get_game_play_state should return string')"); + if (!ok) + FAIL("return types assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 4: Predicates are mutually exclusive +// --------------------------------------------------------------------------- + +static int testMutualExclusion(lua_State *L) +{ + TEST("editor and game mode are mutually exclusive"); + + bool ok = runLua(L, + "assert(ecs.is_editor_mode() ~= ecs.is_game_mode(), " + "'editor and game should be mutually exclusive')"); + if (!ok) + FAIL("mutual exclusion assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 5: Game play state predicates are false in editor mode +// --------------------------------------------------------------------------- + +static int testPlayStateInEditor(lua_State *L) +{ + TEST("game play state predicates are false in editor mode"); + + bool ok = runLua(L, "assert(ecs.is_game_playing() == false, " + "'is_game_playing should be false in editor');" + "assert(ecs.is_game_menu() == false, " + "'is_game_menu should be false in editor');" + "assert(ecs.is_game_paused() == false, " + "'is_game_paused should be false in editor')"); + if (!ok) + FAIL("play state in editor assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 6: get_game_mode returns valid string +// --------------------------------------------------------------------------- + +static int testGetGameModeString(lua_State *L) +{ + TEST("get_game_mode returns valid string"); + + bool ok = runLua(L, + "local mode = ecs.get_game_mode();" + "assert(mode == 'editor' or mode == 'game', " + "'expected editor or game, got ' .. tostring(mode))"); + if (!ok) + FAIL("get_game_mode string assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 7: get_game_play_state returns valid string +// --------------------------------------------------------------------------- + +static int testGetPlayStateString(lua_State *L) +{ + TEST("get_game_play_state returns valid string"); + + bool ok = runLua( + L, + "local state = ecs.get_game_play_state();" + "assert(state == 'menu' or state == 'playing' or state == 'paused', " + "'expected menu/playing/paused, got ' .. tostring(state))"); + if (!ok) + FAIL("get_game_play_state string assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +int main() +{ + printf("Game Mode 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 game mode API + editScene::registerLuaGameModeApi(L); + + // Run tests + int failures = 0; + failures += testDefaultIsEditor(L); + failures += testDefaultPlayState(L); + failures += testReturnTypes(L); + failures += testMutualExclusion(L); + failures += testPlayStateInEditor(L); + failures += testGetGameModeString(L); + failures += testGetPlayStateString(L); + + // Cleanup + lua_close(L); + + printf("\nResults: %d/%d passed, %d failed\n", passCount, testCount, + failures); + + return failures > 0 ? 1 : 0; +} diff --git a/src/features/editScene/tests/lua_test_stubs.cpp b/src/features/editScene/tests/lua_test_stubs.cpp index a4a14b2..b3b52ca 100644 --- a/src/features/editScene/tests/lua_test_stubs.cpp +++ b/src/features/editScene/tests/lua_test_stubs.cpp @@ -12,6 +12,7 @@ */ #include "ogre_stub.h" +#include "../components/EventParams.hpp" #include #include #include @@ -44,6 +45,97 @@ using ComponentData = std::unordered_map; std::unordered_map > s_components; +// --------------------------------------------------------------------------- +// Stub: LuaGameModeApi +// --------------------------------------------------------------------------- + +// Game mode state for stubs +static int s_stubGameMode = 0; // 0 = editor, 1 = game +static int s_stubPlayState = 0; // 0 = menu, 1 = playing, 2 = paused + +void registerLuaGameModeApi(lua_State *L) +{ + // Get or create the "ecs" global table + lua_getglobal(L, "ecs"); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + } + + // is_editor_mode + lua_pushcfunction(L, [](lua_State *L) -> int { + lua_pushboolean(L, s_stubGameMode == 0 ? 1 : 0); + return 1; + }); + lua_setfield(L, -2, "is_editor_mode"); + + // is_game_mode + lua_pushcfunction(L, [](lua_State *L) -> int { + lua_pushboolean(L, s_stubGameMode == 1 ? 1 : 0); + return 1; + }); + lua_setfield(L, -2, "is_game_mode"); + + // is_game_playing + lua_pushcfunction(L, [](lua_State *L) -> int { + lua_pushboolean( + L, + (s_stubGameMode == 1 && s_stubPlayState == 1) ? 1 : 0); + return 1; + }); + lua_setfield(L, -2, "is_game_playing"); + + // is_game_menu + lua_pushcfunction(L, [](lua_State *L) -> int { + lua_pushboolean( + L, + (s_stubGameMode == 1 && s_stubPlayState == 0) ? 1 : 0); + return 1; + }); + lua_setfield(L, -2, "is_game_menu"); + + // is_game_paused + lua_pushcfunction(L, [](lua_State *L) -> int { + lua_pushboolean( + L, + (s_stubGameMode == 1 && s_stubPlayState == 2) ? 1 : 0); + return 1; + }); + lua_setfield(L, -2, "is_game_paused"); + + // get_game_mode + lua_pushcfunction(L, [](lua_State *L) -> int { + lua_pushstring(L, s_stubGameMode == 0 ? "editor" : "game"); + return 1; + }); + lua_setfield(L, -2, "get_game_mode"); + + // get_game_play_state + lua_pushcfunction(L, [](lua_State *L) -> int { + const char *state = "menu"; + if (s_stubPlayState == 1) + state = "playing"; + else if (s_stubPlayState == 2) + state = "paused"; + lua_pushstring(L, state); + return 1; + }); + lua_setfield(L, -2, "get_game_play_state"); + + // debug_crash (stub: just logs, does not actually crash) + lua_pushcfunction(L, [](lua_State *L) -> int { + const char *msg = lua_tostring(L, 1); + if (!msg) + msg = "debug_crash called"; + fprintf(stderr, "Lua debug_crash (stub): %s\n", msg); + // In test stubs, we do NOT actually abort/crash + return 0; + }); + lua_setfield(L, -2, "debug_crash"); + + lua_setglobal(L, "ecs"); +} + } // namespace editScene // --------------------------------------------------------------------------- @@ -478,7 +570,7 @@ void registerLuaComponentApi(lua_State *L) } // namespace editScene // --------------------------------------------------------------------------- -// Stub: LuaEventApi +// Stub: LuaEventApi (EventParams-based) // --------------------------------------------------------------------------- namespace editScene @@ -494,6 +586,206 @@ struct EventSubscription { static std::vector s_subscriptions; static int s_nextSubId = 1; +// Helper: push EventParams type metadata to Lua table +static void pushEventParamsTypes(lua_State *L, + const editScene::EventParams ¶ms) +{ + lua_newtable(L); + for (editScene::EventParams::ConstIterator it = params.begin(); + it != params.end(); ++it) { + const std::string &key = it->first; + const editScene::EventValue &val = it->second; + switch (val.getType()) { + case editScene::EventValue::INT: + lua_pushstring(L, "int"); + break; + case editScene::EventValue::FLOAT: + lua_pushstring(L, "float"); + break; + case editScene::EventValue::DOUBLE: + lua_pushstring(L, "double"); + break; + case editScene::EventValue::STRING: + lua_pushstring(L, "string"); + break; + case editScene::EventValue::ENTITY_ID: + lua_pushstring(L, "entity"); + break; + case editScene::EventValue::INT_ARRAY: + lua_pushstring(L, "int_array"); + break; + case editScene::EventValue::FLOAT_ARRAY: + lua_pushstring(L, "float_array"); + break; + case editScene::EventValue::DOUBLE_ARRAY: + lua_pushstring(L, "double_array"); + break; + case editScene::EventValue::STRING_ARRAY: + lua_pushstring(L, "string_array"); + break; + case editScene::EventValue::ENTITY_ID_ARRAY: + lua_pushstring(L, "entity_array"); + break; + default: + lua_pushstring(L, "unknown"); + break; + } + lua_setfield(L, -2, key.c_str()); + } +} + +// Helper: push EventParams values to Lua table +static void pushEventParamsToLua(lua_State *L, + const editScene::EventParams ¶ms) +{ + lua_newtable(L); + for (editScene::EventParams::ConstIterator it = params.begin(); + it != params.end(); ++it) { + const std::string &key = it->first; + const editScene::EventValue &val = it->second; + switch (val.getType()) { + case editScene::EventValue::INT: + lua_pushinteger(L, val.getInt()); + break; + case editScene::EventValue::FLOAT: + lua_pushnumber(L, (lua_Number)val.getFloat()); + break; + case editScene::EventValue::DOUBLE: + lua_pushnumber(L, (lua_Number)val.getDouble()); + break; + case editScene::EventValue::STRING: + lua_pushstring(L, val.getString().c_str()); + break; + case editScene::EventValue::ENTITY_ID: + lua_pushinteger(L, (lua_Integer)val.getEntityId()); + break; + case editScene::EventValue::INT_ARRAY: { + lua_newtable(L); + const auto &arr = val.getIntArray(); + for (size_t i = 0; i < arr.size(); i++) { + lua_pushinteger(L, arr[i]); + lua_rawseti(L, -2, i + 1); + } + break; + } + case editScene::EventValue::FLOAT_ARRAY: { + lua_newtable(L); + const auto &arr = val.getFloatArray(); + for (size_t i = 0; i < arr.size(); i++) { + lua_pushnumber(L, (lua_Number)arr[i]); + lua_rawseti(L, -2, i + 1); + } + break; + } + case editScene::EventValue::DOUBLE_ARRAY: { + lua_newtable(L); + const auto &arr = val.getDoubleArray(); + for (size_t i = 0; i < arr.size(); i++) { + lua_pushnumber(L, (lua_Number)arr[i]); + lua_rawseti(L, -2, i + 1); + } + break; + } + case editScene::EventValue::STRING_ARRAY: { + lua_newtable(L); + const auto &arr = val.getStringArray(); + for (size_t i = 0; i < arr.size(); i++) { + lua_pushstring(L, arr[i].c_str()); + lua_rawseti(L, -2, i + 1); + } + break; + } + case editScene::EventValue::ENTITY_ID_ARRAY: { + lua_newtable(L); + const auto &arr = val.getEntityIdArray(); + for (size_t i = 0; i < arr.size(); i++) { + lua_pushinteger(L, (lua_Integer)arr[i]); + lua_rawseti(L, -2, i + 1); + } + break; + } + default: + lua_pushnil(L); + break; + } + lua_setfield(L, -2, key.c_str()); + } + // Add _types metadata table + pushEventParamsTypes(L, params); + lua_setfield(L, -2, "_types"); +} + +// Helper: read a Lua value and add it to EventParams +static void readLuaValueToEventParams(lua_State *L, int idx, + const std::string &key, + editScene::EventParams ¶ms) +{ + int absIdx = lua_absindex(L, idx); + int type = lua_type(L, absIdx); + if (type == LUA_TNIL) { + // Skip nil values + } else if (type == LUA_TNUMBER) { + lua_Number val = lua_tonumber(L, absIdx); + // Check if it's an integer + if (val == (lua_Integer)val) { + params.setInt(key, (int)val); + } else { + // Store as double (Lua numbers are doubles) + params.setDouble(key, (double)val); + } + } else if (type == LUA_TSTRING) { + params.setString(key, lua_tostring(L, absIdx)); + } else if (type == LUA_TBOOLEAN) { + params.setInt(key, lua_toboolean(L, absIdx) ? 1 : 0); + } else if (type == LUA_TTABLE) { + // Check if it's an array (all integer keys) + bool isArray = true; + bool isStringArray = false; + bool isNumArray = false; + int maxKey = 0; + + lua_pushnil(L); + while (lua_next(L, absIdx) != 0) { + if (lua_type(L, -2) == LUA_TNUMBER) { + int k = (int)lua_tointeger(L, -2); + if (k > maxKey) + maxKey = k; + if (lua_type(L, -1) == LUA_TSTRING) + isStringArray = true; + else if (lua_type(L, -1) == LUA_TNUMBER) + isNumArray = true; + } else { + isArray = false; + } + lua_pop(L, 1); + } + + if (isArray && maxKey > 0) { + if (isStringArray) { + std::vector arr; + for (int i = 1; i <= maxKey; i++) { + lua_rawgeti(L, absIdx, i); + if (lua_type(L, -1) == LUA_TSTRING) + arr.push_back( + lua_tostring(L, -1)); + lua_pop(L, 1); + } + params.setStringArray(key, arr); + } else if (isNumArray) { + std::vector arr; + for (int i = 1; i <= maxKey; i++) { + lua_rawgeti(L, absIdx, i); + if (lua_type(L, -1) == LUA_TNUMBER) + arr.push_back( + lua_tonumber(L, -1)); + lua_pop(L, 1); + } + params.setDoubleArray(key, arr); + } + } + } +} + void registerLuaEventApi(lua_State *L) { // Get or create the "ecs" global table @@ -543,6 +835,22 @@ void registerLuaEventApi(lua_State *L) if (!eventName) return 0; + // Build EventParams from Lua table (arg 2) + editScene::EventParams params; + if (lua_istable(L, 2)) { + lua_pushnil(L); + while (lua_next(L, 2) != 0) { + if (lua_type(L, -2) == LUA_TSTRING) { + const char *key = lua_tostring(L, -2); + if (key) { + readLuaValueToEventParams( + L, -1, key, params); + } + } + lua_pop(L, 1); + } + } + // Call all matching subscriptions for (auto &sub : s_subscriptions) { if (sub.eventName == eventName) { @@ -551,12 +859,8 @@ void registerLuaEventApi(lua_State *L) sub.callbackRef); // Push event name lua_pushstring(L, eventName); - // Push params (table or nil) - if (lua_istable(L, 2)) { - lua_pushvalue(L, 2); - } else { - lua_pushnil(L); - } + // Push params as Lua table + pushEventParamsToLua(L, params); // Call callback(event, params) if (lua_pcall(L, 2, 0, 0) != LUA_OK) { fprintf(stderr, diff --git a/src/features/editScene/tests/ogre_stub.h b/src/features/editScene/tests/ogre_stub.h index 05d39ae..ba528bb 100644 --- a/src/features/editScene/tests/ogre_stub.h +++ b/src/features/editScene/tests/ogre_stub.h @@ -15,6 +15,7 @@ #include #include #include +#include namespace Ogre {