game_start event works

This commit is contained in:
2026-05-02 23:43:48 +03:00
parent 39a053d4ee
commit 76c3ead4a8
31 changed files with 3725 additions and 694 deletions

View File

@@ -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"

View File

@@ -1,5 +1,6 @@
#include <iostream>
#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<PlayerControllerSystem>(
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) {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -0,0 +1,739 @@
#ifndef EDITSCENE_EVENT_PARAMS_HPP
#define EDITSCENE_EVENT_PARAMS_HPP
#pragma once
#include <Ogre.h>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include <cassert>
/**
* @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<uint64_t>)
* - Array of integers (std::vector<int64_t>)
* - Array of floats (std::vector<float>)
* - Array of doubles (std::vector<double>)
* - Array of strings (std::vector<std::string>)
*
* 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<T>*
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<int64_t>(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<uint64_t> &arr)
: type(ENTITY_ID_ARRAY)
, asEntityId(0)
, strPtr(nullptr)
, arrayPtr(new std::vector<uint64_t>(arr))
, arraySize(arr.size())
{
}
explicit EventValue(const std::vector<int64_t> &arr)
: type(INT_ARRAY)
, asEntityId(0)
, strPtr(nullptr)
, arrayPtr(new std::vector<int64_t>(arr))
, arraySize(arr.size())
{
}
explicit EventValue(const std::vector<int> &arr)
: type(INT_ARRAY)
, asEntityId(0)
, strPtr(nullptr)
, arrayPtr(new std::vector<int64_t>(arr.begin(), arr.end()))
, arraySize(arr.size())
{
}
explicit EventValue(const std::vector<float> &arr)
: type(FLOAT_ARRAY)
, asEntityId(0)
, strPtr(nullptr)
, arrayPtr(new std::vector<float>(arr))
, arraySize(arr.size())
{
}
explicit EventValue(const std::vector<double> &arr)
: type(DOUBLE_ARRAY)
, asEntityId(0)
, strPtr(nullptr)
, arrayPtr(new std::vector<double>(arr))
, arraySize(arr.size())
{
}
explicit EventValue(const std::vector<std::string> &arr)
: type(STRING_ARRAY)
, asEntityId(0)
, strPtr(nullptr)
, arrayPtr(new std::vector<std::string>(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<uint64_t> &getEntityIdArray() const
{
assert(type == ENTITY_ID_ARRAY && arrayPtr != nullptr);
return *static_cast<const std::vector<uint64_t> *>(arrayPtr);
}
const std::vector<int64_t> &getIntArray() const
{
assert(type == INT_ARRAY && arrayPtr != nullptr);
return *static_cast<const std::vector<int64_t> *>(arrayPtr);
}
const std::vector<float> &getFloatArray() const
{
assert(type == FLOAT_ARRAY && arrayPtr != nullptr);
return *static_cast<const std::vector<float> *>(arrayPtr);
}
const std::vector<double> &getDoubleArray() const
{
assert(type == DOUBLE_ARRAY && arrayPtr != nullptr);
return *static_cast<const std::vector<double> *>(arrayPtr);
}
const std::vector<std::string> &getStringArray() const
{
assert(type == STRING_ARRAY && arrayPtr != nullptr);
return *static_cast<const std::vector<std::string> *>(arrayPtr);
}
// --- Convenience: get numeric value as double ---
double asNumeric() const
{
switch (type) {
case INT:
return static_cast<double>(asInt);
case FLOAT:
return static_cast<double>(asFloat);
case DOUBLE:
return asDouble;
case ENTITY_ID:
return static_cast<double>(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<uint64_t>(
*static_cast<const std::vector<uint64_t> *>(
other.arrayPtr));
} else if (other.type == INT_ARRAY && other.arrayPtr) {
arrayPtr = new std::vector<int64_t>(
*static_cast<const std::vector<int64_t> *>(
other.arrayPtr));
} else if (other.type == FLOAT_ARRAY && other.arrayPtr) {
arrayPtr = new std::vector<float>(
*static_cast<const std::vector<float> *>(
other.arrayPtr));
} else if (other.type == DOUBLE_ARRAY && other.arrayPtr) {
arrayPtr = new std::vector<double>(
*static_cast<const std::vector<double> *>(
other.arrayPtr));
} else if (other.type == STRING_ARRAY && other.arrayPtr) {
arrayPtr = new std::vector<std::string>(
*static_cast<const std::vector<std::string> *>(
other.arrayPtr));
}
}
void destroyHeapData()
{
if (type == STRING) {
delete strPtr;
} else if (type == ENTITY_ID_ARRAY) {
delete static_cast<std::vector<uint64_t> *>(arrayPtr);
} else if (type == INT_ARRAY) {
delete static_cast<std::vector<int64_t> *>(arrayPtr);
} else if (type == FLOAT_ARRAY) {
delete static_cast<std::vector<float> *>(arrayPtr);
} else if (type == DOUBLE_ARRAY) {
delete static_cast<std::vector<double> *>(arrayPtr);
} else if (type == STRING_ARRAY) {
delete static_cast<std::vector<std::string> *>(
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<uint64_t> &val)
{
m_values[key] = EventValue(val);
}
void setIntArray(const std::string &key,
const std::vector<int64_t> &val)
{
m_values[key] = EventValue(val);
}
void setFloatArray(const std::string &key,
const std::vector<float> &val)
{
m_values[key] = EventValue(val);
}
void setDoubleArray(const std::string &key,
const std::vector<double> &val)
{
m_values[key] = EventValue(val);
}
void setStringArray(const std::string &key,
const std::vector<std::string> &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<std::string, EventValue>::const_iterator
ConstIterator;
typedef std::unordered_map<std::string, EventValue>::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<std::string, EventValue> m_values;
};
} // namespace editScene
#endif // EDITSCENE_EVENT_PARAMS_HPP

View File

@@ -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)")

View File

@@ -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!")

View File

@@ -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

View File

@@ -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!")

View File

@@ -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!")

View File

@@ -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

View File

@@ -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!")

View File

@@ -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!")

View File

@@ -1,9 +1,8 @@
#include "LuaEventApi.hpp"
#include "LuaEntityApi.hpp"
#include "../systems/EventBus.hpp"
#include "../components/GoapBlackboard.hpp"
#include "../components/EventParams.hpp"
#include <OgreLogManager.h>
#include <OgreVector3.h>
#include <unordered_map>
namespace editScene
@@ -23,161 +22,312 @@ static std::unordered_map<int, EventBus::ListenerId> 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 &params)
{
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<std::string> 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<int64_t> 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<double> 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 &params) {
const EventParams &params) {
// 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) {

View File

@@ -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

View File

@@ -0,0 +1,171 @@
#include "LuaGameModeApi.hpp"
#include "../GameMode.hpp"
#include <OgreLogManager.h>
#include <cstdio>
#include <cstdlib>
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

View File

@@ -0,0 +1,39 @@
#ifndef EDITSCENE_LUA_GAMEMODE_API_HPP
#define EDITSCENE_LUA_GAMEMODE_API_HPP
#pragma once
#include <lua.hpp>
/**
* @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

View File

@@ -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)

View File

@@ -2,7 +2,7 @@
#include "../EditorApp.hpp"
#include "../components/DialogueComponent.hpp"
#include "../systems/EventBus.hpp"
#include "../components/GoapBlackboard.hpp"
#include "../components/EventParams.hpp"
#include <imgui.h>
#include <OgreFontManager.h>
#include <OgreImGuiOverlay.h>
@@ -18,8 +18,8 @@ DialogueSystem::DialogueSystem(flecs::world &world,
{
// Subscribe to dialogue events
EventBus::getInstance().subscribe(
"dialogue_show",
[this](const Ogre::String &, const GoapBlackboard &params) {
"dialogue_show", [this](const Ogre::String &,
const editScene::EventParams &params) {
// Find the first entity with DialogueComponent
m_world.query<DialogueComponent>().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<Ogre::String> 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);
});

View File

@@ -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 {

View File

@@ -38,7 +38,7 @@ void EventBus::unsubscribe(ListenerId id)
}
void EventBus::send(const Ogre::String &eventName,
const GoapBlackboard &params)
const editScene::EventParams &params)
{
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 &paramName, int value)
const Ogre::String &paramName, 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 &paramName, int64_t value)
{
editScene::EventParams params;
params.setInt(paramName, value);
send(eventName, params);
}
void EventBus::send(const Ogre::String &eventName,
const Ogre::String &paramName, 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 &paramName,
const Ogre::Vector3 &value)
const Ogre::String &paramName, 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 &paramName, uint64_t entityId)
{
editScene::EventParams params;
params.setEntityId(paramName, entityId);
send(eventName, params);
}

View File

@@ -2,7 +2,7 @@
#define EDITSCENE_EVENT_BUS_HPP
#pragma once
#include "../components/GoapBlackboard.hpp"
#include "../components/EventParams.hpp"
#include <Ogre.h>
#include <cstdint>
#include <functional>
@@ -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<void(const Ogre::String &eventName,
const GoapBlackboard &params)>;
const editScene::EventParams &params)>;
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 &params = {});
const editScene::EventParams &params = {});
/** Convenience: send event with a single string param. */
void send(const Ogre::String &eventName, const Ogre::String &paramName,
const Ogre::String &value);
/** Convenience: send event with a single int param. */
void send(const Ogre::String &eventName,
const Ogre::String &paramName, int value);
void send(const Ogre::String &eventName, const Ogre::String &paramName,
int64_t value);
/** Convenience: send event with a single float param. */
void send(const Ogre::String &eventName,
const Ogre::String &paramName, float value);
void send(const Ogre::String &eventName, const Ogre::String &paramName,
float value);
/** Convenience: send event with a single Vec3 param. */
void send(const Ogre::String &eventName,
const Ogre::String &paramName,
const Ogre::Vector3 &value);
/** Convenience: send event with a single double param. */
void send(const Ogre::String &eventName, const Ogre::String &paramName,
double value);
/** Convenience: send event with a single entity ID param. */
void send(const Ogre::String &eventName, const Ogre::String &paramName,
uint64_t entityId);
private:
EventBus() = default;
@@ -59,7 +66,7 @@ private:
};
ListenerId m_nextId = 1;
std::unordered_map<Ogre::String, std::vector<Listener>> m_listeners;
std::unordered_map<Ogre::String, std::vector<Listener> > m_listeners;
std::unordered_map<ListenerId, Ogre::String> m_idToEvent;
};

View File

@@ -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 <OgreLogManager.h>
// 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<std::string> 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 &params) {
this->onEvent(id, handler.eventName, params);
[this, id](const Ogre::String &eventName,
const editScene::EventParams &params) {
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 &params)
const editScene::EventParams &params)
{
(void)eventName;
flecs::entity e = m_world.entity(entityId);
if (!e.is_alive() || !e.has<EventHandlerComponent>())
// Check if entity is still alive
flecs::entity e(m_world, entityId);
if (!e.is_alive())
return;
auto &handler = e.get<EventHandlerComponent>();
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<EventHandlerComponent>()) {
Ogre::LogManager::getSingleton().stream()
<< "EventHandlerSystem: Entity " << entityId
<< " has no EventHandlerComponent";
return;
}
// Inject params immediately so the first update() tick sees them
std::unordered_set<std::string> injected;
injectParams(e, params, injected);
m_injectedKeys[entityId] = std::move(injected);
const EventHandlerComponent &handlerComp =
e.get<EventHandlerComponent>();
// 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 &params,
std::unordered_set<std::string> &outKeys)
void EventHandlerSystem::injectParams(flecs::entity e,
const editScene::EventParams &params,
std::unordered_set<std::string> &outKeys)
{
if (!e.has<GoapBlackboard>())
e.set<GoapBlackboard>({});
// 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<GoapBlackboard>();
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<std::string> &keys)
flecs::entity e, const std::unordered_set<std::string> &keys)
{
if (!e.has<GoapBlackboard>())
return;
auto &bb = e.get_mut<GoapBlackboard>();
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<flecs::entity_t> currentEntities;
m_world.query<EventHandlerComponent>().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<flecs::entity_t> 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<flecs::entity_t> 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();
}

View File

@@ -2,25 +2,23 @@
#define EDITSCENE_EVENT_HANDLER_SYSTEM_HPP
#pragma once
#include "EventBus.hpp"
#include "../components/EventParams.hpp"
#include <flecs.h>
#include <Ogre.h>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#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 &params);
void injectParams(flecs::entity e, const GoapBlackboard &params,
void onEvent(flecs::entity_t entityId, const Ogre::String &eventName,
const editScene::EventParams &params);
void injectParams(flecs::entity e, const editScene::EventParams &params,
std::unordered_set<std::string> &outKeys);
void removeParams(flecs::entity e,
const std::unordered_set<std::string> &keys);
@@ -51,14 +50,16 @@ private:
flecs::world &m_world;
BehaviorTreeSystem *m_btSystem;
// Per-entity event subscription
std::unordered_map<flecs::entity_t, EventBus::ListenerId> m_subscriptions;
// Map from entity ID to EventBus listener ID
std::unordered_map<flecs::entity_t, EventBus::ListenerId>
m_subscriptions;
// Per-entity active handler (one at a time per entity)
// Currently active handlers (entity ID -> handler state)
std::unordered_map<flecs::entity_t, ActiveHandler> m_activeHandlers;
// Keys injected into blackboard per active handler
std::unordered_map<flecs::entity_t, std::unordered_set<std::string>> m_injectedKeys;
// Keys injected into each entity's blackboard (for cleanup)
std::unordered_map<flecs::entity_t, std::unordered_set<std::string> >
m_injectedKeys;
};
#endif // EDITSCENE_EVENT_HANDLER_SYSTEM_HPP

View File

@@ -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);

View File

@@ -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 <cstdio>
#include <cstring>
#include <cassert>
#include <string>
#include <vector>
#include <cmath>
// 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<uint64_t> 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<int64_t> 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<float> 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<double> 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<std::string> 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<int64_t> 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<float> 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<double> 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<std::string> 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<uint64_t> 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;
}

View File

@@ -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 <cstdio>
#include <cstring>
#include <cassert>
#include <string>
#include <vector>
// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager)
#include "ogre_stub.h"
// Include Lua
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}
// 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;
}

View File

@@ -12,6 +12,7 @@
*/
#include "ogre_stub.h"
#include "../components/EventParams.hpp"
#include <lua.hpp>
#include <cstdio>
#include <cassert>
@@ -44,6 +45,97 @@ using ComponentData = std::unordered_map<std::string, ComponentFieldValue>;
std::unordered_map<int, std::unordered_map<std::string, ComponentData> >
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<EventSubscription> s_subscriptions;
static int s_nextSubId = 1;
// Helper: push EventParams type metadata to Lua table
static void pushEventParamsTypes(lua_State *L,
const editScene::EventParams &params)
{
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 &params)
{
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 &params)
{
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<std::string> 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<double> 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,

View File

@@ -15,6 +15,7 @@
#include <string>
#include <vector>
#include <cstdint>
#include <unordered_map>
namespace Ogre
{