Compare commits

..

42 Commits

Author SHA1 Message Date
11530dd7fc Dialogue uses arrays 2026-05-03 01:25:25 +03:00
3fd167ebff More events added 2026-05-03 01:11:14 +03:00
5952a96ee6 Fixed fonts sizes 2026-05-03 00:02:24 +03:00
76c3ead4a8 game_start event works 2026-05-02 23:43:48 +03:00
39a053d4ee Game mode API 2026-05-02 20:25:16 +03:00
c5da977857 Dualogue event API doc/samples 2026-05-02 18:25:30 +03:00
3e7b0169d5 Lua behavior tree 2026-05-01 13:54:44 +03:00
f918c5cefb Better handling of lua tasks 2026-05-01 04:02:47 +03:00
976ced3731 Lua-based behavior tree node 2026-05-01 00:31:06 +03:00
0fd8deaf53 direct save/load action database 2026-04-30 20:21:18 +03:00
4d843c18c7 Lua API 2026-04-30 19:07:35 +03:00
0ed83966da Lua action APIs 2026-04-30 10:03:56 +03:00
998984f75a Root motion fixed now 2026-04-29 18:45:37 +03:00
02fa78764a Lua API implemented 2026-04-29 14:13:50 +03:00
abe6eef6b3 Modules update 2026-04-29 12:53:13 +03:00
cca732b41b Fixes for event system 2026-04-27 20:53:04 +03:00
8507a3a501 Event system 2026-04-27 18:45:01 +03:00
b9cce0248a Labels and actuators work perfectly! 2026-04-27 09:03:47 +03:00
fa49bb5005 Actuators 2026-04-27 06:55:05 +03:00
37441aa8fd Motion fixed 2026-04-27 06:04:14 +03:00
a1b74aa2d5 Now Path Following component works 2026-04-27 05:37:41 +03:00
c80d9c96e6 AI motion refactoring 2026-04-27 05:24:45 +03:00
a75db85027 Can disable physics stepping 2026-04-26 22:15:15 +03:00
7563937ab8 Prefab placement at cursor 2026-04-26 20:51:20 +03:00
425bb8411d Fixed crash with entity destruction 2026-04-26 17:56:19 +03:00
9b29b68b33 Prefab editing and window hiding 2026-04-26 17:45:57 +03:00
7557c710fb Added prefabs 2026-04-26 16:43:37 +03:00
ce2f6c1306 Navmesh generation works with cell grids 2026-04-26 14:28:54 +03:00
e0e8e316d4 Fixed navmesh 2026-04-26 00:50:55 +03:00
abd2dc22d3 Now can test smart object action on player character again 2026-04-26 00:00:36 +03:00
a5df60769f Now repeated smart object action works perfectly 2026-04-25 23:08:00 +03:00
75ba39895f Teleport node works 2026-04-25 21:55:21 +03:00
2cff982473 Delays and animation conflicts fixed 2026-04-25 20:59:50 +03:00
3bd2801d1d Smart objects work! 2026-04-25 09:04:12 +03:00
2e358275f0 Path following works great 2026-04-25 01:17:31 +03:00
5ed7552164 Path following 2026-04-25 00:11:21 +03:00
2b3482da88 Normal display tool implemented 2026-04-24 21:12:02 +03:00
1d2c330481 Material fixes, playing with navmesh 2026-04-24 20:29:54 +03:00
a0d2561587 navmesh 2026-04-24 04:37:38 +03:00
e95b904f4e Underwater effect 2026-04-23 01:55:09 +03:00
9d4fad1d10 Water plane 2026-04-23 01:29:53 +03:00
4335a8cb05 Skybox and sun 2026-04-23 00:38:20 +03:00
414 changed files with 209987 additions and 1126 deletions

View File

@@ -69,3 +69,27 @@ material Debug/Red2
}
}
/**
* Debug material for normal visualization overlay.
* Renders on top of everything (depth_check off, depth_write off)
* Uses vertex colours so each normal line can have its own colour.
* Rendered in overlay queue to appear on top of all geometry.
*/
material Debug/NormalOverlay
{
technique
{
pass
{
lighting off
depth_check off
depth_write off
ambient 1.0 1.0 1.0 1.0
diffuse vertexcolour
specular 0.0 0.0 0.0 1.0
cull_software none
cull_hardware none
scene_blend alpha_blend
}
}
}

View File

@@ -8,14 +8,21 @@ find_package(SDL2 REQUIRED)
find_package(Jolt REQUIRED)
find_package(OgreProcedural REQUIRED CONFIG)
# Build RecastNavigation from copied source (RTTI-compatible)
add_subdirectory(recastnavigation)
# Collect all source files
set(EDITSCENE_SOURCES
main.cpp
EditorApp.cpp
GameMode.cpp
systems/EditorUISystem.cpp
systems/SceneSerializer.cpp
systems/PhysicsSystem.cpp
systems/BuoyancySystem.cpp
systems/EditorSunSystem.cpp
systems/EditorSkyboxSystem.cpp
systems/EditorWaterPlaneSystem.cpp
systems/LightSystem.cpp
systems/CameraSystem.cpp
systems/LodSystem.cpp
@@ -24,13 +31,47 @@ set(EDITSCENE_SOURCES
systems/ProceduralMaterialSystem.cpp
systems/ProceduralMeshSystem.cpp
systems/CellGridSystem.cpp
systems/NormalDebugSystem.cpp
systems/RoomLayoutSystem.cpp
systems/FurnitureLibrary.cpp
systems/StartupMenuSystem.cpp
systems/PlayerControllerSystem.cpp
systems/CharacterSlotSystem.cpp
systems/AnimationTreeSystem.cpp
systems/BehaviorTreeSystem.cpp
systems/NavMeshSystem.cpp
recast/TileCacheNavMesh.cpp
recast/PartitionedMesh.cpp
recast/fastlz.c
systems/CharacterSystem.cpp
systems/SmartObjectSystem.cpp
components/SmartObjectModule.cpp
ui/SmartObjectEditor.cpp
components/GoapPlannerModule.cpp
systems/GoapRunnerSystem.cpp
systems/PathFollowingSystem.cpp
systems/GoapPlannerSystem.cpp
components/GoapRunnerModule.cpp
components/PathFollowingModule.cpp
ui/GoapRunnerEditor.cpp
ui/PathFollowingEditor.cpp
ui/GoapPlannerEditor.cpp
systems/ActuatorSystem.cpp
ui/ActuatorEditor.cpp
components/ActuatorModule.cpp
systems/EventBus.cpp
systems/EventHandlerSystem.cpp
ui/EventHandlerEditor.cpp
components/EventHandlerModule.cpp
systems/PrefabSystem.cpp
ui/PrefabInstanceEditor.cpp
systems/ItemSystem.cpp
components/ItemModule.cpp
components/InventoryModule.cpp
ui/ItemEditor.cpp
ui/InventoryEditor.cpp
ui/TransformEditor.cpp
ui/RenderableEditor.cpp
ui/PhysicsColliderEditor.cpp
@@ -62,7 +103,22 @@ set(EDITSCENE_SOURCES
ui/StartupMenuEditor.cpp
ui/PlayerControllerEditor.cpp
ui/BuoyancyInfoEditor.cpp
ui/GoapBlackboardEditor.cpp
ui/BehaviorTreeEditor.cpp
ui/InlineBehaviorTreeEditor.cpp
ui/NavMeshEditor.cpp
ui/ActionDatabaseEditor.cpp
ui/ActionDatabaseSingletonEditor.cpp
ui/ActionDebugEditor.cpp
ui/ComponentRegistration.cpp
components/GoapBlackboard.cpp
components/GoapExpression.cpp
components/GoapGoal.cpp
components/ActionDatabase.cpp
components/ActionDatabaseModule.cpp
components/ActionDebugModule.cpp
components/GoapBlackboardModule.cpp
components/NavMeshModule.cpp
components/LightModule.cpp
components/CameraModule.cpp
components/LodModule.cpp
@@ -81,12 +137,25 @@ set(EDITSCENE_SOURCES
components/CellGrid.cpp
components/StartupMenuModule.cpp
components/PlayerControllerModule.cpp
components/DialogueComponentModule.cpp
systems/DialogueSystem.cpp
ui/DialogueEditor.cpp
components/BuoyancyInfoModule.cpp
components/WaterPhysicsModule.cpp
components/WaterPlaneModule.cpp
components/SunModule.cpp
components/SkyboxModule.cpp
camera/EditorCamera.cpp
gizmo/Gizmo.cpp
gizmo/Cursor3D.cpp
physics/physics.cpp
lua/LuaState.cpp
lua/LuaEntityApi.cpp
lua/LuaComponentApi.cpp
lua/LuaEventApi.cpp
lua/LuaActionApi.cpp
lua/LuaBehaviorTreeApi.cpp
lua/LuaGameModeApi.cpp
)
set(EDITSCENE_HEADERS
@@ -100,6 +169,8 @@ set(EDITSCENE_HEADERS
components/BuoyancyInfo.hpp
components/WaterPhysics.hpp
components/WaterPlane.hpp
components/Sun.hpp
components/Skybox.hpp
components/Light.hpp
components/Camera.hpp
components/Lod.hpp
@@ -116,22 +187,62 @@ set(EDITSCENE_HEADERS
components/CellGrid.hpp
components/StartupMenu.hpp
components/PlayerController.hpp
components/DialogueComponent.hpp
systems/DialogueSystem.hpp
ui/DialogueEditor.hpp
systems/StartupMenuSystem.hpp
systems/PlayerControllerSystem.hpp
systems/EditorUISystem.hpp
systems/CellGridSystem.hpp
systems/NormalDebugSystem.hpp
systems/RoomLayoutSystem.hpp
systems/FurnitureLibrary.hpp
systems/ProceduralMaterialSystem.hpp
systems/ProceduralMeshSystem.hpp
systems/CharacterSlotSystem.hpp
systems/AnimationTreeSystem.hpp
systems/BehaviorTreeSystem.hpp
systems/NavMeshSystem.hpp
recast/TileCacheNavMesh.hpp
recast/PartitionedMesh.hpp
recast/fastlz.h
systems/CharacterSystem.hpp
systems/SmartObjectSystem.hpp
components/SmartObject.hpp
ui/SmartObjectEditor.hpp
components/GoapPlanner.hpp
components/PathFollowing.hpp
components/GoapRunner.hpp
ui/GoapPlannerEditor.hpp
ui/GoapRunnerEditor.hpp
ui/PathFollowingEditor.hpp
systems/PrefabSystem.hpp
systems/GoapRunnerSystem.hpp
systems/PathFollowingSystem.hpp
systems/GoapPlannerSystem.hpp
components/Actuator.hpp
ui/ActuatorEditor.hpp
systems/EventBus.hpp
components/EventHandler.hpp
systems/EventHandlerSystem.hpp
ui/EventHandlerEditor.hpp
components/PrefabInstance.hpp
ui/PrefabInstanceEditor.hpp
systems/ItemSystem.hpp
components/Item.hpp
components/Inventory.hpp
ui/ItemEditor.hpp
ui/InventoryEditor.hpp
systems/ProceduralTextureSystem.hpp
systems/StaticGeometrySystem.hpp
systems/SceneSerializer.hpp
systems/PhysicsSystem.hpp
systems/BuoyancySystem.hpp
systems/EditorSunSystem.hpp
systems/EditorSkyboxSystem.hpp
systems/EditorWaterPlaneSystem.hpp
systems/LightSystem.hpp
systems/CameraSystem.hpp
systems/LodSystem.hpp
@@ -169,9 +280,34 @@ set(EDITSCENE_HEADERS
ui/StartupMenuEditor.hpp
ui/PlayerControllerEditor.hpp
ui/BuoyancyInfoEditor.hpp
ui/GoapBlackboardEditor.hpp
ui/GoapBlackboardComponentEditor.hpp
ui/BehaviorTreeEditor.hpp
ui/InlineBehaviorTreeEditor.hpp
ui/NavMeshEditor.hpp
ui/NavMeshGeometrySourceEditor.hpp
ui/ActionDatabaseEditor.hpp
ui/ActionDatabaseSingletonEditor.hpp
ui/ActionDebugEditor.hpp
components/GoapBlackboard.hpp
components/GoapExpression.hpp
components/NavMesh.hpp
components/BehaviorTree.hpp
components/GoapAction.hpp
components/GoapGoal.hpp
components/ActionDatabase.hpp
components/ActionDebug.hpp
camera/EditorCamera.hpp
gizmo/Gizmo.hpp
gizmo/Cursor3D.hpp
physics/physics.h
lua/LuaState.hpp
lua/LuaEntityApi.hpp
lua/LuaComponentApi.hpp
lua/LuaEventApi.hpp
lua/LuaActionApi.hpp
lua/LuaBehaviorTreeApi.hpp
lua/LuaGameModeApi.hpp
)
add_executable(editSceneEditor ${EDITSCENE_SOURCES} ${EDITSCENE_HEADERS})
@@ -188,10 +324,154 @@ target_link_libraries(editSceneEditor
nlohmann_json::nlohmann_json
Jolt::Jolt
OgreProcedural::OgreProcedural
RecastNavigation::Recast
RecastNavigation::Detour
RecastNavigation::DetourTileCache
RecastNavigation::DetourCrowd
RecastNavigation::DebugUtils
lua
)
target_include_directories(editSceneEditor PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/Recast/Include
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/Detour/Include
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/DetourTileCache/Include
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/DetourCrowd/Include
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/DebugUtils/Include
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
${CMAKE_SOURCE_DIR}/src/lua/lpeg-1.1.0
)
# ---------------------------------------------------------------------------
# Tests: Lua API standalone tests
# ---------------------------------------------------------------------------
# These standalone tests verify the Lua API functions work correctly.
# They do not require OGRE or Flecs - only Lua and the core component types.
# Test: Entity Lua API
add_executable(entity_lua_test
tests/entity_lua_test.cpp
tests/lua_test_stubs.cpp
)
target_link_libraries(entity_lua_test
lua
)
target_include_directories(entity_lua_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tests
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
)
# Test: Component Lua API
add_executable(component_lua_test
tests/component_lua_test.cpp
tests/lua_test_stubs.cpp
)
target_link_libraries(component_lua_test
lua
)
target_include_directories(component_lua_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tests
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
)
# Test: Event Lua API
add_executable(event_lua_test
tests/event_lua_test.cpp
tests/lua_test_stubs.cpp
)
target_link_libraries(event_lua_test
lua
)
target_include_directories(event_lua_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tests
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
)
# Test: ActionDatabase Lua API
add_executable(action_db_lua_test
tests/action_db_lua_test.cpp
components/ActionDatabase.cpp
components/GoapBlackboard.cpp
components/GoapGoal.cpp
components/GoapExpression.cpp
lua/LuaActionApi.cpp
)
target_link_libraries(action_db_lua_test
lua
flecs::flecs_static
nlohmann_json::nlohmann_json
)
target_include_directories(action_db_lua_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tests
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
)
# Test: Behavior Tree Lua API
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
)
target_link_libraries(behavior_tree_lua_test
lua
flecs::flecs_static
OgreMain
)
target_include_directories(behavior_tree_lua_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tests
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
)
target_compile_definitions(behavior_tree_lua_test PRIVATE flecs_STATIC)
# 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.)
@@ -214,5 +494,9 @@ add_custom_command(TARGET editSceneEditor POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
"${CMAKE_CURRENT_SOURCE_DIR}/resources.cfg"
"${CMAKE_CURRENT_BINARY_DIR}/resources.cfg"
# Re-copy editScene-specific resources so they aren't overwritten
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_CURRENT_SOURCE_DIR}/resources"
"${CMAKE_CURRENT_BINARY_DIR}/resources"
COMMENT "Copying resources to editSceneEditor build directory"
)

View File

@@ -1,8 +1,12 @@
#include <iostream>
#include "EditorApp.hpp"
#include "GameMode.hpp"
#include "systems/EditorUISystem.hpp"
#include "systems/PhysicsSystem.hpp"
#include "systems/BuoyancySystem.hpp"
#include "systems/EditorSunSystem.hpp"
#include "systems/EditorSkyboxSystem.hpp"
#include "systems/EditorWaterPlaneSystem.hpp"
#include "systems/LightSystem.hpp"
#include "systems/CameraSystem.hpp"
#include "systems/LodSystem.hpp"
@@ -12,10 +16,19 @@
#include "systems/ProceduralMeshSystem.hpp"
#include "systems/CharacterSlotSystem.hpp"
#include "systems/AnimationTreeSystem.hpp"
#include "systems/BehaviorTreeSystem.hpp"
#include "systems/NavMeshSystem.hpp"
#include "systems/CharacterSystem.hpp"
#include "systems/SmartObjectSystem.hpp"
#include "systems/GoapRunnerSystem.hpp"
#include "systems/PathFollowingSystem.hpp"
#include "systems/GoapPlannerSystem.hpp"
#include "systems/CellGridSystem.hpp"
#include "systems/NormalDebugSystem.hpp"
#include "systems/RoomLayoutSystem.hpp"
#include "systems/StartupMenuSystem.hpp"
#include "systems/DialogueSystem.hpp"
#include "systems/PlayerControllerSystem.hpp"
#include "systems/SceneSerializer.hpp"
#include "camera/EditorCamera.hpp"
@@ -30,6 +43,8 @@
#include "components/InWater.hpp"
#include "components/WaterPhysics.hpp"
#include "components/WaterPlane.hpp"
#include "components/Sun.hpp"
#include "components/Skybox.hpp"
#include "components/Light.hpp"
#include "components/Camera.hpp"
#include "components/Lod.hpp"
@@ -45,11 +60,38 @@
#include "components/AnimationTreeTemplate.hpp"
#include "components/Character.hpp"
#include "components/StartupMenu.hpp"
#include "components/DialogueComponent.hpp"
#include "components/PlayerController.hpp"
#include "components/CellGrid.hpp"
#include "components/CellGridModule.hpp"
#include "components/ActionDatabase.hpp"
#include "components/ActionDebug.hpp"
#include "components/BehaviorTree.hpp"
#include "components/GoapBlackboard.hpp"
#include "components/PrefabInstance.hpp"
#include "systems/PrefabSystem.hpp"
#include "components/NavMesh.hpp"
#include "components/SmartObject.hpp"
#include "components/Actuator.hpp"
#include "components/GoapPlanner.hpp"
#include "components/GoapRunner.hpp"
#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"
#include "components/Inventory.hpp"
#include <OgreRTShaderSystem.h>
#include <imgui.h>
#include "lua/LuaEntityApi.hpp"
#include "lua/LuaComponentApi.hpp"
#include "lua/LuaEventApi.hpp"
#include "lua/LuaActionApi.hpp"
#include "lua/LuaBehaviorTreeApi.hpp"
#include "lua/LuaGameModeApi.hpp"
//=============================================================================
// ImGuiRenderListener Implementation
@@ -85,6 +127,14 @@ void ImGuiRenderListener::preViewportUpdate(
m_uiSystem->update(m_deltaTime);
}
// Render actuator markers in game mode (after NewFrame so draw
// commands survive)
if (m_editorApp) {
ActuatorSystem *actuatorSys = m_editorApp->getActuatorSystem();
if (actuatorSys)
actuatorSys->render();
}
// Render startup menu in game mode (inside ImGui frame scope)
if (m_editorApp &&
m_editorApp->getGameMode() == EditorApp::GameMode::Game &&
@@ -93,6 +143,16 @@ void ImGuiRenderListener::preViewportUpdate(
if (sms)
sms->update(m_deltaTime);
}
// Render dialogue box in game mode (inside ImGui frame scope)
if (m_editorApp &&
m_editorApp->getGameMode() == EditorApp::GameMode::Game &&
m_editorApp->getGamePlayState() ==
EditorApp::GamePlayState::Playing) {
DialogueSystem *ds = m_editorApp->getDialogueSystem();
if (ds)
ds->update(m_deltaTime);
}
}
void ImGuiRenderListener::postViewportUpdate(
@@ -138,22 +198,27 @@ EditorApp::~EditorApp()
// This ensures all components with Ogre resources are cleaned up while SceneManager exists
// Collect entities first, then delete after iteration (can't modify during iteration)
std::vector<flecs::entity> entitiesToDelete;
m_world.query<EditorMarkerComponent>().each(
[&](flecs::entity e, EditorMarkerComponent) {
entitiesToDelete.push_back(e);
});
for (auto &e : entitiesToDelete) {
if (e.is_alive()) {
e.destruct();
}
}
// Release all systems
m_playerControllerSystem.reset();
// Destroy dialogue system before other systems
m_dialogueSystem.reset();
m_startupMenuSystem.reset();
m_characterSlotSystem.reset();
m_animationTreeSystem.reset();
m_playerControllerSystem.reset();
m_itemSystem.reset();
m_eventHandlerSystem.reset();
m_actuatorSystem.reset();
m_goapPlannerSystem.reset();
m_pathFollowingSystem.reset();
m_goapRunnerSystem.reset();
m_smartObjectSystem.reset();
m_roomLayoutSystem.reset();
m_normalDebugSystem.reset();
m_cellGridSystem.reset();
m_characterSystem.reset();
m_navMeshSystem.reset();
m_behaviorTreeSystem.reset();
m_animationTreeSystem.reset();
m_characterSlotSystem.reset();
m_proceduralMeshSystem.reset();
m_proceduralMaterialSystem.reset();
m_proceduralTextureSystem.reset();
@@ -218,6 +283,7 @@ void EditorApp::setup()
if (m_uiSystem)
m_uiSystem->setEditorUIEnabled(m_gameMode ==
GameMode::Editor);
m_uiSystem->setEditorCamera(m_camera.get());
// Setup physics system
m_physicsSystem = std::make_unique<EditorPhysicsSystem>(
@@ -231,6 +297,13 @@ void EditorApp::setup()
m_world, m_physicsSystem->getPhysicsWrapper());
m_buoyancySystem->initialize();
m_sunSystem =
std::make_unique<EditorSunSystem>(m_world, m_sceneMgr);
m_skyboxSystem = std::make_unique<EditorSkyboxSystem>(
m_world, m_sceneMgr);
m_waterPlaneSystem = std::make_unique<EditorWaterPlaneSystem>(
m_world, m_sceneMgr);
// Apply debug setting if it was set before system creation
if (m_debugBuoyancy) {
m_buoyancySystem->setDebugEnabled(true);
@@ -286,16 +359,82 @@ void EditorApp::setup()
m_world, m_sceneMgr);
m_animationTreeSystem->initialize();
// Setup Character physics system
// Setup Character physics system (needed by BehaviorTreeSystem)
m_characterSystem =
std::make_unique<CharacterSystem>(m_world, m_sceneMgr);
m_characterSystem->initialize();
m_behaviorTreeSystem = std::make_unique<BehaviorTreeSystem>(
m_world, m_sceneMgr, m_animationTreeSystem.get(),
m_characterSystem.get());
// Setup NavMesh system
m_navMeshSystem =
std::make_unique<NavMeshSystem>(m_world, m_sceneMgr);
// Setup SmartObject system (requires NavMesh, BehaviorTree, and AnimationTree)
m_smartObjectSystem = std::make_unique<SmartObjectSystem>(
m_world, m_sceneMgr, m_navMeshSystem.get(),
m_behaviorTreeSystem.get());
// Wire up AnimationTreeSystem for animation state machine control
m_smartObjectSystem->setAnimationTreeSystem(
m_animationTreeSystem.get());
// Wire up EditorApp for game mode detection
m_smartObjectSystem->setEditorApp(this);
// Setup Actuator system
m_actuatorSystem = std::make_unique<ActuatorSystem>(
m_world, m_sceneMgr, this, m_behaviorTreeSystem.get());
// Setup Event Handler system
m_eventHandlerSystem = std::make_unique<EventHandlerSystem>(
m_world, m_behaviorTreeSystem.get());
// Setup Item system
m_itemSystem = std::make_unique<ItemSystem>(
m_world, m_sceneMgr, this, m_behaviorTreeSystem.get());
// Wire ItemSystem into SmartObjectSystem for BT access
m_smartObjectSystem->setItemSystem(m_itemSystem.get());
// Setup GOAP Runner system
m_goapRunnerSystem = std::make_unique<GoapRunnerSystem>(
m_world, m_sceneMgr, m_smartObjectSystem.get(),
m_behaviorTreeSystem.get(), m_navMeshSystem.get());
m_goapRunnerSystem->setEditorApp(this);
m_goapRunnerSystem->setAnimationTreeSystem(
m_animationTreeSystem.get());
// Setup GOAP Planner system
m_goapPlannerSystem =
std::make_unique<GoapPlannerSystem>(m_world);
m_goapPlannerSystem->setEditorApp(this);
// Setup Path Following system
m_pathFollowingSystem = std::make_unique<PathFollowingSystem>(
m_world, m_sceneMgr, m_navMeshSystem.get());
m_pathFollowingSystem->setAnimationTreeSystem(
m_animationTreeSystem.get());
// Setup CellGrid system
m_cellGridSystem =
std::make_unique<CellGridSystem>(m_world, m_sceneMgr);
m_cellGridSystem->initialize();
// Wire CellGridSystem into NavMeshSystem so it can collect
// batched frame/furniture geometry from StaticGeometry.
m_navMeshSystem->setCellGridSystem(m_cellGridSystem.get());
// Setup NormalDebug system (disabled by default)
m_normalDebugSystem = std::make_unique<NormalDebugSystem>(
m_world, m_sceneMgr, m_cellGridSystem.get());
// Wire NormalDebugSystem into UI for toggle
if (m_uiSystem) {
m_uiSystem->setNormalDebugSystem(
m_normalDebugSystem.get());
}
// Setup RoomLayout system
m_roomLayoutSystem =
std::make_unique<RoomLayoutSystem>(m_world, m_sceneMgr);
@@ -304,17 +443,23 @@ void EditorApp::setup()
// Setup game systems
m_startupMenuSystem = std::make_unique<StartupMenuSystem>(
m_world, m_sceneMgr, this);
m_dialogueSystem = std::make_unique<DialogueSystem>(
m_world, m_sceneMgr, this);
m_playerControllerSystem =
std::make_unique<PlayerControllerSystem>(
m_world, m_sceneMgr, this);
if (m_gameMode == GameMode::Game) {
// Load startup menu scene configured in editor
// Load startup menu scene configured in editor.
// This must happen before show() so the
// StartupMenuComponent entity exists for font preparation.
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 {
@@ -322,12 +467,16 @@ void EditorApp::setup()
"Game mode: Failed to load startup_menu.json: " +
serializer.getLastError());
}
}
// Pre-load menu font before showing overlay
// (OGRE builds the atlas in createFontTexture() during show())
if (m_startupMenuSystem)
m_startupMenuSystem->prepareFont();
// Pre-load fonts before showing overlay so the font is
// added to the atlas before OGRE builds it in createFontTexture().
// The StartupMenuComponent entity is now available from the
// startup_menu.json scene loaded above.
if (m_startupMenuSystem)
m_startupMenuSystem->prepareFont();
if (m_dialogueSystem)
m_dialogueSystem->prepareFont();
}
// Now show the overlay — font atlas will be built with our font
if (m_imguiOverlay)
@@ -348,6 +497,49 @@ void EditorApp::setup()
addInputListener(this);
addInputListener(getImGuiInputListener());
// Initialize Lua scripting
{
lua_State *L = m_lua.getState();
// Store the Flecs world pointer in the Lua registry
// so Lua API functions can access it.
flecs::world *worldPtr = &m_world;
lua_pushlightuserdata(L, worldPtr);
lua_setfield(L, LUA_REGISTRYINDEX,
"EditSceneFlecsWorld");
// Register all Lua API modules.
// Order matters: Entity API creates the "ecs" table,
// Component and Event APIs add to it.
editScene::registerLuaEntityApi(L);
editScene::registerLuaComponentApi(L);
editScene::registerLuaEventApi(L);
editScene::registerLuaActionApi(L);
editScene::registerLuaBehaviorTreeApi(L);
editScene::registerLuaGameModeApi(L);
// Run late setup: load data.lua and initial scripts.
m_lua.lateSetup();
// Auto-load Action Database from "actions.json" in the General
// resource group. If the file is missing or contains errors
// the error is logged and the database stays empty (scene
// components will still be processed below).
ActionDatabase::loadFromJson("actions.json");
// Re-process scene components so that any
// ActionDatabaseComponent entities in the scene override or
// append actions from the file.
ActionDatabase::reloadFromSceneComponents(m_world);
}
if (m_gameMode == GameMode::Game) {
// Queue "game_start" event after Lua scripts are loaded
// so Lua subscribers registered via ecs.subscribe_event()
// will receive the event.
EventBus::getInstance().send("game_start");
}
// Game mode can be set externally before setup() is called
m_setupComplete = true;
@@ -366,12 +558,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);
}
@@ -402,6 +601,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) {
@@ -436,9 +641,14 @@ void EditorApp::startNewGame(const Ogre::String &scenePath)
clearScene();
SceneSerializer serializer(m_world, m_sceneMgr);
if (serializer.loadFromFile(scenePath, m_uiSystem.get())) {
PrefabSystem prefabSys(m_world, m_sceneMgr);
prefabSys.resolveInstances();
setGamePlayState(GamePlayState::Playing);
Ogre::LogManager::getSingleton().logMessage(
"Game started: loaded scene " + scenePath);
// Send "scene_ready" event after scene is loaded and
// entities/components are populated and ready to run.
EventBus::getInstance().send("scene_ready");
} else {
Ogre::LogManager::getSingleton().logMessage(
"Failed to load scene: " + serializer.getLastError());
@@ -497,11 +707,51 @@ void EditorApp::setupECS()
// Register game components
m_world.component<StartupMenuComponent>();
m_world.component<DialogueComponent>();
m_world.component<PlayerControllerComponent>();
m_world.component<InWater>();
// Register environment components
m_world.component<SunComponent>();
m_world.component<SkyboxComponent>();
// Register AI/GOAP components
// ActionDatabase is now a singleton, registered in ActionDatabaseModule
m_world.component<ActionDebug>();
m_world.component<BehaviorTreeComponent>();
m_world.component<GoapBlackboard>();
// Register Smart Object component
m_world.component<SmartObjectComponent>();
// Register Actuator component
m_world.component<ActuatorComponent>();
// Register Event Handler component
m_world.component<EventHandlerComponent>();
// Register GOAP Planner component
m_world.component<GoapPlannerComponent>();
// Register GOAP Runner component
m_world.component<GoapRunnerComponent>();
// Register Path Following component
m_world.component<PathFollowingComponent>();
// Register Navigation components
m_world.component<NavMeshComponent>();
m_world.component<NavMeshGeometrySource>();
// Register CellGrid/Town components
CellGridModule::registerComponents(m_world);
// Register PrefabInstance component
m_world.component<PrefabInstanceComponent>();
// Register Item and Inventory components
m_world.component<ItemComponent>();
m_world.component<InventoryComponent>();
}
void EditorApp::createDefaultEntities()
@@ -648,6 +898,11 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
/* --- Animation / procedural generation --- */
if (m_animationTreeSystem) {
m_animationTreeSystem->update(evt.timeSinceLastFrame);
if (m_behaviorTreeSystem)
m_behaviorTreeSystem->update(evt.timeSinceLastFrame);
}
if (m_pathFollowingSystem) {
m_pathFollowingSystem->update(evt.timeSinceLastFrame);
}
if (m_proceduralMeshSystem) {
m_proceduralMeshSystem->update();
@@ -661,7 +916,43 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
m_cellGridSystem->update();
}
/* --- Normal debug visualization (after geometry is built) --- */
if (m_normalDebugSystem) {
m_normalDebugSystem->update();
}
/* --- NavMesh builds after static geometry is ready --- */
if (m_navMeshSystem) {
m_navMeshSystem->update(evt.timeSinceLastFrame);
}
/* --- Smart Object system (AI navigation to smart objects) --- */
if (m_smartObjectSystem) {
m_smartObjectSystem->update(evt.timeSinceLastFrame);
}
/* --- GOAP Planner system (plan generation) --- */
if (m_goapPlannerSystem) {
m_goapPlannerSystem->update(evt.timeSinceLastFrame);
}
/* --- GOAP Runner system (plan execution) --- */
if (m_goapRunnerSystem) {
m_goapRunnerSystem->update(evt.timeSinceLastFrame);
}
/* --- Actuator system (player interaction prompts) --- */
if (m_actuatorSystem) {
m_actuatorSystem->update(evt.timeSinceLastFrame);
}
/* --- Event Handler system (event-driven BTs) --- */
if (m_eventHandlerSystem) {
m_eventHandlerSystem->update(evt.timeSinceLastFrame);
}
/* --- Dynamic physics (characters after static world) --- */
if (m_characterSystem) {
m_characterSystem->update(evt.timeSinceLastFrame);
}
@@ -682,6 +973,25 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
}
/* --- Rendering support systems --- */
if (m_sunSystem) {
m_sunSystem->update(evt.timeSinceLastFrame);
}
if (m_skyboxSystem) {
Ogre::Camera *cam = nullptr;
if (m_sceneMgr->hasCamera("PlayerCamera"))
cam = m_sceneMgr->getCamera("PlayerCamera");
if (!cam && m_camera)
cam = m_camera->getCamera();
m_skyboxSystem->update(cam);
}
if (m_waterPlaneSystem) {
Ogre::Camera *cam = nullptr;
if (m_sceneMgr->hasCamera("PlayerCamera"))
cam = m_sceneMgr->getCamera("PlayerCamera");
if (!cam && m_camera)
cam = m_camera->getCamera();
m_waterPlaneSystem->update(evt.timeSinceLastFrame, cam);
}
if (m_lightSystem) {
m_lightSystem->update();
}

View File

@@ -9,6 +9,7 @@
#include <OgreRenderTargetListener.h>
#include <flecs.h>
#include <memory>
#include "lua/LuaState.hpp"
// Forward declarations
class EditorUISystem;
@@ -23,12 +24,26 @@ class ProceduralMaterialSystem;
class ProceduralMeshSystem;
class CharacterSlotSystem;
class AnimationTreeSystem;
class BehaviorTreeSystem;
class NavMeshSystem;
class CharacterSystem;
class CellGridSystem;
class RoomLayoutSystem;
class StartupMenuSystem;
class DialogueSystem;
class PlayerControllerSystem;
class BuoyancySystem;
class EditorSunSystem;
class EditorSkyboxSystem;
class EditorWaterPlaneSystem;
class NormalDebugSystem;
class SmartObjectSystem;
class GoapRunnerSystem;
class PathFollowingSystem;
class GoapPlannerSystem;
class ActuatorSystem;
class EventHandlerSystem;
class ItemSystem;
class EditorApp;
/**
@@ -175,6 +190,18 @@ public:
{
return m_startupMenuSystem.get();
}
DialogueSystem *getDialogueSystem() const
{
return m_dialogueSystem.get();
}
ActuatorSystem *getActuatorSystem() const
{
return m_actuatorSystem.get();
}
EventHandlerSystem *getEventHandlerSystem() const
{
return m_eventHandlerSystem.get();
}
Ogre::ImGuiOverlay *getImGuiOverlay() const
{
return m_imguiOverlay;
@@ -196,6 +223,9 @@ private:
std::unique_ptr<ImGuiRenderListener> m_imguiListener;
std::unique_ptr<EditorPhysicsSystem> m_physicsSystem;
std::unique_ptr<BuoyancySystem> m_buoyancySystem;
std::unique_ptr<EditorSunSystem> m_sunSystem;
std::unique_ptr<EditorSkyboxSystem> m_skyboxSystem;
std::unique_ptr<EditorWaterPlaneSystem> m_waterPlaneSystem;
std::unique_ptr<EditorLightSystem> m_lightSystem;
std::unique_ptr<EditorCameraSystem> m_cameraSystem;
std::unique_ptr<EditorLodSystem> m_lodSystem;
@@ -205,12 +235,23 @@ private:
std::unique_ptr<ProceduralMeshSystem> m_proceduralMeshSystem;
std::unique_ptr<CharacterSlotSystem> m_characterSlotSystem;
std::unique_ptr<AnimationTreeSystem> m_animationTreeSystem;
std::unique_ptr<BehaviorTreeSystem> m_behaviorTreeSystem;
std::unique_ptr<NavMeshSystem> m_navMeshSystem;
std::unique_ptr<CharacterSystem> m_characterSystem;
std::unique_ptr<CellGridSystem> m_cellGridSystem;
std::unique_ptr<NormalDebugSystem> m_normalDebugSystem;
std::unique_ptr<RoomLayoutSystem> m_roomLayoutSystem;
std::unique_ptr<SmartObjectSystem> m_smartObjectSystem;
std::unique_ptr<GoapRunnerSystem> m_goapRunnerSystem;
std::unique_ptr<PathFollowingSystem> m_pathFollowingSystem;
std::unique_ptr<GoapPlannerSystem> m_goapPlannerSystem;
std::unique_ptr<ActuatorSystem> m_actuatorSystem;
std::unique_ptr<EventHandlerSystem> m_eventHandlerSystem;
std::unique_ptr<ItemSystem> m_itemSystem;
// Game systems
std::unique_ptr<StartupMenuSystem> m_startupMenuSystem;
std::unique_ptr<DialogueSystem> m_dialogueSystem;
std::unique_ptr<PlayerControllerSystem> m_playerControllerSystem;
// State
@@ -221,6 +262,9 @@ private:
bool m_setupComplete = false;
bool m_debugBuoyancy = false;
// Lua scripting
editScene::LuaState m_lua;
// Editor visualization nodes
Ogre::SceneNode *m_gridNode = nullptr;
Ogre::SceneNode *m_axisNode = nullptr;

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

@@ -0,0 +1,515 @@
#include "ActionDatabase.hpp"
#ifndef OGRE_STUB_H
#include <OgreLogManager.h>
#include <OgreResourceGroupManager.h>
#endif
#include <flecs.h>
#include <nlohmann/json.hpp>
#include <fstream>
#include <filesystem>
// ---------------------------------------------------------------------------
// Singleton
// ---------------------------------------------------------------------------
ActionDatabase &ActionDatabase::getSingleton()
{
static ActionDatabase instance;
return instance;
}
ActionDatabase *ActionDatabase::getSingletonPtr()
{
return &getSingleton();
}
// ---------------------------------------------------------------------------
// Find methods
// ---------------------------------------------------------------------------
const GoapAction *ActionDatabase::findAction(const Ogre::String &name) const
{
for (const auto &action : actions) {
if (action.name == name)
return &action;
}
return nullptr;
}
GoapAction *ActionDatabase::findAction(const Ogre::String &name)
{
for (auto &action : actions) {
if (action.name == name)
return &action;
}
return nullptr;
}
const GoapGoal *ActionDatabase::findGoal(const Ogre::String &name) const
{
for (const auto &goal : goals) {
if (goal.name == name)
return &goal;
}
return nullptr;
}
GoapGoal *ActionDatabase::findGoal(const Ogre::String &name)
{
for (auto &goal : goals) {
if (goal.name == name)
return &goal;
}
return nullptr;
}
// ---------------------------------------------------------------------------
// Add or replace
// ---------------------------------------------------------------------------
void ActionDatabase::addOrReplaceAction(const GoapAction &action)
{
for (auto &a : actions) {
if (a.name == action.name) {
a = action;
return;
}
}
actions.push_back(action);
}
void ActionDatabase::addOrReplaceGoal(const GoapGoal &goal)
{
for (auto &g : goals) {
if (g.name == goal.name) {
g = goal;
return;
}
}
goals.push_back(goal);
}
// ---------------------------------------------------------------------------
// Remove methods
// ---------------------------------------------------------------------------
bool ActionDatabase::removeAction(const Ogre::String &name)
{
for (auto it = actions.begin(); it != actions.end(); ++it) {
if (it->name == name) {
actions.erase(it);
return true;
}
}
return false;
}
bool ActionDatabase::removeGoal(const Ogre::String &name)
{
for (auto it = goals.begin(); it != goals.end(); ++it) {
if (it->name == name) {
goals.erase(it);
return true;
}
}
return false;
}
// ---------------------------------------------------------------------------
// Selection / validation
// ---------------------------------------------------------------------------
const GoapGoal *
ActionDatabase::selectBestGoal(const GoapBlackboard &blackboard) const
{
const GoapGoal *best = nullptr;
int bestPriority = -1;
for (const auto &goal : goals) {
if (!goal.isValid(blackboard))
continue;
if (goal.priority > bestPriority) {
bestPriority = goal.priority;
best = &goal;
}
}
return best;
}
std::vector<const GoapAction *>
ActionDatabase::getValidActions(const GoapBlackboard &blackboard) const
{
std::vector<const GoapAction *> result;
for (const auto &action : actions) {
if (action.canRun(blackboard))
result.push_back(&action);
}
return result;
}
// ---------------------------------------------------------------------------
// Clear
// ---------------------------------------------------------------------------
void ActionDatabase::clear()
{
actions.clear();
goals.clear();
}
// ---------------------------------------------------------------------------
// ActionDatabaseComponent
// ---------------------------------------------------------------------------
void ActionDatabaseComponent::syncToSingleton() const
{
auto &db = ActionDatabase::getSingleton();
db.clear();
for (const auto &action : actions)
db.addOrReplaceAction(action);
for (const auto &goal : goals)
db.addOrReplaceGoal(goal);
}
// ---------------------------------------------------------------------------
// JSON serialization helpers (local to this translation unit)
// ---------------------------------------------------------------------------
static nlohmann::json serializeGoapBlackboard(const GoapBlackboard &bb)
{
nlohmann::json json;
json["bits"] = (uint64_t)bb.bits;
json["mask"] = (uint64_t)bb.mask;
if (bb.bitmask != ~0ULL)
json["bitmask"] = (uint64_t)bb.bitmask;
if (!bb.values.empty()) {
json["values"] = nlohmann::json::object();
for (const auto &pair : bb.values)
json["values"][pair.first] = pair.second;
}
if (!bb.floatValues.empty()) {
json["floatValues"] = nlohmann::json::object();
for (const auto &pair : bb.floatValues)
json["floatValues"][pair.first] = pair.second;
}
if (!bb.vec3Values.empty()) {
json["vec3Values"] = nlohmann::json::object();
for (const auto &pair : bb.vec3Values) {
nlohmann::json v;
v.push_back(pair.second.x);
v.push_back(pair.second.y);
v.push_back(pair.second.z);
json["vec3Values"][pair.first] = v;
}
}
return json;
}
static void deserializeGoapBlackboard(GoapBlackboard &bb,
const nlohmann::json &json)
{
bb.bits = json.value("bits", (uint64_t)0);
bb.mask = json.value("mask", (uint64_t)0);
bb.bitmask = json.value("bitmask", ~0ULL);
bb.values.clear();
if (json.contains("values") && json["values"].is_object()) {
for (auto &[key, val] : json["values"].items())
bb.values[key] = val.get<int>();
}
bb.floatValues.clear();
if (json.contains("floatValues") && json["floatValues"].is_object()) {
for (auto &[key, val] : json["floatValues"].items())
bb.floatValues[key] = val.get<float>();
}
bb.vec3Values.clear();
if (json.contains("vec3Values") && json["vec3Values"].is_object()) {
for (auto &[key, val] : json["vec3Values"].items()) {
if (val.is_array() && val.size() >= 3)
bb.vec3Values[key] =
Ogre::Vector3(val[0].get<float>(),
val[1].get<float>(),
val[2].get<float>());
}
}
}
static nlohmann::json serializeBehaviorTreeNode(const BehaviorTreeNode &node)
{
nlohmann::json json;
json["type"] = node.type;
if (!node.name.empty())
json["name"] = node.name;
if (!node.params.empty())
json["params"] = node.params;
if (!node.children.empty()) {
json["children"] = nlohmann::json::array();
for (const auto &child : node.children)
json["children"].push_back(
serializeBehaviorTreeNode(child));
}
return json;
}
static void deserializeBehaviorTreeNode(BehaviorTreeNode &node,
const nlohmann::json &json)
{
node.type = json.value("type", "task");
node.name = json.value("name", "");
node.params = json.value("params", "");
node.children.clear();
if (json.contains("children") && json["children"].is_array()) {
for (const auto &childJson : json["children"]) {
BehaviorTreeNode child;
deserializeBehaviorTreeNode(child, childJson);
node.children.push_back(child);
}
}
}
static nlohmann::json serializeGoapAction(const GoapAction &action)
{
nlohmann::json json;
json["name"] = action.name;
json["cost"] = action.cost;
json["preconditions"] = serializeGoapBlackboard(action.preconditions);
json["effects"] = serializeGoapBlackboard(action.effects);
if (action.preconditionMask != ~0ULL)
json["preconditionMask"] = action.preconditionMask;
json["behaviorTree"] = serializeBehaviorTreeNode(action.behaviorTree);
if (!action.behaviorTreeName.empty())
json["behaviorTreeName"] = action.behaviorTreeName;
return json;
}
static void deserializeGoapAction(GoapAction &action,
const nlohmann::json &json)
{
action.name = json.value("name", "Unnamed");
action.cost = json.value("cost", 1);
if (json.contains("preconditions"))
deserializeGoapBlackboard(action.preconditions,
json["preconditions"]);
if (json.contains("effects"))
deserializeGoapBlackboard(action.effects, json["effects"]);
action.preconditionMask = json.value("preconditionMask", ~0ULL);
if (json.contains("behaviorTree"))
deserializeBehaviorTreeNode(action.behaviorTree,
json["behaviorTree"]);
action.behaviorTreeName = json.value("behaviorTreeName", "");
}
static nlohmann::json serializeGoapGoal(const GoapGoal &goal)
{
nlohmann::json json;
json["name"] = goal.name;
json["priority"] = goal.priority;
json["target"] = serializeGoapBlackboard(goal.target);
if (!goal.condition.empty())
json["condition"] = goal.condition;
return json;
}
static void deserializeGoapGoal(GoapGoal &goal, const nlohmann::json &json)
{
goal.name = json.value("name", "Unnamed");
goal.priority = json.value("priority", 1);
if (json.contains("target"))
deserializeGoapBlackboard(goal.target, json["target"]);
goal.condition = json.value("condition", "");
}
// ---------------------------------------------------------------------------
// saveToJson / loadFromJson
// ---------------------------------------------------------------------------
bool ActionDatabase::saveToJson(const std::string &filename)
{
try {
// Resolve the filesystem path from the "General" resource group
Ogre::ResourceGroupManager &rgm =
Ogre::ResourceGroupManager::getSingleton();
const Ogre::ResourceGroupManager::LocationList &locations =
rgm.getResourceLocationList("General");
if (locations.empty()) {
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase::saveToJson: "
"no resource locations for group 'General'");
return false;
}
// Use the first location's path
std::string dir = locations.begin()->archive->getName();
std::string filepath = dir + "/" + filename;
// Backup existing file
if (std::filesystem::exists(filepath)) {
std::string backup = filepath + ".bak";
try {
std::filesystem::copy_file(
filepath, backup,
std::filesystem::copy_options::
overwrite_existing);
} catch (const std::exception &e) {
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase::saveToJson: "
"backup failed: " +
Ogre::String(e.what()));
}
}
const ActionDatabase &db = getSingleton();
nlohmann::json root;
root["actions"] = nlohmann::json::array();
for (const auto &action : db.actions)
root["actions"].push_back(serializeGoapAction(action));
root["goals"] = nlohmann::json::array();
for (const auto &goal : db.goals)
root["goals"].push_back(serializeGoapGoal(goal));
// Save bit names
nlohmann::json bitNames = nlohmann::json::array();
for (int i = 0; i < 64; i++) {
const char *name = GoapBlackboard::getBitName(i);
if (name) {
nlohmann::json entry;
entry["index"] = i;
entry["name"] = name;
bitNames.push_back(entry);
}
}
if (!bitNames.empty())
root["bitNames"] = bitNames;
std::ofstream file(filepath);
if (!file.is_open()) {
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase::saveToJson: "
"failed to open " +
filepath);
return false;
}
file << root.dump(4);
file.close();
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase saved to " + filepath);
return true;
} catch (const std::exception &e) {
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase::saveToJson error: " +
Ogre::String(e.what()));
return false;
}
}
bool ActionDatabase::loadFromJson(const std::string &filename)
{
try {
// Resolve the filesystem path from the "General" resource group
Ogre::ResourceGroupManager &rgm =
Ogre::ResourceGroupManager::getSingleton();
const Ogre::ResourceGroupManager::LocationList &locations =
rgm.getResourceLocationList("General");
if (locations.empty()) {
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase::loadFromJson: "
"no resource locations for group 'General'");
return false;
}
std::string dir = locations.begin()->archive->getName();
std::string filepath = dir + "/" + filename;
if (!std::filesystem::exists(filepath)) {
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase::loadFromJson: "
"file not found: " +
filepath);
return false;
}
std::ifstream file(filepath);
if (!file.is_open()) {
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase::loadFromJson: "
"failed to open " +
filepath);
return false;
}
nlohmann::json root;
try {
file >> root;
} catch (const nlohmann::json::parse_error &e) {
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase::loadFromJson: "
"JSON parse error in " +
filepath + ": " + Ogre::String(e.what()));
return false;
}
file.close();
ActionDatabase &db = getSingleton();
// Load actions (add/replace)
if (root.contains("actions") && root["actions"].is_array()) {
for (const auto &actionJson : root["actions"]) {
GoapAction action;
deserializeGoapAction(action, actionJson);
db.addOrReplaceAction(action);
}
}
// Load goals (add/replace)
if (root.contains("goals") && root["goals"].is_array()) {
for (const auto &goalJson : root["goals"]) {
GoapGoal goal;
deserializeGoapGoal(goal, goalJson);
db.addOrReplaceGoal(goal);
}
}
// Load bit names
if (root.contains("bitNames") && root["bitNames"].is_array()) {
for (const auto &entry : root["bitNames"]) {
if (entry.contains("index") &&
entry.contains("name"))
GoapBlackboard::setBitName(
entry["index"].get<int>(),
entry["name"]
.get<std::string>());
}
}
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase loaded from " + filepath);
return true;
} catch (const std::exception &e) {
Ogre::LogManager::getSingleton().logMessage(
"ActionDatabase::loadFromJson error: " +
Ogre::String(e.what()));
return false;
}
}
// ---------------------------------------------------------------------------
// reloadFromSceneComponents
// ---------------------------------------------------------------------------
void ActionDatabase::reloadFromSceneComponents(flecs::world &world)
{
// First, load from file (if available) — this is done by the caller
// before calling this function. Here we just re-sync from scene
// entities so that scene-defined actions are applied on top.
// Iterate all entities with ActionDatabaseComponent
world.each([](flecs::entity e, ActionDatabaseComponent &dbComp) {
(void)e;
dbComp.syncToSingleton();
});
}

View File

@@ -0,0 +1,114 @@
#ifndef EDITSCENE_ACTION_DATABASE_HPP
#define EDITSCENE_ACTION_DATABASE_HPP
#pragma once
#include "GoapAction.hpp"
#include "GoapGoal.hpp"
#include <vector>
#include <unordered_map>
#include <string>
// Forward declaration for reloadFromSceneComponents
namespace flecs
{
class world;
}
/**
* Global action database singleton.
*
* Holds the master list of GOAP actions and goals that characters can use.
* This is a singleton accessible from anywhere in the codebase.
* The ActionDatabaseComponent on a scene entity stores the actions/goals
* and syncs them to the singleton on scene load.
*/
class ActionDatabase {
public:
/** Get the singleton instance */
static ActionDatabase &getSingleton();
static ActionDatabase *getSingletonPtr();
std::vector<GoapAction> actions;
std::vector<GoapGoal> goals;
// Find an action by name
const GoapAction *findAction(const Ogre::String &name) const;
GoapAction *findAction(const Ogre::String &name);
// Find a goal by name
const GoapGoal *findGoal(const Ogre::String &name) const;
GoapGoal *findGoal(const Ogre::String &name);
// Add or replace an action by name
void addOrReplaceAction(const GoapAction &action);
// Add or replace a goal by name
void addOrReplaceGoal(const GoapGoal &goal);
// Remove an action by name
bool removeAction(const Ogre::String &name);
// Remove a goal by name
bool removeGoal(const Ogre::String &name);
// Select the best valid goal for a given blackboard
// Returns nullptr if no valid goal exists
const GoapGoal *selectBestGoal(const GoapBlackboard &blackboard) const;
// Build a list of actions that can run from a given blackboard state
std::vector<const GoapAction *>
getValidActions(const GoapBlackboard &blackboard) const;
// Clear all actions and goals
void clear();
/**
* Save the action database to a JSON file.
* Creates a backup of the existing file (if any) by appending ".bak".
* The file is written to the filesystem path resolved from the
* "General" resource group.
*
* @param filename The filename (e.g. "actions.json").
* @return true on success.
*/
static bool saveToJson(const std::string &filename);
/**
* Load the action database from a JSON file.
* The file is located via the "General" resource group.
* On failure (file not found, parse error) the error is logged
* and the database is left unchanged.
*
* @param filename The filename (e.g. "actions.json").
* @return true on success.
*/
static bool loadFromJson(const std::string &filename);
/**
* Re-process all ActionDatabaseComponent entities in the given
* Flecs world: clear the singleton and re-sync from every entity
* that carries the component. This is used after a reload so
* scene-defined actions are re-applied on top of the file.
*/
static void reloadFromSceneComponents(flecs::world &world);
private:
ActionDatabase() = default;
~ActionDatabase() = default;
ActionDatabase(const ActionDatabase &) = delete;
ActionDatabase &operator=(const ActionDatabase &) = delete;
};
/**
* Flecs component that stores action database data on a scene entity.
* When set on an entity, it syncs its contents to the ActionDatabase singleton.
*/
struct ActionDatabaseComponent {
std::vector<GoapAction> actions;
std::vector<GoapGoal> goals;
/** Sync this component's data to the ActionDatabase singleton */
void syncToSingleton() const;
};
#endif // EDITSCENE_ACTION_DATABASE_HPP

View File

@@ -0,0 +1,47 @@
#include "ActionDatabase.hpp"
#include "ActionDebug.hpp"
#include "BehaviorTree.hpp"
#include "GoapAction.hpp"
#include "GoapGoal.hpp"
#include "GoapBlackboard.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/ActionDatabaseEditor.hpp"
#include "../ui/BehaviorTreeEditor.hpp"
REGISTER_COMPONENT_GROUP("Action Database", "AI", ActionDatabaseComponent,
ActionDatabaseEditor)
{
registry.registerComponent<ActionDatabaseComponent>(
"Action Database", "AI",
std::make_unique<ActionDatabaseEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<ActionDatabaseComponent>())
e.set<ActionDatabaseComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<ActionDatabaseComponent>())
e.remove<ActionDatabaseComponent>();
});
}
REGISTER_COMPONENT_GROUP("Behavior Tree", "AI", BehaviorTreeComponent,
BehaviorTreeEditor)
{
registry.registerComponent<BehaviorTreeComponent>(
"Behavior Tree", "AI", std::make_unique<BehaviorTreeEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<BehaviorTreeComponent>()) {
BehaviorTreeComponent bt;
bt.root.type = "sequence";
e.set<BehaviorTreeComponent>(bt);
}
},
// Remover
[](flecs::entity e) {
if (e.has<BehaviorTreeComponent>())
e.remove<BehaviorTreeComponent>();
});
}

View File

@@ -0,0 +1,39 @@
#ifndef EDITSCENE_ACTION_DEBUG_HPP
#define EDITSCENE_ACTION_DEBUG_HPP
#pragma once
#include "GoapBlackboard.hpp"
#include <Ogre.h>
#include <vector>
#include <unordered_map>
/**
* Per-character action debug component.
*
* Allows test-running individual actions and inspecting the character's
* local blackboard state. Used for debugging AI behavior in the editor.
*
* Path following animation states have been moved to PathFollowingComponent.
*/
struct ActionDebug {
// Character's local GOAP blackboard
GoapBlackboard blackboard;
// Currently selected action for test-running
Ogre::String selectedActionName;
// Currently selected goal for testing
Ogre::String selectedGoalName;
// Test-run state
bool isRunning = false;
float runTimer = 0.0f;
Ogre::String currentActionName;
// Debug output
Ogre::String lastResult;
ActionDebug() = default;
};
#endif // EDITSCENE_ACTION_DEBUG_HPP

View File

@@ -0,0 +1,21 @@
#include "ActionDebug.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/ActionDebugEditor.hpp"
REGISTER_COMPONENT_GROUP("Action Debug", "AI", ActionDebug,
ActionDebugEditor)
{
registry.registerComponent<ActionDebug>(
"Action Debug", "AI",
std::make_unique<ActionDebugEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<ActionDebug>())
e.set<ActionDebug>({});
},
// Remover
[](flecs::entity e) {
if (e.has<ActionDebug>())
e.remove<ActionDebug>();
});
}

View File

@@ -0,0 +1,44 @@
#ifndef EDITSCENE_ACTUATOR_HPP
#define EDITSCENE_ACTUATOR_HPP
#pragma once
#include <Ogre.h>
#include <vector>
#include <string>
/**
* Actuator component.
*
* An interactive object visible only to the player character.
* When the player is within radius + height range, an on-screen
* prompt appears and the action can be triggered with the action key.
*
* Unlike SmartObject, Actuators do not use pathfinding or path
* following — they are instantaneous interactions.
*/
struct ActuatorComponent {
// Interaction radius in XZ plane
float radius = 1.5f;
// Maximum height difference for interaction
float height = 1.8f;
// Names of GOAP actions (from ActionDatabase) that this actuator provides
std::vector<Ogre::String> actionNames;
// Runtime: cooldown timer (seconds remaining)
float cooldownTimer = 0.0f;
// Runtime: currently executing an action
bool isExecuting = false;
ActuatorComponent() = default;
explicit ActuatorComponent(float radius_, float height_)
: radius(radius_)
, height(height_)
{
}
};
#endif // EDITSCENE_ACTUATOR_HPP

View File

@@ -0,0 +1,19 @@
#include "Actuator.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/ActuatorEditor.hpp"
REGISTER_COMPONENT_GROUP("Actuator", "Game", ActuatorComponent, ActuatorEditor)
{
registry.registerComponent<ActuatorComponent>(
"Actuator", "Game", std::make_unique<ActuatorEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<ActuatorComponent>())
e.set<ActuatorComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<ActuatorComponent>())
e.remove<ActuatorComponent>();
});
}

View File

@@ -0,0 +1,130 @@
#ifndef EDITSCENE_BEHAVIOR_TREE_HPP
#define EDITSCENE_BEHAVIOR_TREE_HPP
#pragma once
#include <Ogre.h>
#include <vector>
/**
* Data-driven behavior tree node for AI action execution.
*
* Node types:
* "sequence" - Execute children in order until one fails
* "selector" - Execute children in order until one succeeds
* "invert" - Invert the result of a single child
* "task" - Leaf action (references a named task)
* "check" - Leaf condition (references a named check)
* "debugPrint" - Leaf: prints 'name' to console once when active
* "setAnimationState"- Leaf: sets animation state (name="SM/State")
* "isAnimationEnded" - Leaf check: true if anim in state machine ended
* "setBit" - Leaf: sets blackboard bit (name=bit, params=0/1)
* "checkBit" - Leaf check: true if blackboard bit is set
* "setValue" - Leaf: sets blackboard value (name=key, params=val)
* "checkValue" - Leaf check: blackboard comparison (name=key, params="op val")
* "blackboardDump" - Leaf: dumps entire blackboard to log
* "delay" - Leaf: waits for N seconds (params=seconds as float)
* "teleportToChild" - Leaf: teleports character to a named child entity
* of the Smart Object being interacted with.
* name = child entity name to teleport to.
* The character is positioned at the child's absolute
* world transform (position + orientation).
* "disablePhysics" - Leaf: removes character's JPH::BodyID from physics
* system so physics no longer interferes with animation.
* "enablePhysics" - Leaf: re-adds character's JPH::BodyID to physics
* system to restore physics simulation.
*
* --- Item / Inventory nodes ---
* "hasItem" - Leaf check: true if character's inventory has an item
* matching the given itemId (name=itemId).
* "hasItemByName" - Leaf check: true if character's inventory has an item
* matching the given itemName (name=itemName).
* "countItem" - Leaf check: true if character's inventory has at least
* N of itemId (name=itemId, params=count as int).
* "pickupItem" - Leaf: picks up the nearest ItemComponent entity within
* range into the character's inventory.
* name=itemId filter (optional, empty = any).
* "dropItem" - Leaf: drops an item from inventory into the world.
* name=itemId, params=count (optional, default 1).
* "useItem" - Leaf: uses an item from inventory (executes its
* useAction behavior tree). name=itemId.
* "addItemToInventory"- Leaf: adds an item directly to character's inventory
* (for quest rewards, etc.).
* params="itemId,itemName,itemType,count,weight,value"
*
* --- Lua node ---
* "luaTask" - Leaf: calls a registered Lua function.
* name = registered node handler name.
* params = "key=val,key2=val2" passed to the Lua function.
* The Lua function receives (entity_id, params_table)
* and must return "success", "failure", or "running".
* Register handlers via:
* ecs.behavior_tree.register_node("name", function)
*/
struct BehaviorTreeNode {
Ogre::String type = "task";
Ogre::String name; // Action/condition name, or message, or SM/State
Ogre::String params; // Optional extra parameters
std::vector<BehaviorTreeNode> children;
BehaviorTreeNode() = default;
BehaviorTreeNode *findChild(const Ogre::String &childName)
{
for (auto &child : children) {
if (child.name == childName)
return &child;
}
return nullptr;
}
const BehaviorTreeNode *findChild(const Ogre::String &childName) const
{
for (const auto &child : children) {
if (child.name == childName)
return &child;
}
return nullptr;
}
bool canHaveChildren() const
{
return type == "sequence" || type == "selector" ||
type == "invert";
}
bool isLeaf() const
{
return type == "task" || type == "check" ||
type == "debugPrint" || type == "setAnimationState" ||
type == "isAnimationEnded" || type == "setBit" ||
type == "checkBit" || type == "setValue" ||
type == "checkValue" || type == "blackboardDump" ||
type == "delay" || type == "teleportToChild" ||
type == "disablePhysics" || type == "enablePhysics" ||
type == "sendEvent" || type == "hasItem" ||
type == "hasItemByName" || type == "countItem" ||
type == "pickupItem" || type == "dropItem" ||
type == "useItem" || type == "addItemToInventory" ||
type == "luaTask";
}
};
/**
* Behavior tree asset component.
*
* Can be attached to an entity to define a reusable behavior tree,
* or referenced by name from a GoapAction.
*/
struct BehaviorTreeComponent {
BehaviorTreeNode root;
Ogre::String treeName;
bool enabled = true;
bool dirty = true;
void markDirty()
{
dirty = true;
}
};
#endif // EDITSCENE_BEHAVIOR_TREE_HPP

View File

@@ -27,16 +27,35 @@ struct CharacterComponent {
/* Enable/disable physics character */
bool enabled = true;
/* Physics was explicitly disabled (e.g. by behavior tree node).
* When true, the character's JPH::BodyID is removed from the physics
* system but the JPH::Character object is kept alive so it can be
* re-added later. This is separate from 'enabled' which controls
* whether the character system processes this entity at all. */
bool physicsDisabled = false;
/* Dirty flag — triggers rebuild of the Jolt character */
bool dirty = true;
/* When true, the scene node position is driven by root motion
* (AnimationTreeSystem), not by physics. The physics character
* position is synced to match the scene node each frame, and
* physics does NOT write its position back to the scene node. */
bool useRootMotion = false;
/* Floor detection: raycast downward to find ground before enabling gravity */
bool hasFloor = false;
float floorCheckDistance = 2.0f;
bool useGravity = true;
float getHalfHeight() const { return height * 0.5f; }
float getTotalHeight() const { return height + 2.0f * radius; }
float getHalfHeight() const
{
return height * 0.5f;
}
float getTotalHeight() const
{
return height + 2.0f * radius;
}
};
#endif // EDITSCENE_CHARACTER_HPP

View File

@@ -17,6 +17,13 @@ struct CharacterSlotsComponent {
/* Runtime: master entity with shared skeleton (set by CharacterSlotSystem) */
Ogre::Entity *masterEntity = nullptr;
/**
* Front-facing axis for this character model.
* Most models face -Z (NEGATIVE_UNIT_Z), but some face +Z.
* This is used by path following to rotate the character correctly.
*/
Ogre::Vector3 frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z;
CharacterSlotsComponent() = default;
};

View File

@@ -0,0 +1,131 @@
#ifndef EDITSCENE_DIALOGUE_COMPONENT_HPP
#define EDITSCENE_DIALOGUE_COMPONENT_HPP
#pragma once
#include <Ogre.h>
#include <functional>
#include <string>
#include <vector>
/**
* Visual-novel style dialogue box component.
*
* Displays a narration text box at the bottom of the screen with optional
* player choices. The dialogue can be driven via the EventBus system
* (using "dialogue_show" event) or directly via the component API.
*
* Only active in game mode (GamePlayState::Playing).
*
* Event payload (EventParams) parameters:
* "text" (string) - Narration text to display
* "choices" (string_array) - Array of choice label strings (Lua table)
* "speaker" (string) - Optional speaker name
* "auto_progress" (int) - If 1, clicking anywhere progresses (no choices)
*
* Component state transitions:
* Idle -> Showing (on show() or event)
* Showing -> AwaitingChoice (if choices provided)
* Showing -> Idle (if no choices, on click progress)
* AwaitingChoice -> Idle (on choice selected)
*/
struct DialogueComponent {
/** Current state of the dialogue box */
enum class State {
Idle, ///< No dialogue active
Showing, ///< Text is being displayed
AwaitingChoice ///< Waiting for player to pick a choice
};
State state = State::Idle;
/** The narration text to display */
Ogre::String text;
/** Optional speaker name (displayed above the text) */
Ogre::String speaker;
/** Player choice labels (empty = no choices, click to progress) */
std::vector<Ogre::String> choices;
/** Font configuration */
Ogre::String fontName = "Jupiteroid-Regular.ttf";
float fontSize = 24.0f;
/** Speaker name font size (slightly smaller) */
float speakerFontSize = 20.0f;
/** Background opacity (0.0 - 1.0) */
float backgroundOpacity = 0.85f;
/** Height of the dialogue box as fraction of screen height (0.0 - 1.0) */
float boxHeightFraction = 0.25f;
/** Vertical position as fraction from top (0.0 = top, 0.75 = bottom quarter) */
float boxPositionFraction = 0.75f;
/** Whether the dialogue box is enabled (can be toggled) */
bool enabled = true;
/** Callback invoked when a choice is selected (choice index, 1-based) */
std::function<void(int)> onChoiceSelected;
/** Callback invoked when dialogue is dismissed (no choices mode) */
std::function<void()> onDismissed;
/** Callback invoked when dialogue starts showing */
std::function<void()> onShow;
/* --- API --- */
/** Show dialogue with given text and optional choices */
void show(const Ogre::String &narrationText,
const std::vector<Ogre::String> &choiceLabels = {},
const Ogre::String &speakerName = "")
{
text = narrationText;
choices = choiceLabels;
speaker = speakerName;
state = choices.empty() ? State::Showing :
State::AwaitingChoice;
if (onShow)
onShow();
}
/** Progress the dialogue (click-through when no choices) */
void progress()
{
if (state == State::Showing && choices.empty()) {
state = State::Idle;
if (onDismissed)
onDismissed();
}
}
/** Select a choice by 1-based index */
void selectChoice(int index)
{
if (state == State::AwaitingChoice && index >= 1 &&
index <= (int)choices.size()) {
state = State::Idle;
if (onChoiceSelected)
onChoiceSelected(index);
}
}
/** Check if dialogue is currently active */
bool isActive() const
{
return state != State::Idle;
}
/** Reset dialogue to idle state */
void reset()
{
state = State::Idle;
text.clear();
choices.clear();
speaker.clear();
}
};
#endif // EDITSCENE_DIALOGUE_COMPONENT_HPP

View File

@@ -0,0 +1,23 @@
#include "../ui/ComponentRegistration.hpp"
#include "../ui/DialogueEditor.hpp"
#include "DialogueComponent.hpp"
REGISTER_COMPONENT_GROUP("Dialogue Box", "Game", DialogueComponent,
DialogueEditor)
{
registry.registerComponent<DialogueComponent>(
DialogueComponent_name, DialogueComponent_group,
std::make_unique<DialogueEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<DialogueComponent>()) {
e.set<DialogueComponent>({});
}
},
// Remover
[](flecs::entity e) {
if (e.has<DialogueComponent>()) {
e.remove<DialogueComponent>();
}
});
}

View File

@@ -0,0 +1,21 @@
#ifndef EDITSCENE_EVENT_HANDLER_HPP
#define EDITSCENE_EVENT_HANDLER_HPP
#pragma once
#include <Ogre.h>
/**
* 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
* (EventParams) are injected into the entity's GoapBlackboard before
* the tree runs and cleaned up when the tree completes.
*/
struct EventHandlerComponent {
Ogre::String eventName;
Ogre::String actionName;
bool enabled = true;
};
#endif // EDITSCENE_EVENT_HANDLER_HPP

View File

@@ -0,0 +1,21 @@
#include "EventHandler.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/EventHandlerEditor.hpp"
REGISTER_COMPONENT_GROUP("Event Handler", "Game", EventHandlerComponent,
EventHandlerEditor)
{
registry.registerComponent<EventHandlerComponent>(
"Event Handler", "Game",
std::make_unique<EventHandlerEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<EventHandlerComponent>())
e.set<EventHandlerComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<EventHandlerComponent>())
e.remove<EventHandlerComponent>();
});
}

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,74 @@
#ifndef EDITSCENE_GOAP_ACTION_HPP
#define EDITSCENE_GOAP_ACTION_HPP
#pragma once
#include "GoapBlackboard.hpp"
#include "BehaviorTree.hpp"
#include <Ogre.h>
/**
* A GOAP action definition.
*
* Actions live in the ActionDatabase and can be executed by any character.
* Each action has preconditions (required blackboard state),
* effects (resulting blackboard state), a cost, and a behavior tree.
*/
struct GoapAction {
Ogre::String name;
int cost = 1;
// GOAP preconditions and effects
GoapBlackboard preconditions;
GoapBlackboard effects;
// Bitmask for precondition checking. Only bits set here are compared.
// Defaults to all 1s (check all bits).
uint64_t preconditionMask = ~0ULL;
// Behavior tree to execute when this action is selected
BehaviorTreeNode behaviorTree;
// Optional: reference to a named behavior tree asset
Ogre::String behaviorTreeName;
GoapAction() = default;
explicit GoapAction(const Ogre::String &name_, int cost_ = 1)
: name(name_)
, cost(cost_)
{
}
// Check if the given blackboard satisfies this action's preconditions.
// Only bits in preconditionMask are compared.
bool canRun(const GoapBlackboard &blackboard) const
{
// Fast-path: if mask covers all set bits, use standard check
if (preconditionMask == ~0ULL)
return blackboard.satisfies(preconditions);
// Masked check: only compare bits in preconditionMask
uint64_t relevantBits = preconditionMask;
uint64_t bbBits = blackboard.bits & relevantBits;
uint64_t preBits = preconditions.bits & relevantBits;
uint64_t bbMask = blackboard.mask & relevantBits;
uint64_t preMask = preconditions.mask & relevantBits;
// All precondition bits must be present in blackboard
if ((bbMask & preMask) != preMask)
return false;
if ((bbBits & preMask) != preBits)
return false;
// Check integer values
for (const auto &kv : preconditions.values) {
if (!blackboard.hasValue(kv.first))
return false;
if (blackboard.getValue(kv.first) != kv.second)
return false;
}
return true;
}
};
#endif // EDITSCENE_GOAP_ACTION_HPP

View File

@@ -0,0 +1,227 @@
#include "GoapBlackboard.hpp"
#include <cstdlib>
std::array<std::string, 64> &GoapBlackboard::getBitNameRegistry()
{
static std::array<std::string, 64> registry;
static bool initialized = false;
if (!initialized) {
for (auto &s : registry)
s.clear();
initialized = true;
}
return registry;
}
void GoapBlackboard::setBitName(int index, const std::string &name)
{
if (index < 0 || index >= 64)
return;
getBitNameRegistry()[index] = name;
}
const char *GoapBlackboard::getBitName(int index)
{
if (index < 0 || index >= 64)
return nullptr;
const auto &name = getBitNameRegistry()[index];
return name.empty() ? nullptr : name.c_str();
}
int GoapBlackboard::findBitByName(const std::string &name)
{
if (name.empty())
return -1;
const auto &registry = getBitNameRegistry();
for (int i = 0; i < 64; i++) {
if (registry[i] == name)
return i;
}
return -1;
}
bool GoapBlackboard::satisfies(const GoapBlackboard &other) const
{
// Only compare bits that both sides consider relevant
uint64_t relevantMask = bitmask & other.bitmask;
// Check bits: for every bit set in other's mask (within relevant mask),
// our bit must match
uint64_t commonMask = mask & other.mask & relevantMask;
if ((bits & commonMask) != (other.bits & commonMask))
return false;
// Also check bits that other has set but we don't (within relevant mask)
uint64_t missingMask = other.mask & ~mask & relevantMask;
if (missingMask) {
// Other requires bits we don't have set -> fail
// But only if other has those bits set to 1
if ((other.bits & missingMask) != 0)
return false;
}
// Check values: for every key in other, our value must match
for (const auto &pair : other.values) {
auto it = values.find(pair.first);
if (it == values.end()) {
// If we don't have the key, only satisfy if target is 0
if (pair.second != 0)
return false;
} else if (it->second != pair.second) {
return false;
}
}
return true;
}
void GoapBlackboard::apply(const GoapBlackboard &other)
{
// Apply bit effects
bits = (bits & ~other.mask) | (other.bits & other.mask);
mask |= other.mask;
// Apply value effects
for (const auto &pair : other.values)
values[pair.first] = pair.second;
}
bool GoapBlackboard::getScalarValue(const std::string &key,
float &out) const
{
auto itf = floatValues.find(key);
if (itf != floatValues.end()) {
out = itf->second;
return true;
}
auto iti = values.find(key);
if (iti != values.end()) {
out = static_cast<float>(iti->second);
return true;
}
return false;
}
int GoapBlackboard::distanceTo(const GoapBlackboard &target,
bool ignoreValues) const
{
int distance = 0;
// Only compare bits that both sides consider relevant
uint64_t relevantMask = bitmask & target.bitmask;
// Bit differences (within relevant mask)
uint64_t commonMask = mask & target.mask & relevantMask;
distance += __builtin_popcountll((bits ^ target.bits) & commonMask);
// Bits target cares about but we don't have (within relevant mask)
uint64_t missingInUs = target.mask & ~mask & relevantMask;
distance += __builtin_popcountll(target.bits & missingInUs);
// Bits we care about but target doesn't (within relevant mask)
uint64_t missingInTarget = mask & ~target.mask & relevantMask;
distance += __builtin_popcountll(bits & missingInTarget);
if (ignoreValues)
return distance;
// Value differences (int only — planner ignores float/vec3)
for (const auto &pair : target.values) {
auto it = values.find(pair.first);
if (it == values.end())
distance += std::abs(pair.second);
else
distance += std::abs(it->second - pair.second);
}
// Values we have that target doesn't (may need to be cleared)
for (const auto &pair : values) {
if (target.values.find(pair.first) == target.values.end())
distance += std::abs(pair.second);
}
return distance;
}
Ogre::String GoapBlackboard::dump() const
{
Ogre::String result = "Blackboard:\n";
result += " Bits:\n";
for (int i = 0; i < 64; i++) {
if (hasBit(i)) {
const char *name = getBitName(i);
if (name)
result += " " + Ogre::String(name) + " = " +
(getBit(i) ? "true" : "false") + "\n";
else
result += " bit[" +
Ogre::StringConverter::toString(i) + "] = " +
(getBit(i) ? "true" : "false") + "\n";
}
}
if (!values.empty()) {
result += " Int values:\n";
for (const auto &pair : values)
result += " " + pair.first + " = " +
Ogre::StringConverter::toString(pair.second) +
"\n";
}
if (!floatValues.empty()) {
result += " Float values:\n";
for (const auto &pair : floatValues)
result += " " + pair.first + " = " +
Ogre::StringConverter::toString(pair.second) +
"\n";
}
if (!vec3Values.empty()) {
result += " Vec3 values:\n";
for (const auto &pair : vec3Values)
result += " " + pair.first + " = (" +
Ogre::StringConverter::toString(pair.second.x) +
", " +
Ogre::StringConverter::toString(pair.second.y) +
", " +
Ogre::StringConverter::toString(pair.second.z) +
")\n";
}
if (!stringValues.empty()) {
result += " String values:\n";
for (const auto &pair : stringValues)
result += " " + pair.first + " = " + pair.second + "\n";
}
return result;
}
void GoapBlackboard::merge(const GoapBlackboard &other)
{
// Merge bits
bits = (bits & ~other.mask) | (other.bits & other.mask);
mask |= other.mask;
// Merge values
for (const auto &pair : other.values)
values[pair.first] = pair.second;
for (const auto &pair : other.floatValues)
floatValues[pair.first] = pair.second;
for (const auto &pair : other.vec3Values)
vec3Values[pair.first] = pair.second;
for (const auto &pair : other.stringValues)
stringValues[pair.first] = pair.second;
}
std::vector<int> GoapBlackboard::getSetBits() const
{
std::vector<int> result;
uint64_t m = mask;
while (m) {
int bit = __builtin_ctzll(m);
result.push_back(bit);
m &= m - 1;
}
return result;
}

View File

@@ -0,0 +1,236 @@
#ifndef EDITSCENE_GOAP_BLACKBOARD_HPP
#define EDITSCENE_GOAP_BLACKBOARD_HPP
#pragma once
#include <Ogre.h>
#include <array>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
/**
* Lightweight GOAP blackboard for action preconditions, effects,
* and per-character runtime state.
*
* Uses a 64-bit bitfield for fast boolean flag checks (used by GOAP planner).
* Supports int, float, and Vector3 values (int is used for preconditions/effects;
* float/vec3 are for behavior-tree-driven character state).
*/
struct GoapBlackboard {
// Boolean flags: 64 bits available. These are used by the GOAP planner.
uint64_t bits = 0;
uint64_t mask = 0; // which bits are actually set
// Bitmask for comparison: only bits set here are compared.
// Defaults to all 1s (compare all bits).
uint64_t bitmask = ~0ULL;
// Named integer values (health, hunger, etc.) — used by preconditions/effects
std::unordered_map<std::string, int> values;
// Named float values — runtime character state
std::unordered_map<std::string, float> floatValues;
// Named Vector3 values — runtime character state
std::unordered_map<std::string, Ogre::Vector3> vec3Values;
// Named string values — event params, tags, etc.
std::unordered_map<std::string, Ogre::String> stringValues;
GoapBlackboard() = default;
/* --- Bit accessors --- */
void setBit(int index, bool value)
{
if (index < 0 || index >= 64)
return;
uint64_t bit = 1ULL << index;
mask |= bit;
if (value)
bits |= bit;
else
bits &= ~bit;
}
bool getBit(int index) const
{
if (index < 0 || index >= 64)
return false;
return (bits >> index) & 1ULL;
}
bool hasBit(int index) const
{
if (index < 0 || index >= 64)
return false;
return (mask >> index) & 1ULL;
}
void clearBit(int index)
{
if (index < 0 || index >= 64)
return;
uint64_t bit = 1ULL << index;
mask &= ~bit;
bits &= ~bit;
}
/* --- Integer value accessors (backward compat) --- */
void setValue(const std::string &key, int value)
{
values[key] = value;
}
int getValue(const std::string &key, int defaultValue = 0) const
{
auto it = values.find(key);
if (it != values.end())
return it->second;
return defaultValue;
}
bool hasValue(const std::string &key) const
{
return values.find(key) != values.end();
}
void removeValue(const std::string &key)
{
values.erase(key);
}
/* --- Float value accessors --- */
void setFloatValue(const std::string &key, float value)
{
floatValues[key] = value;
}
float getFloatValue(const std::string &key,
float defaultValue = 0.0f) const
{
auto it = floatValues.find(key);
if (it != floatValues.end())
return it->second;
return defaultValue;
}
bool hasFloatValue(const std::string &key) const
{
return floatValues.find(key) != floatValues.end();
}
void removeFloatValue(const std::string &key)
{
floatValues.erase(key);
}
/* --- Vector3 value accessors --- */
void setVec3Value(const std::string &key, const Ogre::Vector3 &value)
{
vec3Values[key] = value;
}
Ogre::Vector3 getVec3Value(
const std::string &key,
const Ogre::Vector3 &defaultValue = Ogre::Vector3::ZERO) const
{
auto it = vec3Values.find(key);
if (it != vec3Values.end())
return it->second;
return defaultValue;
}
bool hasVec3Value(const std::string &key) const
{
return vec3Values.find(key) != vec3Values.end();
}
void removeVec3Value(const std::string &key)
{
vec3Values.erase(key);
}
/* --- String value accessors --- */
void setStringValue(const std::string &key, const Ogre::String &value)
{
stringValues[key] = value;
}
Ogre::String getStringValue(const std::string &key,
const Ogre::String &defaultValue = "") const
{
auto it = stringValues.find(key);
if (it != stringValues.end())
return it->second;
return defaultValue;
}
bool hasStringValue(const std::string &key) const
{
return stringValues.find(key) != stringValues.end();
}
void removeStringValue(const std::string &key)
{
stringValues.erase(key);
}
/* --- Merge another blackboard into this one --- */
void merge(const GoapBlackboard &other);
/* --- Generic scalar lookup (tries int then float) --- */
bool getScalarValue(const std::string &key, float &out) const;
/* --- GOAP methods --- */
bool satisfies(const GoapBlackboard &other) const;
void apply(const GoapBlackboard &other);
int distanceTo(const GoapBlackboard &target,
bool ignoreValues = false) const;
/* --- Utility --- */
bool isValid() const
{
return mask != 0 || !values.empty() || !floatValues.empty() ||
!vec3Values.empty();
}
void clear()
{
bits = 0;
mask = 0;
values.clear();
floatValues.clear();
vec3Values.clear();
stringValues.clear();
}
Ogre::String dump() const;
// Bit naming: global registry for human-readable bit names
static std::array<std::string, 64> &getBitNameRegistry();
static void setBitName(int index, const std::string &name);
static const char *getBitName(int index);
static int findBitByName(const std::string &name);
// List all set bit indices
std::vector<int> getSetBits() const;
// Equality
bool operator==(const GoapBlackboard &other) const
{
return bits == other.bits && mask == other.mask &&
bitmask == other.bitmask &&
values == other.values &&
floatValues == other.floatValues &&
vec3Values == other.vec3Values &&
stringValues == other.stringValues;
}
bool operator!=(const GoapBlackboard &other) const
{
return !(*this == other);
}
};
#endif // EDITSCENE_GOAP_BLACKBOARD_HPP

View File

@@ -0,0 +1,21 @@
#include "GoapBlackboard.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/GoapBlackboardComponentEditor.hpp"
REGISTER_COMPONENT_GROUP("Blackboard", "AI", GoapBlackboard,
GoapBlackboardComponentEditor)
{
registry.registerComponent<GoapBlackboard>(
"Blackboard", "AI",
std::make_unique<GoapBlackboardComponentEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<GoapBlackboard>())
e.set<GoapBlackboard>({});
},
// Remover
[](flecs::entity e) {
if (e.has<GoapBlackboard>())
e.remove<GoapBlackboard>();
});
}

View File

@@ -0,0 +1,283 @@
#include "GoapExpression.hpp"
#include <cctype>
#include <cstdlib>
#include <cstring>
void GoapExpression::skipWhitespace()
{
while (*m_pos == ' ' || *m_pos == '\t' || *m_pos == '\n' ||
*m_pos == '\r')
m_pos++;
}
bool GoapExpression::match(const char *s)
{
skipWhitespace();
size_t len = strlen(s);
if (strncmp(m_pos, s, len) == 0) {
m_pos += len;
return true;
}
return false;
}
GoapExpression::Node *GoapExpression::parsePrimary()
{
skipWhitespace();
// Parenthesized expression
if (match("(")) {
Node *node = parseExpression();
if (!node)
return nullptr;
if (!match(")")) {
setError("Expected ')'");
delete node;
return nullptr;
}
return node;
}
// Integer literal
if (isdigit(*m_pos) || (*m_pos == '-' && isdigit(m_pos[1]))) {
bool negative = false;
if (*m_pos == '-') {
negative = true;
m_pos++;
}
int value = 0;
while (isdigit(*m_pos)) {
value = value * 10 + (*m_pos - '0');
m_pos++;
}
Node *node = new Node(Node::Value);
node->value = negative ? -value : value;
return node;
}
// Variable name
if (isalpha(*m_pos) || *m_pos == '_') {
std::string name;
while (isalnum(*m_pos) || *m_pos == '_') {
name += *m_pos;
m_pos++;
}
Node *node = new Node(Node::Variable);
node->name = name;
return node;
}
setError("Unexpected character in expression");
return nullptr;
}
GoapExpression::Node *GoapExpression::parseComparison()
{
Node *left = parsePrimary();
if (!left)
return nullptr;
skipWhitespace();
if (match("==")) {
Node *right = parsePrimary();
if (!right) {
delete left;
return nullptr;
}
Node *node = new Node(Node::Equal);
node->left = left;
node->right = right;
return node;
} else if (match("!=")) {
Node *right = parsePrimary();
if (!right) {
delete left;
return nullptr;
}
Node *node = new Node(Node::NotEqual);
node->left = left;
node->right = right;
return node;
} else if (match("<=")) {
Node *right = parsePrimary();
if (!right) {
delete left;
return nullptr;
}
Node *node = new Node(Node::LessEqual);
node->left = left;
node->right = right;
return node;
} else if (match(">=")) {
Node *right = parsePrimary();
if (!right) {
delete left;
return nullptr;
}
Node *node = new Node(Node::GreaterEqual);
node->left = left;
node->right = right;
return node;
} else if (match("<")) {
Node *right = parsePrimary();
if (!right) {
delete left;
return nullptr;
}
Node *node = new Node(Node::Less);
node->left = left;
node->right = right;
return node;
} else if (match(">")) {
Node *right = parsePrimary();
if (!right) {
delete left;
return nullptr;
}
Node *node = new Node(Node::Greater);
node->left = left;
node->right = right;
return node;
}
return left;
}
GoapExpression::Node *GoapExpression::parseNot()
{
skipWhitespace();
if (match("!")) {
Node *child = parseNot();
if (!child)
return nullptr;
Node *node = new Node(Node::Not);
node->left = child;
return node;
}
return parseComparison();
}
GoapExpression::Node *GoapExpression::parseAnd()
{
Node *left = parseNot();
if (!left)
return nullptr;
while (true) {
if (match("&&")) {
Node *right = parseNot();
if (!right) {
delete left;
return nullptr;
}
Node *node = new Node(Node::And);
node->left = left;
node->right = right;
left = node;
} else {
break;
}
}
return left;
}
GoapExpression::Node *GoapExpression::parseExpression()
{
Node *left = parseAnd();
if (!left)
return nullptr;
while (true) {
if (match("||")) {
Node *right = parseAnd();
if (!right) {
delete left;
return nullptr;
}
Node *node = new Node(Node::Or);
node->left = left;
node->right = right;
left = node;
} else {
break;
}
}
return left;
}
bool GoapExpression::parse(const char *expr)
{
clear();
m_expr = expr;
m_pos = expr;
m_root = parseExpression();
if (!m_root)
return false;
skipWhitespace();
if (*m_pos != '\0') {
setError("Unexpected trailing characters");
clear();
return false;
}
return true;
}
int GoapExpression::evalNode(Node *node, const GoapBlackboard &bb) const
{
if (!node)
return 0;
switch (node->type) {
case Node::Value:
return node->value;
case Node::Variable:
return bb.getValue(node->name, 0);
case Node::Equal:
return evalNode(node->left, bb) == evalNode(node->right, bb) ? 1 : 0;
case Node::NotEqual:
return evalNode(node->left, bb) != evalNode(node->right, bb) ? 1 : 0;
case Node::Less:
return evalNode(node->left, bb) < evalNode(node->right, bb) ? 1 : 0;
case Node::Greater:
return evalNode(node->left, bb) > evalNode(node->right, bb) ? 1 : 0;
case Node::LessEqual:
return evalNode(node->left, bb) <= evalNode(node->right, bb) ? 1 : 0;
case Node::GreaterEqual:
return evalNode(node->left, bb) >= evalNode(node->right, bb) ? 1 : 0;
case Node::And:
return evalNode(node->left, bb) && evalNode(node->right, bb) ? 1 : 0;
case Node::Or:
return evalNode(node->left, bb) || evalNode(node->right, bb) ? 1 : 0;
case Node::Not:
return !evalNode(node->left, bb) ? 1 : 0;
}
return 0;
}
bool GoapExpression::evaluate(const GoapBlackboard &blackboard) const
{
if (!m_root)
return false;
return evalNode(m_root, blackboard) != 0;
}
void GoapExpression::setError(const char *msg)
{
m_error = msg;
if (m_pos && m_expr) {
m_error += " at position ";
m_error += std::to_string(m_pos - m_expr);
m_error += " near \"";
m_error += std::string(m_pos, strnlen(m_pos, 20));
m_error += "\"";
}
}
void GoapExpression::clear()
{
delete m_root;
m_root = nullptr;
m_expr = nullptr;
m_pos = nullptr;
m_error.clear();
}

View File

@@ -0,0 +1,85 @@
#ifndef EDITSCENE_GOAP_EXPRESSION_HPP
#define EDITSCENE_GOAP_EXPRESSION_HPP
#pragma once
#include "GoapBlackboard.hpp"
#include <string>
/**
* Simple expression evaluator for GOAP goal conditions.
*
* Supports:
* - Variable names (looked up in blackboard values, default 0)
* - Integer literals
* - Comparisons: ==, !=, <, >, <=, >=
* - Boolean operators: &&, ||
* - Parentheses for grouping
* - Unary negation: !
*
* Example: "health > 20 && (hunger > 50 || have_food == 1)"
*/
class GoapExpression {
public:
GoapExpression() = default;
// Parse an expression string. Returns true on success.
bool parse(const char *expr);
// Evaluate the parsed expression against a blackboard.
// Returns false if expression was not parsed successfully.
bool evaluate(const GoapBlackboard &blackboard) const;
// Get last error message
const std::string &getError() const { return m_error; }
private:
struct Node {
enum Type {
Value, // integer literal
Variable, // blackboard variable name
Equal,
NotEqual,
Less,
Greater,
LessEqual,
GreaterEqual,
And,
Or,
Not
} type;
int value = 0; // for Value
std::string name; // for Variable
Node *left = nullptr;
Node *right = nullptr;
Node(Type t)
: type(t)
{
}
~Node()
{
delete left;
delete right;
}
};
const char *m_expr = nullptr;
const char *m_pos = nullptr;
std::string m_error;
Node *m_root = nullptr;
void skipWhitespace();
bool match(const char *s);
Node *parseExpression(); // ||
Node *parseAnd(); // &&
Node *parseNot(); // !
Node *parseComparison(); // ==, !=, <, >, <=, >=
Node *parsePrimary(); // value, variable, (expr)
int evalNode(Node *node, const GoapBlackboard &bb) const;
void setError(const char *msg);
void clear();
};
#endif // EDITSCENE_GOAP_EXPRESSION_HPP

View File

@@ -0,0 +1,24 @@
#include "GoapGoal.hpp"
#include "GoapExpression.hpp"
bool GoapGoal::isSatisfied(const GoapBlackboard &blackboard) const
{
return blackboard.satisfies(target);
}
bool GoapGoal::isValid(const GoapBlackboard &blackboard) const
{
// If already satisfied, not a valid goal to pursue
if (isSatisfied(blackboard))
return false;
// Evaluate condition if present
if (!condition.empty()) {
GoapExpression expr;
if (expr.parse(condition.c_str())) {
return expr.evaluate(blackboard);
}
}
return true;
}

View File

@@ -0,0 +1,43 @@
#ifndef EDITSCENE_GOAP_GOAL_HPP
#define EDITSCENE_GOAP_GOAL_HPP
#pragma once
#include "GoapBlackboard.hpp"
#include <Ogre.h>
/**
* A GOAP goal definition.
*
* Goals are selected based on priority and validity.
* The target blackboard defines the desired world state.
* The condition string provides additional runtime validity checks
* using a simple expression language against blackboard values.
*/
struct GoapGoal {
Ogre::String name;
int priority = 1;
// Target blackboard state to achieve
GoapBlackboard target;
// Optional condition expression (e.g. "health > 20 && hunger > 50")
// If empty, the goal is always considered for validity checking
Ogre::String condition;
GoapGoal() = default;
explicit GoapGoal(const Ogre::String &name_, int priority_ = 1)
: name(name_)
, priority(priority_)
{
}
// Check if the goal is already satisfied by the given blackboard
bool isSatisfied(const GoapBlackboard &blackboard) const;
// Check if the goal is valid for the given blackboard
// (condition evaluates to true and goal is not already satisfied)
bool isValid(const GoapBlackboard &blackboard) const;
};
#endif // EDITSCENE_GOAP_GOAL_HPP

View File

@@ -0,0 +1,99 @@
#ifndef EDITSCENE_GOAP_PLANNER_HPP
#define EDITSCENE_GOAP_PLANNER_HPP
#pragma once
#include <Ogre.h>
#include <vector>
#include <string>
#include <queue>
/**
* GOAP Planner component.
*
* Holds a curated list of action and goal names from an ActionDatabase,
* plus configuration for smart-object action discovery.
* The planner resolves names against an ActionDatabase at runtime.
*
* The actionNames and goalNames lists act as external references:
* prefabs can store them even when the ActionDatabase is not
* part of the prefab itself.
*/
struct GoapPlannerComponent {
// Selected action names from ActionDatabase
std::vector<Ogre::String> actionNames;
// Selected goal names from ActionDatabase
std::vector<Ogre::String> goalNames;
// Maximum distance to search for smart objects with matching actions
float smartObjectDistance = 50.0f;
// Whether to include smart object actions in planning
bool includeSmartObjects = true;
// Optional reference to an external ActionDatabase entity by name.
Ogre::String actionDatabaseRef;
// -----------------------------------------------------------------
// Runtime plan queue (not serialized)
// -----------------------------------------------------------------
struct Plan {
std::vector<Ogre::String> actions;
int totalCost = 0;
Ogre::String goalName;
};
std::vector<Plan> planQueue;
// Planner status
enum class Status {
Idle, // No planning requested
Planning, // Currently planning
PlansAvailable, // One or more plans in queue
NoPlanFound // Planning finished but no valid plan found
};
Status status = Status::Idle;
// Goal name that was used for the current plan batch
Ogre::String currentGoalName;
// Planning control
bool planDirty = true;
int maxPlans = 3; // stop after generating this many plans
// Planning progress (for status display)
int plansGenerated = 0;
int nodesExplored = 0;
GoapPlannerComponent() = default;
void clearPlans()
{
planQueue.clear();
plansGenerated = 0;
status = Status::Idle;
}
// Pop the cheapest plan from the queue
Plan popCheapestPlan()
{
if (planQueue.empty())
return Plan();
size_t bestIdx = 0;
for (size_t i = 1; i < planQueue.size(); i++) {
if (planQueue[i].totalCost < planQueue[bestIdx].totalCost)
bestIdx = i;
}
Plan result = std::move(planQueue[bestIdx]);
planQueue.erase(planQueue.begin() + bestIdx);
if (planQueue.empty() && status == Status::PlansAvailable)
status = Status::Idle;
return result;
}
bool hasPlans() const
{
return !planQueue.empty();
}
};
#endif // EDITSCENE_GOAP_PLANNER_HPP

View File

@@ -0,0 +1,21 @@
#include "GoapPlanner.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/GoapPlannerEditor.hpp"
REGISTER_COMPONENT_GROUP("GOAP Planner", "AI", GoapPlannerComponent,
GoapPlannerEditor)
{
registry.registerComponent<GoapPlannerComponent>(
"GOAP Planner", "AI",
std::make_unique<GoapPlannerEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<GoapPlannerComponent>())
e.set<GoapPlannerComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<GoapPlannerComponent>())
e.remove<GoapPlannerComponent>();
});
}

View File

@@ -0,0 +1,50 @@
#ifndef EDITSCENE_GOAP_RUNNER_HPP
#define EDITSCENE_GOAP_RUNNER_HPP
#pragma once
#include <Ogre.h>
#include <string>
#include <vector>
/**
* GOAP Runner component.
*
* Executes plans from GoapPlannerComponent.
* For normal actions: runs the action's behavior tree.
* For smart object actions: pathfinds to the smart object and executes.
* After plan completion, marks the planner dirty for replanning.
*/
struct GoapRunnerComponent {
// Current plan execution state
enum class State {
Idle, // No plan running
RunningAction, // Executing a normal action
MovingToSmartObject, // Pathfinding to a smart object
ExecutingSmartObject, // Executing smart object action
PlanComplete // Plan finished, waiting for replan
};
State state = State::Idle;
// Index of current action in the plan
int currentActionIndex = 0;
// Name of the currently executing action
Ogre::String currentActionName;
// Timer for action execution
float actionTimer = 0.0f;
// Entity ID of target smart object (if applicable)
uint64_t targetSmartObjectId = 0;
// Active plan actions (copied from planner when plan starts)
std::vector<Ogre::String> planActions;
// Whether to auto-replan after completion
bool autoReplan = true;
GoapRunnerComponent() = default;
};
#endif // EDITSCENE_GOAP_RUNNER_HPP

View File

@@ -0,0 +1,21 @@
#include "GoapRunner.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/GoapRunnerEditor.hpp"
REGISTER_COMPONENT_GROUP("GOAP Runner", "AI", GoapRunnerComponent,
GoapRunnerEditor)
{
registry.registerComponent<GoapRunnerComponent>(
"GOAP Runner", "AI",
std::make_unique<GoapRunnerEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<GoapRunnerComponent>())
e.set<GoapRunnerComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<GoapRunnerComponent>())
e.remove<GoapRunnerComponent>();
});
}

View File

@@ -0,0 +1,165 @@
#ifndef EDITSCENE_INVENTORY_HPP
#define EDITSCENE_INVENTORY_HPP
#pragma once
#include <Ogre.h>
#include <flecs.h>
#include <vector>
#include <string>
#include <cstdint>
/**
* A single slot in an inventory.
* Stores a reference to an item entity (if the item is a world entity)
* or stores item data directly for items that exist only in inventory.
*/
struct InventorySlot {
// Flecs entity ID of the item (0 if slot is empty)
flecs::entity_t itemEntity = 0;
// Item data for items that exist only in inventory (no world entity)
Ogre::String itemId;
Ogre::String itemName;
Ogre::String itemType;
int stackSize = 0;
int maxStackSize = 99;
float weight = 0.1f;
int value = 1;
Ogre::String useActionName;
bool isEmpty() const
{
return itemEntity == 0 && stackSize <= 0;
}
void clear()
{
itemEntity = 0;
itemId.clear();
itemName.clear();
itemType.clear();
stackSize = 0;
maxStackSize = 99;
weight = 0.1f;
value = 1;
useActionName.clear();
}
};
/**
* Inventory component.
*
* Attached to a character entity to hold items.
* Can also be attached to container entities (chests, barrels, etc.)
* to define their contents.
*
* The inventory stores items as InventorySlot entries, each of which
* may reference a world ItemComponent entity or hold item data directly.
*/
struct InventoryComponent {
// Maximum number of slots
int maxSlots = 20;
// Current slots
std::vector<InventorySlot> slots;
// Total weight of all items (computed)
float totalWeight = 0.0f;
// Maximum weight capacity (0 = unlimited)
float maxWeight = 50.0f;
// Whether this inventory is a container (chest, barrel, etc.)
// Containers can be opened by characters to transfer items.
bool isContainer = false;
// Whether this inventory is currently open (for containers being browsed)
bool isOpen = false;
InventoryComponent() = default;
explicit InventoryComponent(int maxSlots_)
: maxSlots(maxSlots_)
{
slots.reserve(maxSlots_);
}
/** Find the first empty slot index, or -1 if full. */
int findEmptySlot() const
{
for (int i = 0; i < maxSlots; i++) {
if (i >= (int)slots.size())
return i;
if (slots[i].isEmpty())
return i;
}
return -1;
}
/** Find a slot containing an item with the given itemId. */
int findItem(const Ogre::String &itemId) const
{
for (int i = 0; i < (int)slots.size(); i++) {
if (!slots[i].isEmpty() && slots[i].itemId == itemId)
return i;
}
return -1;
}
/** Find a slot containing an item with the given itemName. */
int findItemByName(const Ogre::String &itemName) const
{
for (int i = 0; i < (int)slots.size(); i++) {
if (!slots[i].isEmpty() &&
slots[i].itemName == itemName)
return i;
}
return -1;
}
/** Count total number of items (sum of stack sizes). */
int countItems() const
{
int count = 0;
for (const auto &slot : slots) {
if (!slot.isEmpty())
count += slot.stackSize;
}
return count;
}
/** Count how many of a specific itemId are in the inventory. */
int countItem(const Ogre::String &itemId) const
{
int count = 0;
for (const auto &slot : slots) {
if (!slot.isEmpty() && slot.itemId == itemId)
count += slot.stackSize;
}
return count;
}
/** Check if inventory has at least one of a specific itemId. */
bool hasItem(const Ogre::String &itemId) const
{
return findItem(itemId) >= 0;
}
/** Check if inventory has at least one of a specific itemName. */
bool hasItemByName(const Ogre::String &itemName) const
{
return findItemByName(itemName) >= 0;
}
/** Recalculate total weight. */
void recalculateWeight()
{
totalWeight = 0.0f;
for (const auto &slot : slots) {
if (!slot.isEmpty())
totalWeight += slot.weight * slot.stackSize;
}
}
};
#endif // EDITSCENE_INVENTORY_HPP

View File

@@ -0,0 +1,20 @@
#include "Inventory.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/InventoryEditor.hpp"
REGISTER_COMPONENT_GROUP("Inventory", "Game", InventoryComponent,
InventoryEditor)
{
registry.registerComponent<InventoryComponent>(
"Inventory", "Game", std::make_unique<InventoryEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<InventoryComponent>())
e.set<InventoryComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<InventoryComponent>())
e.remove<InventoryComponent>();
});
}

View File

@@ -0,0 +1,59 @@
#ifndef EDITSCENE_ITEM_HPP
#define EDITSCENE_ITEM_HPP
#pragma once
#include <Ogre.h>
#include <string>
#include <vector>
/**
* Item definition component.
*
* Attached to a world entity that represents a pickable item.
* The ActuatorSystem detects items (entities with ItemComponent)
* and shows "E - Pick up [ItemName]" prompts to the player.
*
* Items can also be placed in containers (chests, etc.) which
* have an InventoryComponent.
*
* For AI characters, behavior tree nodes (hasItem, pickupItem,
* dropItem, useItem, addItemToInventory) provide inventory access.
*/
struct ItemComponent {
// Display name of the item (e.g. "Apple", "Sword", "Key")
Ogre::String itemName = "Item";
// Item type for categorization (e.g. "food", "weapon", "key", "quest")
Ogre::String itemType = "misc";
// Unique identifier for this item definition
// Multiple entities can share the same itemId (e.g. multiple coins)
Ogre::String itemId;
// Stack size: how many of this item are in this stack
int stackSize = 1;
// Maximum stack size (0 = no stacking)
int maxStackSize = 99;
// Weight per unit (for encumbrance calculations)
float weight = 0.1f;
// Value (for trading)
int value = 1;
// Name of the GOAP action to execute when "using" this item
// (e.g. "eat", "equip", "read"). Empty = no use action.
Ogre::String useActionName;
ItemComponent() = default;
explicit ItemComponent(const Ogre::String &name,
const Ogre::String &type = "misc")
: itemName(name)
, itemType(type)
{
}
};
#endif // EDITSCENE_ITEM_HPP

View File

@@ -0,0 +1,19 @@
#include "Item.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/ItemEditor.hpp"
REGISTER_COMPONENT_GROUP("Item", "Game", ItemComponent, ItemEditor)
{
registry.registerComponent<ItemComponent>(
"Item", "Game", std::make_unique<ItemEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<ItemComponent>())
e.set<ItemComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<ItemComponent>())
e.remove<ItemComponent>();
});
}

View File

@@ -0,0 +1,59 @@
#ifndef EDITSCENE_NAVMESH_HPP
#define EDITSCENE_NAVMESH_HPP
#pragma once
#include <Ogre.h>
#include <cstdint>
/**
* Navigation mesh component.
*
* Attached to a single "manager" entity that owns the tiled navmesh
* for the entire scene. The NavMeshSystem collects geometry from
* static rigid bodies, StaticGeometry members, and entities with
* NavMeshGeometrySource to build the mesh.
*/
struct NavMeshComponent {
// Recast build parameters
float cellSize = 0.3f;
float cellHeight = 0.2f;
float agentHeight = 2.5f;
float agentRadius = 0.5f;
float agentMaxClimb = 1.0f;
float agentMaxSlope = 20.0f;
float edgeMaxLen = 12.0f;
float edgeMaxError = 1.3f;
float regionMinSize = 50.0f;
float regionMergeSize = 20.0f;
int tileSize = 48; // cells per tile
// Runtime flags
bool enabled = true;
bool debugDraw = false;
bool dirty = true;
// Partial rebuild tracking (not serialized)
bool needsPartialRebuild = false;
Ogre::AxisAlignedBox rebuildArea;
};
/**
* Component that forces an entity's geometry to be included in
* navmesh generation regardless of physics state.
*/
struct NavMeshGeometrySource {
bool include = true;
};
/**
* Component for entities that want pathfinding on this navmesh.
*/
struct NavMeshAgent {
Ogre::Vector3 targetPos = Ogre::Vector3::ZERO;
bool hasTarget = false;
bool pathPending = false;
std::vector<Ogre::Vector3> currentPath;
int pathIndex = 0;
};
#endif // EDITSCENE_NAVMESH_HPP

View File

@@ -0,0 +1,38 @@
#include "NavMesh.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/NavMeshEditor.hpp"
#include "../ui/NavMeshGeometrySourceEditor.hpp"
REGISTER_COMPONENT_GROUP("NavMesh", "Navigation", NavMeshComponent,
NavMeshEditor)
{
registry.registerComponent<NavMeshComponent>(
"NavMesh", "Navigation",
std::make_unique<NavMeshEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<NavMeshComponent>())
e.set<NavMeshComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<NavMeshComponent>())
e.remove<NavMeshComponent>();
});
}
REGISTER_COMPONENT_GROUP("NavMesh Geometry Source", "Navigation",
NavMeshGeometrySource, NavMeshGeometrySourceEditor)
{
registry.registerComponent<NavMeshGeometrySource>(
"NavMesh Geometry Source", "Navigation",
std::make_unique<NavMeshGeometrySourceEditor>(),
[](flecs::entity e) {
if (!e.has<NavMeshGeometrySource>())
e.set<NavMeshGeometrySource>({});
},
[](flecs::entity e) {
if (e.has<NavMeshGeometrySource>())
e.remove<NavMeshGeometrySource>();
});
}

View File

@@ -0,0 +1,77 @@
#ifndef EDITSCENE_PATH_FOLLOWING_HPP
#define EDITSCENE_PATH_FOLLOWING_HPP
#pragma once
#include <Ogre.h>
#include <vector>
#include <string>
#include <utility>
/**
* Animation state configuration for path following.
*
* Each state (e.g. "idle", "walk", "run") consists of unlimited
* state-machine-name -> state-name pairs applied via AnimationTreeSystem.
*/
struct PathFollowingState {
Ogre::String name;
std::vector<std::pair<Ogre::String, Ogre::String> > stateMachineStates;
PathFollowingState() = default;
explicit PathFollowingState(const Ogre::String &name_)
: name(name_)
{
}
};
/**
* Path Following component.
*
* Configures animation states for locomotion (idle/walk/run)
* and stores the current locomotion state. Used by GoapRunner
* to animate characters during plan execution.
*/
struct PathFollowingComponent {
// Animation state configurations
std::vector<PathFollowingState> pathFollowingStates = {
PathFollowingState("idle"),
PathFollowingState("walk"),
PathFollowingState("run"),
};
// Current locomotion state name
Ogre::String currentLocomotionState = "idle";
// Walk speed (m/s) for root motion scaling
float walkSpeed = 2.5f;
// Run speed (m/s) for root motion scaling
float runSpeed = 5.0f;
// Whether to use root motion
bool useRootMotion = true;
// Target position for path following (set by GoapRunner)
Ogre::Vector3 targetPosition = Ogre::Vector3::ZERO;
// Whether we have an active target
bool hasTarget = false;
// Path waypoints (set by GoapRunner, followed by PathFollowingSystem)
std::vector<Ogre::Vector3> path;
int pathIndex = 0;
float pathRecalcTimer = 0.0f;
PathFollowingComponent() = default;
const PathFollowingState *findState(const Ogre::String &name) const
{
for (const auto &state : pathFollowingStates) {
if (state.name == name)
return &state;
}
return nullptr;
}
};
#endif // EDITSCENE_PATH_FOLLOWING_HPP

View File

@@ -0,0 +1,21 @@
#include "PathFollowing.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/PathFollowingEditor.hpp"
REGISTER_COMPONENT_GROUP("Path Following", "AI", PathFollowingComponent,
PathFollowingEditor)
{
registry.registerComponent<PathFollowingComponent>(
"Path Following", "AI",
std::make_unique<PathFollowingEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<PathFollowingComponent>())
e.set<PathFollowingComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<PathFollowingComponent>())
e.remove<PathFollowingComponent>();
});
}

View File

@@ -26,6 +26,17 @@ struct PlayerControllerComponent {
Ogre::String swimIdleState = "swim-idle";
Ogre::String swimState = "swim";
Ogre::String swimFastState = "swim-fast";
/* Actuator interaction settings */
float actuatorDistance = 25.0f;
float actuatorCooldown = 1.5f;
Ogre::Vector3 actuatorColor = Ogre::Vector3(0.0f, 0.4f, 1.0f);
float distantCircleRadius = 8.0f;
float nearCircleRadius = 14.0f;
float actuatorLabelFontSize = 14.0f;
/* Runtime: set by ActuatorSystem while executing an action */
bool inputLocked = false;
};
#endif // EDITSCENE_PLAYERCONTROLLER_HPP

View File

@@ -0,0 +1,24 @@
#ifndef EDITSCENE_PREFABINSTANCE_HPP
#define EDITSCENE_PREFABINSTANCE_HPP
#pragma once
#include <string>
/**
* @brief Marks an entity as an instance of a prefab asset.
*
* The entity's TransformComponent acts as the world-space override
* for the prefab root. All other components (and the child subtree)
* are loaded from the prefab file at runtime and are NOT serialized
* with the main scene.
*/
struct PrefabInstanceComponent {
/** Path to the prefab JSON file (relative to working dir) */
std::string prefabPath;
/** Set to true once the prefab has been instantiated.
* Prevents double-instantiation on repeated resolve calls.
*/
bool instantiated = false;
};
#endif // EDITSCENE_PREFABINSTANCE_HPP

View File

@@ -2,6 +2,7 @@
#include "../ui/ComponentRegistration.hpp"
#include "../ui/ProceduralMaterialEditor.hpp"
#include <OgreMaterialManager.h>
#include <OgreRTShaderSystem.h>
// Register ProceduralMaterial component
REGISTER_COMPONENT_GROUP("Procedural Material", "Material", ProceduralMaterialComponent, ProceduralMaterialEditor)
@@ -22,6 +23,12 @@ REGISTER_COMPONENT_GROUP("Procedural Material", "Material", ProceduralMaterialCo
// Clean up Ogre material - wrap in try/catch since MaterialManager may be shutting down
if (material.ogreMaterial) {
try {
Ogre::RTShader::ShaderGenerator *shaderGen =
Ogre::RTShader::ShaderGenerator::getSingletonPtr();
if (shaderGen) {
shaderGen->removeAllShaderBasedTechniques(
*material.ogreMaterial);
}
if (Ogre::MaterialManager::getSingletonPtr()) {
Ogre::MaterialManager::getSingleton().remove(material.ogreMaterial);
}

View File

@@ -0,0 +1,58 @@
#ifndef EDITSCENE_SKYBOX_HPP
#define EDITSCENE_SKYBOX_HPP
#pragma once
#include <Ogre.h>
/**
* Skybox component - procedural sky rendered as a large cube
* with a fragment shader that creates dynamic day/night/sunset
* sky gradients.
*
* Designed to work alongside SunComponent on the same entity.
* If no SunComponent is present, uses default noon lighting.
*/
struct SkyboxComponent {
// Enable/disable skybox
bool enabled = true;
// Size of the skybox cube (default 500)
float size = 500.0f;
// Day sky colors
Ogre::ColourValue dayTopColor = Ogre::ColourValue(0.2f, 0.5f, 1.0f);
Ogre::ColourValue dayBottomColor = Ogre::ColourValue(0.6f, 0.8f, 1.0f);
// Night sky colors
Ogre::ColourValue nightTopColor = Ogre::ColourValue(0.0f, 0.0f, 0.05f);
Ogre::ColourValue nightBottomColor = Ogre::ColourValue(0.05f, 0.05f, 0.15f);
// Horizon glow colors
Ogre::ColourValue sunriseColor = Ogre::ColourValue(1.0f, 0.5f, 0.2f);
Ogre::ColourValue sunsetColor = Ogre::ColourValue(1.0f, 0.3f, 0.1f);
// Angular size of sun/moon discs in the sky shader (0.01 - 0.2)
float sunSize = 0.05f;
float moonSize = 0.03f;
// Enable simple stars at night
bool starsEnabled = true;
// Cloud coverage (0.0 = clear, 1.0 = overcast)
// Not yet implemented in shader but reserved for future
float cloudiness = 0.0f;
// Runtime objects (managed by EditorSkyboxSystem)
Ogre::SceneNode *sceneNode = nullptr;
Ogre::ManualObject *manualObject = nullptr;
// Dirty flag - triggers rebuild
bool dirty = true;
void markDirty()
{
dirty = true;
}
};
#endif // EDITSCENE_SKYBOX_HPP

View File

@@ -0,0 +1,24 @@
#include "Skybox.hpp"
#include "Transform.hpp"
#include "EditorMarker.hpp"
#include "EntityName.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/SkyboxEditor.hpp"
REGISTER_COMPONENT_GROUP("Skybox", "Environment", SkyboxComponent, SkyboxEditor)
{
registry.registerComponent<SkyboxComponent>(
"Skybox", "Environment", std::make_unique<SkyboxEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<SkyboxComponent>()) {
e.set<SkyboxComponent>({});
}
},
// Remover
[](flecs::entity e) {
if (e.has<SkyboxComponent>()) {
e.remove<SkyboxComponent>();
}
});
}

View File

@@ -0,0 +1,38 @@
#ifndef EDITSCENE_SMART_OBJECT_HPP
#define EDITSCENE_SMART_OBJECT_HPP
#pragma once
#include <Ogre.h>
#include <vector>
#include <string>
/**
* Smart Object component.
*
* Defines an interactive object in the world that characters can
* navigate to and perform GOAP actions on.
*
* The entity's TransformComponent defines the position/orientation.
* Characters will pathfind to within `radius` distance in XZ plane
* and `height` difference in Y, then execute the selected action.
*/
struct SmartObjectComponent {
// Interaction radius in XZ plane
float radius = 1.0f;
// Maximum height difference for interaction
float height = 1.8f;
// Names of GOAP actions (from ActionDatabase) that this object provides
std::vector<Ogre::String> actionNames;
SmartObjectComponent() = default;
explicit SmartObjectComponent(float radius_, float height_)
: radius(radius_)
, height(height_)
{
}
};
#endif // EDITSCENE_SMART_OBJECT_HPP

View File

@@ -0,0 +1,20 @@
#include "SmartObject.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/SmartObjectEditor.hpp"
REGISTER_COMPONENT_GROUP("Smart Object", "AI", SmartObjectComponent,
SmartObjectEditor)
{
registry.registerComponent<SmartObjectComponent>(
"Smart Object", "AI", std::make_unique<SmartObjectEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<SmartObjectComponent>())
e.set<SmartObjectComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<SmartObjectComponent>())
e.remove<SmartObjectComponent>();
});
}

View File

@@ -0,0 +1,70 @@
#ifndef EDITSCENE_SUN_HPP
#define EDITSCENE_SUN_HPP
#pragma once
#include <Ogre.h>
/**
* Sun component - manages a directional light that rotates
* based on in-game time, with sun/moon visualization.
*
* Designed to work alongside SkyboxComponent on the same entity.
*/
struct SunComponent {
// Enable/disable sun system
bool enabled = true;
// Time of day in hours (0.0 - 24.0)
float timeOfDay = 12.0f;
// Game time speed: game-hours per real-second
// 0.01 = 1 game hour per 100 real seconds (slow)
// 0.1 = 1 game hour per 10 real seconds
// 1.0 = 1 game hour per 1 real second (fast)
float timeSpeed = 0.05f;
// Sun color when at zenith (day)
Ogre::ColourValue sunColor = Ogre::ColourValue(1.0f, 0.95f, 0.8f);
// Moon color when sun is below horizon (night)
Ogre::ColourValue moonColor = Ogre::ColourValue(0.3f, 0.3f, 0.5f);
// Ambient light colors
Ogre::ColourValue ambientDay = Ogre::ColourValue(0.3f, 0.3f, 0.3f);
Ogre::ColourValue ambientNight = Ogre::ColourValue(0.05f, 0.05f, 0.15f);
Ogre::ColourValue ambientSunrise = Ogre::ColourValue(0.3f, 0.2f, 0.15f);
Ogre::ColourValue ambientSunset = Ogre::ColourValue(0.25f, 0.15f, 0.1f);
// Sun / moon visualization spheres
bool showSunSphere = true;
bool showMoonSphere = true;
float sunSphereSize = 5.0f;
float moonSphereSize = 3.0f;
// Orbit tilt (degrees) - tilts the sun path north/south
float orbitTilt = 15.0f;
// Light intensity multiplier
float intensity = 1.0f;
// Cast shadows
bool castShadows = true;
// Runtime objects (managed by EditorSunSystem)
Ogre::Light *light = nullptr;
Ogre::SceneNode *lightNode = nullptr;
Ogre::SceneNode *sunSphereNode = nullptr;
Ogre::SceneNode *moonSphereNode = nullptr;
Ogre::ManualObject *sunSphere = nullptr;
Ogre::ManualObject *moonSphere = nullptr;
// Dirty flag - triggers rebuild
bool dirty = true;
void markDirty()
{
dirty = true;
}
};
#endif // EDITSCENE_SUN_HPP

View File

@@ -0,0 +1,24 @@
#include "Sun.hpp"
#include "Transform.hpp"
#include "EditorMarker.hpp"
#include "EntityName.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/SunEditor.hpp"
REGISTER_COMPONENT_GROUP("Sun", "Environment", SunComponent, SunEditor)
{
registry.registerComponent<SunComponent>(
"Sun", "Environment", std::make_unique<SunEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<SunComponent>()) {
e.set<SunComponent>({});
}
},
// Remover
[](flecs::entity e) {
if (e.has<SunComponent>()) {
e.remove<SunComponent>();
}
});
}

View File

@@ -6,58 +6,68 @@
/**
* WaterPlane component
* Provides visual representation of water surface for buoyancy system
* Creates a plane mesh at the water surface Y level for visualization
* Visual water surface with reflection/refraction
* for the editScene editor. Lightweight and OpenGL ES 2.0 compatible.
*/
struct WaterPlane {
// Enable/disable water plane visualization
// Enable/disable water
bool enabled = true;
// Water surface Y level (world space) - should match WaterPhysics::waterSurfaceY
// Water surface Y level (world space)
float waterSurfaceY = -0.1f;
// Plane size (width and depth)
float planeSize = 100.0f;
float planeSize = 500.0f;
// Material name for water plane
Ogre::String materialName = "BaseWhiteNoLighting";
// Opacity (0.0 = transparent, 1.0 = opaque)
float opacity = 0.3f;
// Color tint
Ogre::ColourValue color = Ogre::ColourValue(0.2f, 0.4f, 0.8f, 0.3f);
// Whether to update automatically from WaterPhysics component
// Whether to update waterSurfaceY from WaterPhysics component
bool autoUpdateFromWaterPhysics = true;
// Scene node for the water plane (managed by system)
Ogre::SceneNode *sceneNode = nullptr;
// Visual settings
Ogre::ColourValue waterColor = Ogre::ColourValue(0.0f, 0.3f, 0.5f,
0.8f);
float reflectivity = 0.5f;
float waveSpeed = 1.0f;
float waveScale = 0.02f;
float tiling = 0.012f;
// Manual object for the water plane (managed by system)
// Render texture size (power of two recommended)
int renderTextureSize = 512;
// Runtime objects (managed by EditorWaterPlaneSystem)
Ogre::SceneNode *sceneNode = nullptr;
Ogre::ManualObject *manualObject = nullptr;
// Mark component as dirty (needs update)
// Render-to-texture resources
Ogre::TexturePtr renderTexture;
Ogre::Camera *reflectionCamera = nullptr;
Ogre::Camera *refractionCamera = nullptr;
Ogre::Viewport *reflectionViewport = nullptr;
Ogre::Viewport *refractionViewport = nullptr;
Ogre::RenderTarget *renderTarget = nullptr;
// Clip planes
Ogre::Plane reflectionPlane;
Ogre::Plane reflectionClipPlane;
Ogre::Plane refractionClipPlane;
// Camera following state
Ogre::Vector3 lastCameraPos = Ogre::Vector3::ZERO;
float positionUpdateTimer = 0.0f;
static constexpr float POSITION_UPDATE_INTERVAL = 0.3f;
// Which viewport to update this frame (0=reflection, 1=refraction)
int updateViewportIndex = 0;
// Time accumulator for shader
float shaderTime = 0.0f;
// Dirty flag
bool dirty = true;
void markDirty()
{
dirty = true;
}
WaterPlane() = default;
WaterPlane(float surfaceY, float size, const Ogre::String &material,
float opac, const Ogre::ColourValue &col, bool autoUpdate)
: waterSurfaceY(surfaceY)
, planeSize(size)
, materialName(material)
, opacity(opac)
, color(col)
, autoUpdateFromWaterPhysics(autoUpdate)
, dirty(true)
{
}
};
#endif // EDITSCENE_WATERPLANE_HPP
#endif // EDITSCENE_WATERPLANE_HPP

View File

@@ -1,23 +1,15 @@
#include "WaterPlane.hpp"
#include "../components/WaterPhysics.hpp"
#include "../components/EditorMarker.hpp"
#include "../components/EntityName.hpp"
#include "../components/Transform.hpp"
#include "Transform.hpp"
#include "EditorMarker.hpp"
#include "EntityName.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/WaterPlaneEditor.hpp"
#include <OgreLogManager.h>
#include <OgreManualObject.h>
#include <OgreSceneManager.h>
#include <OgreSceneNode.h>
#include <OgreMaterialManager.h>
#include <OgreTechnique.h>
#include <OgrePass.h>
// Register WaterPlane component
REGISTER_COMPONENT("Water Plane", WaterPlane, WaterPlaneEditor)
REGISTER_COMPONENT_GROUP("Water Plane", "Water", WaterPlane, WaterPlaneEditor)
{
registry.registerComponent<WaterPlane>(
"Water Plane", "Water", std::make_unique<WaterPlaneEditor>(),
"Water Plane", "Water",
std::make_unique<WaterPlaneEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<WaterPlane>()) {
@@ -31,236 +23,3 @@ REGISTER_COMPONENT("Water Plane", WaterPlane, WaterPlaneEditor)
}
});
}
/**
* WaterPlaneModule
* Manages WaterPlane components and creates visual water surface representations
*/
class WaterPlaneModule {
public:
WaterPlaneModule(flecs::world &world, Ogre::SceneManager *sceneMgr)
: m_world(world)
, m_sceneMgr(sceneMgr)
{
// Register WaterPlane component
world.component<WaterPlane>();
// Create system for updating water planes
world.system<WaterPlane>("UpdateWaterPlanes")
.kind(flecs::OnUpdate)
.each([this](flecs::entity entity,
WaterPlane &waterPlane) {
updateWaterPlane(entity, waterPlane);
});
// Create system for cleaning up water planes when entities are destroyed
world.observer<WaterPlane>("CleanupWaterPlanes")
.event(flecs::OnRemove)
.each([this](flecs::entity entity,
WaterPlane &waterPlane) {
cleanupWaterPlane(waterPlane);
});
Ogre::LogManager::getSingleton().logMessage(
"WaterPlaneModule initialized");
}
~WaterPlaneModule()
{
// Clean up any remaining water planes
m_world.each<WaterPlane>(
[this](flecs::entity entity, WaterPlane &waterPlane) {
cleanupWaterPlane(waterPlane);
});
}
private:
void updateWaterPlane(flecs::entity entity, WaterPlane &waterPlane)
{
// Skip if not enabled
if (!waterPlane.enabled) {
if (waterPlane.sceneNode) {
waterPlane.sceneNode->setVisible(false);
}
return;
}
// Auto-update water surface Y from WaterPhysics if enabled
if (waterPlane.autoUpdateFromWaterPhysics) {
// Find WaterPhysics component in the world
m_world.query<WaterPhysics>().each(
[&](flecs::entity wpEntity,
WaterPhysics &waterPhysics) {
if (waterPhysics.waterSurfaceY !=
waterPlane.waterSurfaceY) {
waterPlane.waterSurfaceY =
waterPhysics
.waterSurfaceY;
waterPlane.markDirty();
}
});
}
// Create or update water plane visualization
if (waterPlane.dirty || !waterPlane.sceneNode) {
createOrUpdateWaterPlane(entity, waterPlane);
waterPlane.dirty = false;
}
// Ensure visibility
if (waterPlane.sceneNode) {
waterPlane.sceneNode->setVisible(true);
}
}
void createOrUpdateWaterPlane(flecs::entity entity,
WaterPlane &waterPlane)
{
// Clean up existing water plane
cleanupWaterPlane(waterPlane);
// Create scene node
Ogre::String nodeName =
"WaterPlaneNode_" +
Ogre::StringConverter::toString(entity.id());
waterPlane.sceneNode =
m_sceneMgr->getRootSceneNode()->createChildSceneNode(
nodeName);
// Create manual object
Ogre::String objName =
"WaterPlaneObject_" +
Ogre::StringConverter::toString(entity.id());
waterPlane.manualObject =
m_sceneMgr->createManualObject(objName);
// Create or get material for water plane
Ogre::String materialName =
"WaterPlaneMaterial_" +
Ogre::StringConverter::toString(entity.id());
Ogre::MaterialPtr material =
Ogre::MaterialManager::getSingleton()
.createOrRetrieve(
materialName,
Ogre::ResourceGroupManager::
DEFAULT_RESOURCE_GROUP_NAME)
.first.staticCast<Ogre::Material>();
// Configure material for transparent water
material->setReceiveShadows(false);
material->getTechnique(0)->getPass(0)->setDiffuse(
waterPlane.color);
material->getTechnique(0)->getPass(0)->setAmbient(
waterPlane.color * 0.5f);
material->getTechnique(0)->getPass(0)->setSelfIllumination(
waterPlane.color * 0.2f);
material->getTechnique(0)->getPass(0)->setSceneBlending(
Ogre::SBT_TRANSPARENT_ALPHA);
material->getTechnique(0)->getPass(0)->setDepthWriteEnabled(
false);
material->getTechnique(0)->getPass(0)->setLightingEnabled(true);
// Create water plane geometry
float halfSize = waterPlane.planeSize * 0.5f;
waterPlane.manualObject->begin(
materialName, Ogre::RenderOperation::OT_TRIANGLE_LIST);
// Create a simple plane (2 triangles)
// Vertex 0: (-halfSize, waterSurfaceY, -halfSize)
waterPlane.manualObject->position(
-halfSize, waterPlane.waterSurfaceY, -halfSize);
waterPlane.manualObject->normal(0.0f, 1.0f, 0.0f);
waterPlane.manualObject->textureCoord(0.0f, 0.0f);
// Vertex 1: (halfSize, waterSurfaceY, -halfSize)
waterPlane.manualObject->position(
halfSize, waterPlane.waterSurfaceY, -halfSize);
waterPlane.manualObject->normal(0.0f, 1.0f, 0.0f);
waterPlane.manualObject->textureCoord(1.0f, 0.0f);
// Vertex 2: (halfSize, waterSurfaceY, halfSize)
waterPlane.manualObject->position(
halfSize, waterPlane.waterSurfaceY, halfSize);
waterPlane.manualObject->normal(0.0f, 1.0f, 0.0f);
waterPlane.manualObject->textureCoord(1.0f, 1.0f);
// Vertex 3: (-halfSize, waterSurfaceY, halfSize)
waterPlane.manualObject->position(
-halfSize, waterPlane.waterSurfaceY, halfSize);
waterPlane.manualObject->normal(0.0f, 1.0f, 0.0f);
waterPlane.manualObject->textureCoord(0.0f, 1.0f);
// First triangle
waterPlane.manualObject->triangle(0, 1, 2);
// Second triangle
waterPlane.manualObject->triangle(0, 2, 3);
waterPlane.manualObject->end();
// Attach to scene node
waterPlane.sceneNode->attachObject(waterPlane.manualObject);
// Log creation
Ogre::LogManager::getSingleton().logMessage(
"Created water plane at Y=" +
Ogre::StringConverter::toString(
waterPlane.waterSurfaceY) +
", size=" +
Ogre::StringConverter::toString(waterPlane.planeSize));
}
void cleanupWaterPlane(WaterPlane &waterPlane)
{
if (waterPlane.manualObject) {
if (waterPlane.sceneNode) {
waterPlane.sceneNode->detachObject(
waterPlane.manualObject);
}
m_sceneMgr->destroyManualObject(
waterPlane.manualObject);
waterPlane.manualObject = nullptr;
}
if (waterPlane.sceneNode) {
m_sceneMgr->destroySceneNode(waterPlane.sceneNode);
waterPlane.sceneNode = nullptr;
}
}
flecs::world &m_world;
Ogre::SceneManager *m_sceneMgr;
};
// Function to initialize WaterPlaneModule
void initializeWaterPlaneModule(flecs::world &world,
Ogre::SceneManager *sceneMgr)
{
static WaterPlaneModule *module = nullptr;
if (!module) {
module = new WaterPlaneModule(world, sceneMgr);
// Create default WaterPlane entity if none exists
bool hasWaterPlane = false;
world.query<WaterPlane>().each(
[&](flecs::entity, WaterPlane &) {
hasWaterPlane = true;
});
if (!hasWaterPlane) {
flecs::entity waterPlaneEntity =
world.entity("WaterPlane");
waterPlaneEntity.set<WaterPlane>({});
waterPlaneEntity.add<EditorMarkerComponent>();
waterPlaneEntity.set<EntityNameComponent>(
EntityNameComponent("Water Plane"));
// Add Transform component for potential future use
waterPlaneEntity.set<TransformComponent>(
TransformComponent());
Ogre::LogManager::getSingleton().logMessage(
"Created default WaterPlane entity");
}
}
}

View File

@@ -0,0 +1,409 @@
#include "Cursor3D.hpp"
#include "../components/Transform.hpp"
#include <cmath>
static const float COLOR_RED[3] = { 1.0f, 0.2f, 0.2f };
static const float COLOR_GREEN[3] = { 0.2f, 1.0f, 0.2f };
static const float COLOR_BLUE[3] = { 0.2f, 0.4f, 1.0f };
static const float COLOR_CYAN[3] = { 0.0f, 1.0f, 1.0f };
static const float COLOR_YELLOW[3] = { 1.0f, 1.0f, 0.0f };
static bool projectRayOntoAxis(const Ogre::Ray &ray,
const Ogre::Vector3 &axisOrigin,
const Ogre::Vector3 &axisDir, float &outT)
{
Ogre::Vector3 rayOrigin = ray.getOrigin();
Ogre::Vector3 rayDir = ray.getDirection();
Ogre::Vector3 w0 = rayOrigin - axisOrigin;
float a = rayDir.dotProduct(rayDir);
float b = rayDir.dotProduct(axisDir);
float c = axisDir.dotProduct(axisDir);
float d = rayDir.dotProduct(w0);
float e = axisDir.dotProduct(w0);
float denom = a * c - b * b;
if (std::abs(denom) < 0.0001f)
return false;
outT = (a * e - b * d) / denom;
return true;
}
Cursor3D::Cursor3D(Ogre::SceneManager *sceneMgr)
: m_sceneMgr(sceneMgr)
, m_cursorNode(nullptr)
, m_axisX(nullptr)
, m_axisY(nullptr)
, m_axisZ(nullptr)
, m_centerMarker(nullptr)
, m_position(Ogre::Vector3::ZERO)
, m_orientation(Ogre::Quaternion::IDENTITY)
, m_size(1.0f)
, m_visible(false)
, m_mode(Mode::Translate)
, m_selectedAxis(Axis::None)
, m_hoveredAxis(Axis::None)
, m_isDragging(false)
, m_dragStartT(0.0f)
, m_dragStartAngle(0.0f)
{
m_cursorNode = m_sceneMgr->getRootSceneNode()->createChildSceneNode(
"Cursor3DNode");
m_axisX = m_sceneMgr->createManualObject("CursorAxisX");
m_axisY = m_sceneMgr->createManualObject("CursorAxisY");
m_axisZ = m_sceneMgr->createManualObject("CursorAxisZ");
m_centerMarker = m_sceneMgr->createManualObject("CursorCenter");
m_cursorNode->attachObject(m_axisX);
m_cursorNode->attachObject(m_axisY);
m_cursorNode->attachObject(m_axisZ);
m_cursorNode->attachObject(m_centerMarker);
m_axisX->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
m_axisY->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
m_axisZ->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
m_centerMarker->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
m_cursorNode->setVisible(false);
createGeometry();
}
void Cursor3D::shutdown()
{
if (!m_sceneMgr)
return;
auto detach = [&](Ogre::ManualObject *obj) {
if (m_cursorNode && obj) {
try {
m_cursorNode->detachObject(obj);
} catch (...) {
}
}
};
auto destroy = [&](Ogre::ManualObject *obj) {
if (obj) {
try {
m_sceneMgr->destroyManualObject(obj);
} catch (...) {
}
}
};
detach(m_axisX);
detach(m_axisY);
detach(m_axisZ);
detach(m_centerMarker);
destroy(m_axisX);
m_axisX = nullptr;
destroy(m_axisY);
m_axisY = nullptr;
destroy(m_axisZ);
m_axisZ = nullptr;
destroy(m_centerMarker);
m_centerMarker = nullptr;
if (m_cursorNode) {
try {
m_sceneMgr->destroySceneNode(m_cursorNode);
} catch (...) {
}
m_cursorNode = nullptr;
}
m_sceneMgr = nullptr;
}
Cursor3D::~Cursor3D()
{
if (m_sceneMgr)
shutdown();
}
void Cursor3D::setPosition(const Ogre::Vector3 &pos)
{
m_position = pos;
updateNodeTransform();
}
void Cursor3D::setOrientation(const Ogre::Quaternion &rot)
{
m_orientation = rot;
updateNodeTransform();
}
void Cursor3D::setVisible(bool visible)
{
m_visible = visible;
if (m_cursorNode)
m_cursorNode->setVisible(visible);
}
bool Cursor3D::isVisible() const
{
return m_visible;
}
void Cursor3D::setSize(float size)
{
m_size = size;
createGeometry();
}
void Cursor3D::snapToTransform(const TransformComponent &transform)
{
if (transform.node) {
m_position = transform.node->_getDerivedPosition();
m_orientation = transform.node->_getDerivedOrientation();
} else {
m_position = transform.position;
m_orientation = transform.rotation;
}
updateNodeTransform();
}
void Cursor3D::applyToTransform(TransformComponent &transform) const
{
if (!transform.node)
return;
Ogre::SceneNode *parent = static_cast<Ogre::SceneNode *>(
transform.node->getParent());
if (parent) {
transform.position = parent->convertWorldToLocalPosition(
m_position);
transform.rotation = parent->convertWorldToLocalOrientation(
m_orientation);
} else {
transform.position = m_position;
transform.rotation = m_orientation;
}
transform.applyToNode();
transform.markChanged();
}
void Cursor3D::updateNodeTransform()
{
if (m_cursorNode) {
m_cursorNode->setPosition(m_position);
m_cursorNode->setOrientation(m_orientation);
}
}
void Cursor3D::createGeometry()
{
float len = 0.5f * m_size;
float half = 0.05f * m_size;
// X Axis
bool xSel = (m_selectedAxis == Axis::X || m_hoveredAxis == Axis::X);
m_axisX->clear();
m_axisX->begin("Ogre/AxisGizmo",
Ogre::RenderOperation::OT_LINE_LIST);
m_axisX->colour(xSel ? COLOR_YELLOW[0] : COLOR_RED[0],
xSel ? COLOR_YELLOW[1] : COLOR_RED[1],
xSel ? COLOR_YELLOW[2] : COLOR_RED[2]);
m_axisX->position(0, 0, 0);
m_axisX->position(len, 0, 0);
m_axisX->end();
// Y Axis
bool ySel = (m_selectedAxis == Axis::Y || m_hoveredAxis == Axis::Y);
m_axisY->clear();
m_axisY->begin("Ogre/AxisGizmo",
Ogre::RenderOperation::OT_LINE_LIST);
m_axisY->colour(ySel ? COLOR_YELLOW[0] : COLOR_GREEN[0],
ySel ? COLOR_YELLOW[1] : COLOR_GREEN[1],
ySel ? COLOR_YELLOW[2] : COLOR_GREEN[2]);
m_axisY->position(0, 0, 0);
m_axisY->position(0, len, 0);
m_axisY->end();
// Z Axis
bool zSel = (m_selectedAxis == Axis::Z || m_hoveredAxis == Axis::Z);
m_axisZ->clear();
m_axisZ->begin("Ogre/AxisGizmo",
Ogre::RenderOperation::OT_LINE_LIST);
m_axisZ->colour(zSel ? COLOR_YELLOW[0] : COLOR_BLUE[0],
zSel ? COLOR_YELLOW[1] : COLOR_BLUE[1],
zSel ? COLOR_YELLOW[2] : COLOR_BLUE[2]);
m_axisZ->position(0, 0, 0);
m_axisZ->position(0, 0, len);
m_axisZ->end();
// Center marker
m_centerMarker->clear();
m_centerMarker->begin("Ogre/AxisGizmo",
Ogre::RenderOperation::OT_LINE_LIST);
m_centerMarker->colour(COLOR_CYAN[0], COLOR_CYAN[1],
COLOR_CYAN[2]);
Ogre::Vector3 c[8] = {
Ogre::Vector3(-half, -half, -half),
Ogre::Vector3(half, -half, -half),
Ogre::Vector3(half, half, -half),
Ogre::Vector3(-half, half, -half),
Ogre::Vector3(-half, -half, half),
Ogre::Vector3(half, -half, half),
Ogre::Vector3(half, half, half),
Ogre::Vector3(-half, half, half),
};
auto line = [&](int a, int b) {
m_centerMarker->position(c[a]);
m_centerMarker->position(c[b]);
};
line(0, 1);
line(1, 2);
line(2, 3);
line(3, 0);
line(4, 5);
line(5, 6);
line(6, 7);
line(7, 4);
line(0, 4);
line(1, 5);
line(2, 6);
line(3, 7);
m_centerMarker->end();
}
Cursor3D::Axis Cursor3D::hitTest(const Ogre::Ray &mouseRay)
{
if (!m_cursorNode || !m_axisX->isVisible())
return Axis::None;
Ogre::Vector3 cursorPos = m_cursorNode->getPosition();
float len = 0.5f * m_size;
float threshold = 0.15f * m_size;
float bestDist = 1000000.0f;
Axis bestAxis = Axis::None;
for (int i = 0; i < 3; ++i) {
Ogre::Vector3 axisDir;
if (i == 0)
axisDir = m_cursorNode->getOrientation() *
Ogre::Vector3::UNIT_X;
else if (i == 1)
axisDir = m_cursorNode->getOrientation() *
Ogre::Vector3::UNIT_Y;
else
axisDir = m_cursorNode->getOrientation() *
Ogre::Vector3::UNIT_Z;
for (int j = 0; j <= 8; ++j) {
float t = len * j / 8.0f;
Ogre::Vector3 pointOnAxis = cursorPos + axisDir * t;
Ogre::Vector3 L = pointOnAxis - mouseRay.getOrigin();
float tca = L.dotProduct(mouseRay.getDirection());
if (tca < 0.01f)
continue;
float d2 = L.dotProduct(L) - tca * tca;
if (d2 <= threshold * threshold) {
if (tca < bestDist) {
bestDist = tca;
bestAxis = static_cast<Axis>(i + 1);
}
}
}
}
return bestAxis;
}
bool Cursor3D::onMousePressed(const Ogre::Ray &mouseRay,
const Ogre::Camera *camera)
{
(void)camera;
if (!m_axisX->isVisible())
return false;
m_selectedAxis = hitTest(mouseRay);
if (m_selectedAxis != Axis::None) {
m_isDragging = true;
m_dragStartPosition = m_position;
m_dragStartRotation = m_orientation;
Ogre::Vector3 axisDir;
switch (m_selectedAxis) {
case Axis::X:
axisDir = m_orientation * Ogre::Vector3::UNIT_X;
break;
case Axis::Y:
axisDir = m_orientation * Ogre::Vector3::UNIT_Y;
break;
case Axis::Z:
axisDir = m_orientation * Ogre::Vector3::UNIT_Z;
break;
default:
axisDir = Ogre::Vector3::UNIT_X;
break;
}
m_dragAxisDir = axisDir;
if (m_mode == Mode::Translate) {
projectRayOntoAxis(mouseRay, m_dragStartPosition,
m_dragAxisDir, m_dragStartT);
} else if (m_mode == Mode::Rotate) {
(void)camera;
m_dragStartAngle = 0.0f;
}
createGeometry();
return true;
}
return false;
}
bool Cursor3D::onMouseMoved(const Ogre::Ray &mouseRay,
const Ogre::Vector2 &mouseDelta)
{
if (m_isDragging && m_selectedAxis != Axis::None) {
if (m_mode == Mode::Translate) {
float currentT;
if (!projectRayOntoAxis(mouseRay, m_dragStartPosition,
m_dragAxisDir, currentT)) {
return true;
}
float deltaT = currentT - m_dragStartT;
Ogre::Vector3 newPos =
m_dragStartPosition + m_dragAxisDir * deltaT;
setPosition(newPos);
return true;
} else if (m_mode == Mode::Rotate) {
// Simple axis-based rotation from mouse delta
float deltaAngle = 0.0f;
if (m_selectedAxis == Axis::X) {
// Up/down rotates around X
deltaAngle = mouseDelta.y * 0.5f;
} else if (m_selectedAxis == Axis::Y) {
// Left/right rotates around Y
deltaAngle = -mouseDelta.x * 0.5f;
} else if (m_selectedAxis == Axis::Z) {
// Left/right rotates around Z
deltaAngle = -mouseDelta.x * 0.5f;
}
m_dragStartAngle += deltaAngle;
Ogre::Quaternion rot(
Ogre::Degree(m_dragStartAngle),
m_dragAxisDir);
Ogre::Quaternion newRot = rot * m_dragStartRotation;
setOrientation(newRot);
return true;
}
} else if (m_axisX->isVisible()) {
Axis prevHover = m_hoveredAxis;
m_hoveredAxis = hitTest(mouseRay);
if (prevHover != m_hoveredAxis)
createGeometry();
}
return false;
}
bool Cursor3D::onMouseReleased()
{
if (m_isDragging) {
m_isDragging = false;
m_selectedAxis = Axis::None;
m_hoveredAxis = Axis::None;
createGeometry();
return true;
}
return false;
}

View File

@@ -0,0 +1,96 @@
#ifndef EDITSCENE_CURSOR3D_HPP
#define EDITSCENE_CURSOR3D_HPP
#pragma once
#include <Ogre.h>
#include <OgreManualObject.h>
// Forward declarations
struct TransformComponent;
/**
* 3D Cursor - a visual marker for prefab placement and transform reference.
* Supports axis-based translation and rotation interaction like the gizmo.
*/
class Cursor3D {
public:
enum class Mode {
Translate,
Rotate
};
enum class Axis {
None,
X,
Y,
Z
};
Cursor3D(Ogre::SceneManager *sceneMgr);
~Cursor3D();
void shutdown();
void setPosition(const Ogre::Vector3 &pos);
Ogre::Vector3 getPosition() const { return m_position; }
void setOrientation(const Ogre::Quaternion &rot);
Ogre::Quaternion getOrientation() const { return m_orientation; }
void setVisible(bool visible);
bool isVisible() const;
void setSize(float size);
float getSize() const { return m_size; }
void setMode(Mode mode) { m_mode = mode; }
Mode getMode() const { return m_mode; }
void snapToTransform(const TransformComponent &transform);
void applyToTransform(TransformComponent &transform) const;
/**
* Handle mouse input for cursor interaction.
* Returns true if cursor handled the input.
*/
bool onMousePressed(const Ogre::Ray &mouseRay,
const Ogre::Camera *camera);
bool onMouseMoved(const Ogre::Ray &mouseRay,
const Ogre::Vector2 &mouseDelta);
bool onMouseReleased();
bool isDragging() const { return m_isDragging; }
Axis getSelectedAxis() const { return m_selectedAxis; }
private:
void createGeometry();
void updateNodeTransform();
Axis hitTest(const Ogre::Ray &mouseRay);
Ogre::SceneManager *m_sceneMgr;
Ogre::SceneNode *m_cursorNode;
Ogre::ManualObject *m_axisX;
Ogre::ManualObject *m_axisY;
Ogre::ManualObject *m_axisZ;
Ogre::ManualObject *m_centerMarker;
Ogre::Vector3 m_position;
Ogre::Quaternion m_orientation;
float m_size;
bool m_visible;
Mode m_mode;
Axis m_selectedAxis;
Axis m_hoveredAxis;
bool m_isDragging;
// Drag state
Ogre::Vector3 m_dragStartPosition;
Ogre::Quaternion m_dragStartRotation;
Ogre::Vector3 m_dragAxisDir;
float m_dragStartT;
float m_dragStartAngle;
Ogre::Vector2 m_dragScreenAxis;
};
#endif // EDITSCENE_CURSOR3D_HPP

View File

@@ -0,0 +1,418 @@
-- =============================================================================
-- Action Database Lua API Examples
-- =============================================================================
-- This file demonstrates how to create, query, and manage GOAP actions and
-- goals from Lua using the ecs.action_db API.
--
-- The ActionDatabase is a global singleton. Actions and goals defined here
-- are immediately available to all characters in the scene.
-- =============================================================================
-- =============================================================================
-- Defining Bit Names
-- =============================================================================
-- Before using bits in preconditions/effects, you should define meaningful
-- names for the 64 available bit slots. This makes your actions readable.
--
-- Bit names are global across the entire game session. They map
-- human-readable names (like "has_axe", "is_hungry") to bit indices
-- (0-63) used in GoapBlackboard preconditions and effects.
--
-- You can define bits explicitly at startup:
-- =============================================================================
-- Explicitly assign bit names to specific indices:
ecs.action_db.set_bit_name(0, "has_axe")
ecs.action_db.set_bit_name(1, "has_wood")
ecs.action_db.set_bit_name(2, "is_hungry")
ecs.action_db.set_bit_name(3, "near_tree")
ecs.action_db.set_bit_name(4, "near_well")
ecs.action_db.set_bit_name(5, "has_bucket")
ecs.action_db.set_bit_name(6, "has_food")
ecs.action_db.set_bit_name(7, "near_fire")
ecs.action_db.set_bit_name(8, "has_cooked_food")
ecs.action_db.set_bit_name(9, "is_awake")
ecs.action_db.set_bit_name(10, "at_market")
ecs.action_db.set_bit_name(11, "at_home")
ecs.action_db.set_bit_name(12, "near_chair")
ecs.action_db.set_bit_name(13, "is_sitting")
ecs.action_db.set_bit_name(14, "near_forest")
ecs.action_db.set_bit_name(15, "is_strong")
ecs.action_db.set_bit_name(16, "has_strength")
-- Or use auto_assign_bit() to let the system pick the index:
local idx = ecs.action_db.auto_assign_bit("has_water")
print("'has_water' assigned to bit " .. idx)
-- Look up a bit by name:
local bit_idx = ecs.action_db.find_bit_by_name("has_axe")
print("'has_axe' is at bit " .. bit_idx)
-- Get the name for a bit index:
local name = ecs.action_db.get_bit_name(0)
print("Bit 0 is named '" .. name .. "'")
-- List all currently assigned bit names:
local bits = ecs.action_db.list_bit_names()
print("Assigned bit names:")
for _, b in ipairs(bits) do
print(" Bit " .. b.index .. ": " .. b.name)
end
-- NOTE: If you use a bit name in an action's preconditions/effects
-- that hasn't been explicitly assigned, it will be auto-assigned
-- to the first free slot automatically. So you don't HAVE to
-- pre-define them, but it's good practice for clarity.
-- =============================================================================
-- Creating Actions
-- =============================================================================
-- Simple action with just a name and cost:
ecs.action_db.add_action("idle", 1)
-- Action with preconditions (what must be true before the action can run):
ecs.action_db.add_action("chop_wood", 2,
{
bits = { has_axe = true },
values = { stamina = 10 }
},
{ -- effects (what becomes true after the action runs)
bits = { has_wood = true },
values = { stamina = -5 }
}
)
-- Action with only preconditions, no effects:
ecs.action_db.add_action("fetch_water", 3,
{
bits = { near_well = true, has_bucket = true },
values = { thirst = 50 }
}
)
-- Action with only effects, no preconditions:
ecs.action_db.add_action("rest", 1,
{},
{
values = { stamina = 100, energy = 100 }
}
)
-- Action with float and string values in blackboard:
ecs.action_db.add_action("cook_food", 4,
{
bits = { has_food = true, near_fire = true },
values = { cooking_skill = 3 },
floatValues = { hunger = 50.0 }
},
{
bits = { has_cooked_food = true },
values = { hunger = -30 },
stringValues = { last_action = "cooking" }
}
)
-- =============================================================================
-- Creating Actions WITH Behavior Trees
-- =============================================================================
-- Action with a simple leaf behavior tree (plays an animation):
ecs.action_db.add_action("wave", 1,
{}, -- no preconditions
{}, -- no effects
{ -- behavior tree (arg 5)
type = "sequence",
children = {
{ type = "setAnimationState", name = "SM/Wave" },
{ type = "delay", params = "2.0" },
{ type = "setAnimationState", name = "SM/Idle" }
}
}
)
-- Action with a selector behavior tree (try to chop, fall back to idle):
ecs.action_db.add_action("chop_tree", 3,
{
bits = { near_tree = true },
values = { stamina = 15 }
},
{
bits = { has_wood = true },
values = { stamina = -8, wood_count = 1 }
},
{
type = "selector",
children = {
{
type = "sequence",
children = {
{ type = "checkBit", name = "has_axe", params = "1" },
{ type = "setAnimationState", name = "SM/Chop" },
{ type = "delay", params = "3.0" },
{ type = "setAnimationState", name = "SM/Idle" },
{ type = "setBit", name = "has_wood", params = "1" }
}
},
{
type = "sequence",
children = {
{ type = "debugPrint", name = "No axe! Picking up stick..." },
{ type = "setAnimationState", name = "SM/Pickup" },
{ type = "delay", params = "1.0" },
{ type = "setBit", name = "has_wood", params = "1" }
}
}
}
}
)
-- Action with blackboard checks and value manipulation:
ecs.action_db.add_action("travel_to_market", 5,
{
bits = { is_awake = true },
values = { energy = 20 }
},
{
bits = { at_market = true },
values = { energy = -15 }
},
{
type = "sequence",
children = {
{ type = "checkValue", name = "energy", params = ">= 20" },
{ type = "setAnimationState", name = "SM/Walk" },
{ type = "delay", params = "5.0" },
{ type = "setAnimationState", name = "SM/Idle" },
{ type = "setBit", name = "at_market", params = "1" },
{ type = "setBit", name = "at_home", params = "0" }
}
}
)
-- Action with inventory operations:
ecs.action_db.add_action("gather_wood", 2,
{
bits = { near_forest = true }
},
{
values = { wood_count = 3 }
},
{
type = "sequence",
children = {
{ type = "setAnimationState", name = "SM/Gather" },
{ type = "delay", params = "2.0" },
{ type = "addItemToInventory", params = "wood,Firewood,material,3,1.0,0" },
{ type = "setAnimationState", name = "SM/Idle" }
}
}
)
-- Action that teleports character to a smart object child:
ecs.action_db.add_action("sit_on_chair", 1,
{
bits = { near_chair = true }
},
{
bits = { is_sitting = true }
},
{
type = "sequence",
children = {
{ type = "teleportToChild", name = "SitTarget" },
{ type = "disablePhysics" },
{ type = "setAnimationState", name = "SM/Sit" },
{ type = "delay", params = "5.0" },
{ type = "setAnimationState", name = "SM/Stand" },
{ type = "enablePhysics" }
}
}
)
-- =============================================================================
-- Creating Goals
-- =============================================================================
-- Simple goal with just a name and priority:
ecs.action_db.add_goal("survive", 100)
-- Goal with a target blackboard state:
ecs.action_db.add_goal("gather_resources", 50,
{
bits = { has_wood = true, has_water = true },
values = { wood_count = 5, water_count = 3 }
}
)
-- Goal with a condition expression (evaluated against character's blackboard):
ecs.action_db.add_goal("stay_healthy", 80,
{
values = { health = 100, stamina = 80 }
},
"health < 50 || stamina < 30" -- only valid when character needs healing
)
-- Goal with full specification:
ecs.action_db.add_goal("become_strong", 30,
{
bits = { is_strong = true },
values = { strength = 100 }
},
"strength < 100" -- only valid if not already strong
)
-- =============================================================================
-- Querying Actions and Goals
-- =============================================================================
-- Find an action by name:
local action = ecs.action_db.find_action("chop_wood")
if action then
print("Found action: " .. action.name .. " (cost: " .. action.cost .. ")")
-- action.preconditions and action.effects are tables with:
-- .bits - table of boolean flags
-- .values - table of integer values
-- .floatValues - table of float values
-- .stringValues - table of string values
-- action.behaviorTree is a table with:
-- .type - node type string
-- .name - optional name
-- .params - optional params
-- .children - optional array of child nodes
end
-- Find a goal by name:
local goal = ecs.action_db.find_goal("gather_resources")
if goal then
print("Found goal: " .. goal.name .. " (priority: " .. goal.priority .. ")")
-- goal.target is a blackboard table
-- goal.condition is the condition string
end
-- =============================================================================
-- Listing All Actions and Goals
-- =============================================================================
-- List all action names:
local actions = ecs.action_db.list_actions()
print("Available actions:")
for i, name in ipairs(actions) do
print(" " .. i .. ". " .. name)
end
-- List all goal names:
local goals = ecs.action_db.list_goals()
print("Available goals:")
for i, name in ipairs(goals) do
print(" " .. i .. ". " .. name)
end
-- =============================================================================
-- Removing Actions and Goals
-- =============================================================================
-- Remove an action by name:
local removed = ecs.action_db.remove_action("idle")
if removed then
print("Removed action: idle")
end
-- Remove a goal by name:
ecs.action_db.remove_goal("become_strong")
-- =============================================================================
-- Replacing Actions (same name = replace)
-- =============================================================================
-- If you add an action with the same name as an existing one, it replaces it:
ecs.action_db.add_action("chop_wood", 5,
{
bits = { has_axe = true, has_strength = true },
values = { stamina = 20 }
},
{
bits = { has_wood = true },
values = { stamina = -10, wood_count = 2 }
}
)
-- The old "chop_wood" action is replaced with this new definition.
-- =============================================================================
-- Clearing Everything
-- =============================================================================
-- Remove all actions and goals:
-- ecs.action_db.clear()
-- =============================================================================
-- Blackboard Table Format Reference
-- =============================================================================
--
-- The blackboard table passed to add_action/add_goal has this structure:
--
-- {
-- bits = {
-- has_axe = true, -- boolean flags (use named bits)
-- has_wood = false,
-- is_hungry = true
-- },
-- values = { -- integer values
-- health = 100,
-- stamina = 50,
-- wood_count = 0
-- },
-- floatValues = { -- float values
-- hunger = 75.5,
-- speed = 1.2
-- },
-- stringValues = { -- string values
-- last_action = "idle",
-- current_state = "exploring"
-- }
-- }
--
-- All sub-tables are optional. An empty table or nil means no constraints.
-- =============================================================================
-- =============================================================================
-- Behavior Tree Table Format Reference
-- =============================================================================
--
-- The behaviorTree table (arg 5 of add_action) has this structure:
--
-- {
-- type = "sequence", -- node type (required)
-- name = "optional_name", -- depends on type (task name, anim state, etc.)
-- params = "optional_params", -- extra parameters (delay seconds, bit index, etc.)
-- children = { -- array of child nodes (for sequence/selector/invert)
-- { type = "task", name = "myAction" },
-- { type = "setAnimationState", name = "SM/Walk" },
-- { type = "delay", params = "2.0" },
-- { type = "checkBit", name = "has_axe", params = "1" }
-- }
-- }
--
-- Common node types:
-- "sequence" - Execute children in order until one fails
-- "selector" - Execute children in order until one succeeds
-- "invert" - Invert the result of a single child
-- "task" - Leaf: references a named task
-- "check" - Leaf: references a named condition
-- "debugPrint" - Leaf: prints 'name' to console
-- "setAnimationState"- Leaf: sets animation state (name="SM/State")
-- "isAnimationEnded" - Leaf: true if animation ended
-- "setBit" - Leaf: sets blackboard bit (name=bit, params=0/1)
-- "checkBit" - Leaf: true if blackboard bit is set
-- "setValue" - Leaf: sets blackboard value (name=key, params=val)
-- "checkValue" - Leaf: blackboard comparison (name=key, params="op val")
-- "delay" - Leaf: waits N seconds (params=seconds as float)
-- "teleportToChild" - Leaf: teleports to named child of Smart Object
-- "disablePhysics" - Leaf: removes character from physics
-- "enablePhysics" - Leaf: re-adds character to physics
-- "hasItem" - Leaf check: true if inventory has itemId
-- "pickupItem" - Leaf: picks up nearest item
-- "dropItem" - Leaf: drops item from inventory
-- "useItem" - Leaf: uses item from inventory
-- "addItemToInventory"- Leaf: adds item directly to inventory
-- =============================================================================

View File

@@ -0,0 +1,514 @@
-- =============================================================================
-- Behavior Tree Lua API Examples
-- =============================================================================
-- This file demonstrates how to create custom behavior tree nodes using
-- Lua functions via the ecs.behavior_tree API, and how to use the
-- built-in C++ node types via ecs.behavior_tree.create_node().
--
-- The API allows you to:
-- 1. Register Lua functions as behavior tree node handlers
-- 2. Create behavior tree nodes (both Lua and built-in C++ types)
-- 3. Return "success", "failure", or "running" to control tree flow
-- 4. Pass parameters from the behavior tree editor to your Lua function
-- =============================================================================
-- =============================================================================
-- Registering Lua Node Handlers
-- =============================================================================
-- Use ecs.behavior_tree.register_node(name, function) to register a Lua
-- function as a behavior tree node handler.
--
-- The function receives two arguments:
-- entity_id - The entity ID executing this behavior tree node
-- params - A table of parameters parsed from the node's params string
--
-- The function must return one of:
-- "success" - Node completed successfully (tree continues)
-- "failure" - Node failed (tree stops with failure)
-- "running" - Node is still running (will be called again next frame)
-- =============================================================================
-- =============================================================================
-- Example 1: Simple greeting node
-- =============================================================================
-- Prints a message and succeeds immediately.
ecs.behavior_tree.register_node("say_hello", function(entity_id, params)
local message = params.message or "Hello!"
print("Entity " .. entity_id .. " says: " .. message)
return "success"
end)
-- =============================================================================
-- Example 2: Node that checks a blackboard value
-- =============================================================================
-- Checks if a blackboard integer value meets a minimum threshold.
-- Succeeds if value >= min, fails otherwise.
ecs.behavior_tree.register_node("check_blackboard_value", function(entity_id, params)
local key = params.key
local min_val = tonumber(params.min) or 0
if not key then
return "failure"
end
local bb = ecs.get_component(entity_id, "GoapBlackboard")
if not bb then
return "failure"
end
local value = bb.values[key]
if value == nil then
return "failure"
end
if value >= min_val then
return "success"
else
return "failure"
end
end)
-- =============================================================================
-- Example 3: Node that runs over multiple frames (running state)
-- =============================================================================
-- Waits for a specified duration, storing progress in the blackboard's
-- floatValues. Returns "running" each frame until the duration elapses.
ecs.behavior_tree.register_node("wait_for_duration", function(entity_id, params)
local duration = tonumber(params.duration) or 1.0
local timer_key = params.timer_key or "wait_timer"
local bb = ecs.get_component(entity_id, "GoapBlackboard")
if not bb then
return "failure"
end
local elapsed = bb.floatValues[timer_key] or 0.0
local dt = ecs.get_delta_time() or 0.016
elapsed = elapsed + dt
bb.floatValues[timer_key] = elapsed
ecs.set_component(entity_id, "GoapBlackboard", bb)
if elapsed >= duration then
bb.floatValues[timer_key] = nil
ecs.set_component(entity_id, "GoapBlackboard", bb)
return "success"
end
return "running"
end)
-- =============================================================================
-- Example 4: Node that modifies blackboard values
-- =============================================================================
-- Adds a configurable amount to a blackboard integer value.
ecs.behavior_tree.register_node("add_blackboard_value", function(entity_id, params)
local key = params.key
local amount = tonumber(params.amount) or 1
if not key then
return "failure"
end
local bb = ecs.get_component(entity_id, "GoapBlackboard")
if not bb then
return "failure"
end
bb.values[key] = (bb.values[key] or 0) + amount
ecs.set_component(entity_id, "GoapBlackboard", bb)
return "success"
end)
-- =============================================================================
-- Example 5: Node that sets a blackboard bit
-- =============================================================================
-- Sets or clears a named bit in the blackboard.
ecs.behavior_tree.register_node("set_blackboard_bit", function(entity_id, params)
local bit_name = params.bit
local value = params.value == "1" or params.value == "true"
if not bit_name then
return "failure"
end
local bb = ecs.get_component(entity_id, "GoapBlackboard")
if not bb then
return "failure"
end
bb.bits[bit_name] = value
ecs.set_component(entity_id, "GoapBlackboard", bb)
return "success"
end)
-- =============================================================================
-- Example 6: Node that checks a blackboard bit
-- =============================================================================
-- Checks if a named bit is set to a specific value.
ecs.behavior_tree.register_node("check_blackboard_bit", function(entity_id, params)
local bit_name = params.bit
local expected = params.value ~= "0" and params.value ~= "false"
if not bit_name then
return "failure"
end
local bb = ecs.get_component(entity_id, "GoapBlackboard")
if not bb then
return "failure"
end
local actual = bb.bits[bit_name] == true
if actual == expected then
return "success"
else
return "failure"
end
end)
-- =============================================================================
-- Example 7: Random chance node
-- =============================================================================
-- Succeeds with a configurable probability (0.0 to 1.0).
ecs.behavior_tree.register_node("random_chance", function(entity_id, params)
local probability = tonumber(params.probability) or 0.5
local roll = math.random()
if roll < probability then
return "success"
else
return "failure"
end
end)
-- =============================================================================
-- Using Built-in Node Types via create_node()
-- =============================================================================
-- ecs.behavior_tree.create_node(type, name, params) creates a node table
-- for any built-in C++ node type. If the first argument matches a registered
-- Lua handler name, it creates a luaTask node instead.
--
-- Built-in node types:
-- Control: sequence, selector, invert
-- Animation: setAnimationState, isAnimationEnded
-- Blackboard: setBit, checkBit, setValue, checkValue, blackboardDump
-- Timing: delay
-- Movement: teleportToChild
-- Physics: disablePhysics, enablePhysics
-- Events: sendEvent
-- Inventory: hasItem, hasItemByName, countItem, pickupItem, dropItem,
-- useItem, addItemToInventory
-- Debug: debugPrint
-- Lua: luaTask (auto-detected when name matches a registered handler)
-- =============================================================================
-- =============================================================================
-- Example 8: Action using built-in animation nodes
-- =============================================================================
-- Uses setAnimationState and isAnimationEnded to play an animation and
-- wait for it to finish.
ecs.action_db.add_action("play_walk_animation", 1,
{}, -- preconditions
{}, -- effects
{ -- behavior tree
type = "sequence",
children = {
ecs.behavior_tree.create_node("setAnimationState", "locomotion/walk"),
ecs.behavior_tree.create_node("isAnimationEnded", "locomotion"),
ecs.behavior_tree.create_node("setAnimationState", "locomotion/idle")
}
}
)
-- =============================================================================
-- Example 9: Action using built-in delay node
-- =============================================================================
-- Waits for a specified duration using the built-in delay node.
ecs.action_db.add_action("wait_and_greet", 1,
{},
{},
{
type = "sequence",
children = {
ecs.behavior_tree.create_node("delay", "", "2.0"),
ecs.behavior_tree.create_node("say_hello", "message=Waited 2 seconds!")
}
}
)
-- =============================================================================
-- Example 10: Action using built-in blackboard nodes
-- =============================================================================
-- Sets a blackboard bit, checks it, and sets a value.
ecs.action_db.add_action("blackboard_demo", 1,
{},
{
bits = { has_sword = true },
values = { gold = 100 }
},
{
type = "sequence",
children = {
ecs.behavior_tree.create_node("setBit", "has_sword", "1"),
ecs.behavior_tree.create_node("checkBit", "has_sword"),
ecs.behavior_tree.create_node("setValue", "gold", "100"),
ecs.behavior_tree.create_node("checkValue", "gold", ">= 50"),
ecs.behavior_tree.create_node("blackboardDump", "After setup")
}
}
)
-- =============================================================================
-- Example 11: Action using built-in sendEvent node
-- =============================================================================
-- Sends an event with parameters.
ecs.action_db.add_action("trigger_quest_event", 1,
{},
{},
{
type = "sequence",
children = {
ecs.behavior_tree.create_node("sendEvent", "quest_started",
"quest_id=the_ancient_sword,quest_giver=elder"),
ecs.behavior_tree.create_node("debugPrint", "Quest event sent")
}
}
)
-- =============================================================================
-- Example 12: Action using built-in physics nodes
-- =============================================================================
-- Disables physics, waits, then re-enables.
ecs.action_db.add_action("physics_control", 1,
{},
{},
{
type = "sequence",
children = {
ecs.behavior_tree.create_node("disablePhysics"),
ecs.behavior_tree.create_node("delay", "", "1.0"),
ecs.behavior_tree.create_node("enablePhysics")
}
}
)
-- =============================================================================
-- Example 13: Action using built-in inventory nodes
-- =============================================================================
-- Adds an item to inventory, checks for it, uses it, then drops it.
ecs.action_db.add_action("inventory_demo", 1,
{},
{},
{
type = "sequence",
children = {
ecs.behavior_tree.create_node("addItemToInventory", "potion_01",
"Health Potion,misc,1,0.5,10"),
ecs.behavior_tree.create_node("hasItem", "potion_01"),
ecs.behavior_tree.create_node("countItem", "potion_01", "1"),
ecs.behavior_tree.create_node("useItem", "potion_01"),
ecs.behavior_tree.create_node("dropItem", "potion_01", "1")
}
}
)
-- =============================================================================
-- Example 14: Action using built-in teleport node
-- =============================================================================
-- Teleports the entity to a named child transform.
ecs.action_db.add_action("teleport_to_entrance", 1,
{},
{},
{
type = "sequence",
children = {
ecs.behavior_tree.create_node("teleportToChild", "entrance"),
ecs.behavior_tree.create_node("debugPrint", "Teleported to entrance")
}
}
)
-- =============================================================================
-- Example 15: Complex action mixing Lua and built-in nodes
-- =============================================================================
-- A selector that first tries to use a sword, and if not available,
-- picks one up and equips it.
ecs.action_db.add_action("equip_sword", 2,
{},
{
bits = { has_sword = true }
},
{
type = "selector",
children = {
-- Try to use existing sword
{
type = "sequence",
children = {
ecs.behavior_tree.create_node("checkBit", "has_sword"),
ecs.behavior_tree.create_node("debugPrint", "Already have a sword")
}
},
-- Pick up and equip a sword
{
type = "sequence",
children = {
ecs.behavior_tree.create_node("setAnimationState", "locomotion/walk"),
ecs.behavior_tree.create_node("delay", "", "3.0"),
ecs.behavior_tree.create_node("addItemToInventory", "sword_01",
"Iron Sword,weapon,1,2.5,50"),
ecs.behavior_tree.create_node("setBit", "has_sword", "1"),
ecs.behavior_tree.create_node("setAnimationState", "locomotion/idle"),
ecs.behavior_tree.create_node("debugPrint", "Picked up the sword!")
}
}
}
}
)
-- =============================================================================
-- Example 16: Action with conditional logic using Lua nodes
-- =============================================================================
-- Uses Lua-registered nodes for blackboard checks and modifications.
ecs.action_db.add_action("gain_experience", 2,
{
values = { experience = 0 }
},
{
values = { experience = 15 }
},
{
type = "selector",
children = {
{
type = "sequence",
children = {
ecs.behavior_tree.create_node("check_blackboard_value",
"key=experience,min=50"),
ecs.behavior_tree.create_node("say_hello",
"message=You are experienced!"),
ecs.behavior_tree.create_node("add_blackboard_value",
"key=experience,amount=10")
}
},
{
type = "sequence",
children = {
ecs.behavior_tree.create_node("say_hello",
"message=You are still learning..."),
ecs.behavior_tree.create_node("add_blackboard_value",
"key=experience,amount=5")
}
}
}
}
)
-- =============================================================================
-- Example 17: Action with random outcomes
-- =============================================================================
-- Uses the Lua random_chance node for probabilistic behavior.
ecs.action_db.add_action("try_gamble", 3,
{},
{
values = { gold = 10 }
},
{
type = "sequence",
children = {
ecs.behavior_tree.create_node("random_chance", "probability=0.3"),
ecs.behavior_tree.create_node("say_hello", "message=You won the gamble!"),
ecs.behavior_tree.create_node("add_blackboard_value", "key=gold,amount=10")
}
}
)
-- =============================================================================
-- Example 18: Action with running-state Lua node
-- =============================================================================
-- Uses the wait_for_duration Lua node which returns "running" each frame.
ecs.action_db.add_action("wait_and_continue", 1,
{},
{},
{
type = "sequence",
children = {
ecs.behavior_tree.create_node("wait_for_duration",
"duration=2.5,timer_key=my_timer"),
ecs.behavior_tree.create_node("say_hello", "message=Done waiting!")
}
}
)
-- =============================================================================
-- Example 19: Action with blackboard bit operations via Lua nodes
-- =============================================================================
ecs.action_db.add_action("toggle_flag", 1,
{},
{
bits = { quest_complete = true }
},
{
type = "sequence",
children = {
ecs.behavior_tree.create_node("set_blackboard_bit",
"bit=quest_complete,value=1"),
ecs.behavior_tree.create_node("check_blackboard_bit",
"bit=quest_complete,value=1"),
ecs.behavior_tree.create_node("debugPrint", "Quest flag set and verified")
}
}
)
-- =============================================================================
-- Managing Registered Nodes
-- =============================================================================
-- List all registered node handlers:
local nodes = ecs.behavior_tree.list_nodes()
print("Registered behavior tree nodes:")
for i, name in ipairs(nodes) do
print(" " .. i .. ". " .. name)
end
-- Unregister a node handler:
-- local removed = ecs.behavior_tree.unregister_node("say_hello")
-- if removed then
-- print("Unregistered node: say_hello")
-- end
-- =============================================================================
-- Parameter Format Reference
-- =============================================================================
--
-- The params string passed to create_node() supports:
--
-- Integer: "count=42" -> params.count = 42 (number)
-- Float: "speed=3.5" -> params.speed = 3.5 (number)
-- String: "msg=hello" -> params.msg = "hello" (string)
-- Quoted: 'msg="hello world"' -> params.msg = "hello world" (string)
-- Multiple: "key=val,count=5" -> params.key = "val", params.count = 5
-- Empty: "" -> params = {} (empty table)
--
-- The params table is passed as the second argument to your registered
-- Lua function handler.
-- =============================================================================

View File

@@ -0,0 +1,711 @@
-- =============================================================================
-- Component Lua API Examples
-- =============================================================================
-- This file demonstrates how to add, remove, query, and manipulate ECS
-- components from Lua using the ecs.* Lua API.
--
-- Components are data attached to entities. They define what an entity IS
-- and what it CAN DO. For example, a Transform component gives an entity
-- a position in the world, while a Renderable component makes it visible.
-- =============================================================================
-- =============================================================================
-- Checking for Components
-- =============================================================================
-- Create an entity and check what components it has:
local entity = ecs.create_entity()
ecs.set_entity_name(entity, "TestObject")
-- Check if an entity has a specific component:
if ecs.has_component(entity, "Transform") then
print("Entity has Transform component")
end
-- New entities typically have EditorMarker by default:
if ecs.has_component(entity, "EditorMarker") then
print("Entity has EditorMarker (default for new entities)")
end
-- =============================================================================
-- Adding and Removing Components
-- =============================================================================
-- Add a tag component (a component with no data):
ecs.add_component(entity, "InWater")
print("Added InWater tag")
-- Check it was added:
if ecs.has_component(entity, "InWater") then
print("Entity is now in water")
end
-- Remove a tag component:
ecs.remove_component(entity, "InWater")
if not ecs.has_component(entity, "InWater") then
print("Entity is no longer in water")
end
-- =============================================================================
-- Setting and Getting Components with Data
-- =============================================================================
-- Set a component with data (creates or replaces it):
ecs.set_component(entity, "Transform", {
position = { 10, 20, 30 },
rotation = { 1, 0, 0, 0 }, -- quaternion w, x, y, z
scale = { 2, 2, 2 }
})
-- Get the component back:
local transform = ecs.get_component(entity, "Transform")
if transform then
print("Position: " .. transform.position[1] .. ", "
.. transform.position[2] .. ", "
.. transform.position[3])
print("Scale: " .. transform.scale[1] .. ", "
.. transform.scale[2] .. ", "
.. transform.scale[3])
end
-- =============================================================================
-- Individual Field Access
-- =============================================================================
-- Get a single field from a component:
local pos_x = ecs.get_field(entity, "Transform", "position")
print("Position X from get_field: " .. pos_x[1])
-- Set a single field:
ecs.set_field(entity, "Transform", "position", { 0, 0, 0 })
local new_pos = ecs.get_field(entity, "Transform", "position")
print("New position: " .. new_pos[1] .. ", " .. new_pos[2] .. ", " .. new_pos[3])
-- =============================================================================
-- Practical Examples: Common Components
-- =============================================================================
-- Create a player character:
function create_player(name, x, y, z)
local player = ecs.create_entity()
ecs.set_entity_name(player, name)
-- Transform (position in the world):
ecs.set_component(player, "Transform", {
position = { x, y, z },
rotation = { 1, 0, 0, 0 },
scale = { 1, 1, 1 }
})
-- Renderable (visible mesh):
ecs.set_component(player, "Renderable", {
meshName = "character.mesh",
visible = true
})
-- Character (physics capsule for movement):
ecs.set_component(player, "Character", {
radius = 0.4,
height = 1.8,
offset = { 0, 0.9, 0 },
enabled = true,
useGravity = true
})
-- PlayerController (camera and input):
ecs.set_component(player, "PlayerController", {
cameraMode = 1,
tpsDistance = 5.0,
tpsHeight = 2.0,
mouseSensitivity = 0.5
})
-- Inventory:
ecs.set_component(player, "Inventory", {
maxSlots = 20,
maxWeight = 50.0,
isContainer = true
})
print("Created player: " .. name)
return player
end
local hero = create_player("Hero", 0, 0, 0)
-- Create a light source:
function create_light(name, x, y, z, light_type)
local light = ecs.create_entity()
ecs.set_entity_name(light, name)
ecs.set_component(light, "Transform", {
position = { x, y, z },
rotation = { 1, 0, 0, 0 },
scale = { 1, 1, 1 }
})
ecs.set_component(light, "Light", {
lightType = light_type or 0, -- 0=point, 1=directional, 2=spot
diffuseColor = { 1, 1, 1, 1 },
intensity = 1.5,
range = 100,
castShadows = true
})
print("Created light: " .. name)
return light
end
local sun = create_light("Sun", 0, 100, 0, 1) -- directional light
-- Create a building with smart object interaction:
function create_building(name, x, z)
local building = ecs.create_entity()
ecs.set_entity_name(building, name)
ecs.set_component(building, "Transform", {
position = { x, 0, z },
rotation = { 1, 0, 0, 0 },
scale = { 1, 1, 1 }
})
ecs.set_component(building, "Renderable", {
meshName = "building.mesh",
visible = true
})
ecs.set_component(building, "SmartObject", {
radius = 2.0,
height = 3.0,
actionNames = { "enter", "exit" }
})
-- RigidBody for physics:
ecs.set_component(building, "RigidBody", {
bodyType = 0, -- 0=static
mass = 0,
friction = 0.5,
restitution = 0.1,
enabled = true
})
print("Created building: " .. name)
return building
end
local house = create_building("House", 10, 10)
-- Create an item:
function create_item(name, item_id, x, y, z)
local item = ecs.create_entity()
ecs.set_entity_name(item, name)
ecs.set_component(item, "Transform", {
position = { x, y, z },
rotation = { 1, 0, 0, 0 },
scale = { 0.5, 0.5, 0.5 }
})
ecs.set_component(item, "Renderable", {
meshName = "potion.mesh",
visible = true
})
ecs.set_component(item, "Item", {
itemName = name,
itemType = "consumable",
itemId = item_id,
stackSize = 1,
maxStackSize = 10,
weight = 0.5,
value = 50,
useActionName = "drink_potion"
})
print("Created item: " .. name)
return item
end
local potion = create_item("Health Potion", "potion_health", 5, 0.5, 5)
-- Create a NavMesh area:
function create_navmesh_area(name)
local nav = ecs.create_entity()
ecs.set_entity_name(nav, name)
ecs.set_component(nav, "NavMesh", {
cellSize = 0.3,
cellHeight = 0.2,
agentHeight = 2.0,
agentRadius = 0.5,
agentMaxClimb = 0.5,
agentMaxSlope = 45.0,
enabled = true
})
print("Created NavMesh: " .. name)
return nav
end
local navmesh = create_navmesh_area("MainNavMesh")
-- =============================================================================
-- Working with Tag Components
-- =============================================================================
-- Tag components are boolean markers with no data fields.
-- Common tags include:
-- EditorMarker - marks entities visible in the editor
-- InWater - entity is currently in water
-- GeneratedPhysicsTag - physics was auto-generated
-- ParentComponent - entity is a parent in a hierarchy
-- NavMeshGeometrySource - entity contributes to navmesh generation
-- Example: Mark entities for different purposes:
local marker1 = ecs.create_entity()
ecs.add_component(marker1, "GeneratedPhysicsTag")
local marker2 = ecs.create_entity()
ecs.add_component(marker2, "NavMeshGeometrySource")
-- =============================================================================
-- Working with GOAP Components
-- =============================================================================
-- Create an NPC with GOAP AI:
function create_npc(name, x, z)
local npc = ecs.create_entity()
ecs.set_entity_name(npc, name)
ecs.set_component(npc, "Transform", {
position = { x, 0, z },
rotation = { 1, 0, 0, 0 },
scale = { 1, 1, 1 }
})
ecs.set_component(npc, "Character", {
radius = 0.4,
height = 1.8,
offset = { 0, 0.9, 0 },
enabled = true,
useGravity = true
})
-- GOAP blackboard (character's knowledge about the world):
ecs.set_component(npc, "GoapBlackboard", {
values = {
health = 100,
stamina = 100,
hunger = 0,
wood_count = 0
},
floatValues = {
hunger = 0.0
},
stringValues = {
state = "idle"
}
})
-- GOAP planner (plans actions to achieve goals):
ecs.set_component(npc, "GoapPlanner", {
enabled = true,
maxIterations = 100
})
-- GOAP runner (executes the plan):
ecs.set_component(npc, "GoapRunner", {
enabled = true,
currentAction = "idle"
})
-- Behavior tree for idle behavior:
ecs.set_component(npc, "BehaviorTree", {
treeName = "idle_behavior",
enabled = true
})
-- Path following for movement:
ecs.set_component(npc, "PathFollowing", {
enabled = true,
speed = 1.5
})
-- NavMesh agent for navigation:
ecs.set_component(npc, "NavMeshAgent", {
enabled = true,
radius = 0.5,
height = 2.0
})
print("Created NPC: " .. name)
return npc
end
local npc = create_npc("Villager_01", -10, -10)
-- =============================================================================
-- Working with Event Handler Components
-- =============================================================================
-- Add event handlers to entities:
ecs.set_component(npc, "EventHandler", {
eventName = "collision",
actionName = "handle_collision",
enabled = true
})
-- =============================================================================
-- Working with Dialogue Components
-- =============================================================================
-- Add dialogue to an NPC:
ecs.set_component(npc, "Dialogue", {
text = "Hello there, traveler!",
speaker = "Villager_01",
enabled = true
})
-- =============================================================================
-- Working with Water Components
-- =============================================================================
-- Create a water plane:
function create_water(name, y)
local water = ecs.create_entity()
ecs.set_entity_name(water, name)
ecs.set_component(water, "WaterPlane", {
enabled = true,
waterSurfaceY = y or 0.0,
planeSize = 1000,
reflectivity = 0.5,
waveSpeed = 1.0
})
ecs.set_component(water, "WaterPhysics", {
enabled = true,
waveHeight = 0.5
})
print("Created water: " .. name)
return water
end
local ocean = create_water("Ocean", 0.0)
-- =============================================================================
-- Working with Sky and Sun Components
-- =============================================================================
-- Create a skybox:
function create_sky(name)
local sky = ecs.create_entity()
ecs.set_entity_name(sky, name)
ecs.set_component(sky, "Skybox", {
enabled = true,
size = 500,
starsEnabled = true
})
print("Created sky: " .. name)
return sky
end
local skybox = create_sky("Skybox")
-- Update sun properties:
ecs.set_component(sun, "Sun", {
enabled = true,
timeOfDay = 12.0,
timeSpeed = 1.0,
intensity = 1.0,
castShadows = true
})
-- =============================================================================
-- Working with Camera Components
-- =============================================================================
-- Create a camera:
function create_camera(name)
local cam = ecs.create_entity()
ecs.set_entity_name(cam, name)
ecs.set_component(cam, "Camera", {
fovY = 60,
nearClip = 0.1,
farClip = 1000,
orthographic = false
})
print("Created camera: " .. name)
return cam
end
local camera = create_camera("MainCamera")
-- =============================================================================
-- Working with Prefab Components
-- =============================================================================
-- Mark an entity as a prefab instance:
ecs.set_component(house, "PrefabInstance", {
prefabPath = "prefabs/house.json",
instantiated = true
})
-- =============================================================================
-- Working with CellGrid Components
-- =============================================================================
-- Create a cell grid for spatial partitioning:
function create_cell_grid(name, width, height, depth)
local grid = ecs.create_entity()
ecs.set_entity_name(grid, name)
ecs.set_component(grid, "CellGrid", {
width = width or 10,
height = height or 5,
depth = depth or 10,
cellSize = 1.0,
cellHeight = 0.5
})
print("Created cell grid: " .. name)
return grid
end
local grid = create_cell_grid("WorldGrid", 20, 10, 20)
-- =============================================================================
-- Working with Town/District/Lot/Room Components
-- =============================================================================
-- Create a town with districts, lots, and rooms:
function create_town(name)
local town = ecs.create_entity()
ecs.set_entity_name(town, name)
ecs.set_component(town, "Town", {
townName = name,
population = 500
})
-- Create a district:
local district = ecs.create_entity()
ecs.set_entity_name(district, "Market District")
ecs.set_component(district, "District", {
districtName = "Market District",
districtType = "commercial"
})
-- Create a lot:
local lot = ecs.create_entity()
ecs.set_entity_name(lot, "Residential Lot 1")
ecs.set_component(lot, "Lot", {
lotName = "Residential Lot 1",
lotType = "residential",
width = 20,
depth = 30
})
-- Create a room:
local room = ecs.create_entity()
ecs.set_entity_name(room, "Kitchen")
ecs.set_component(room, "Room", {
roomName = "Kitchen",
roomType = "kitchen",
floor = 0
})
print("Created town: " .. name)
return town
end
local rivendell = create_town("Rivendell")
-- =============================================================================
-- Working with Furniture Components
-- =============================================================================
-- Create a furniture template:
function create_furniture(name, mesh, category)
local furniture = ecs.create_entity()
ecs.set_entity_name(furniture, name)
ecs.set_component(furniture, "FurnitureTemplate", {
templateName = name,
meshName = mesh or "chair.mesh",
category = category or "seating"
})
print("Created furniture: " .. name)
return furniture
end
local chair = create_furniture("Wooden Chair", "chair.mesh", "seating")
-- =============================================================================
-- Working with Animation Components
-- =============================================================================
-- Add animation tree to a character:
ecs.set_component(npc, "AnimationTree", {
treeName = "humanoid",
enabled = true
})
ecs.set_component(npc, "AnimationTreeTemplate", {
templateName = "humanoid_base",
blendTime = 0.2
})
-- =============================================================================
-- Working with Physics Components
-- =============================================================================
-- Add a physics collider to an entity:
ecs.set_component(house, "PhysicsCollider", {
shapeType = "box",
size = { 5, 3, 5 },
enabled = true
})
-- Add buoyancy info for water physics:
ecs.set_component(house, "BuoyancyInfo", {
enabled = true,
buoyancy = 1.0,
linearDrag = 0.5,
angularDrag = 0.3
})
-- =============================================================================
-- Working with LOD Components
-- =============================================================================
-- Add LOD settings:
ecs.set_component(entity, "Lod", {
lodLevel = 0,
distance = 100.0
})
ecs.set_component(entity, "LodSettings", {
enabled = true,
lodBias = 1.0
})
-- =============================================================================
-- Working with Static Geometry Components
-- =============================================================================
-- Batch entities into static geometry:
ecs.set_component(entity, "StaticGeometry", {
batchName = "forest_batch",
enabled = true
})
ecs.set_component(entity, "StaticGeometryMember", {
parentBatch = "forest_batch",
enabled = true
})
-- =============================================================================
-- Working with Procedural Components
-- =============================================================================
-- Create procedural textures and materials:
ecs.set_component(entity, "ProceduralTexture", {
textureName = "grass",
width = 512,
height = 512
})
ecs.set_component(entity, "ProceduralMaterial", {
materialName = "ground_mat",
baseColor = { 0.5, 0.5, 0.5, 1.0 }
})
-- =============================================================================
-- Working with Primitive Components
-- =============================================================================
-- Create a primitive shape:
ecs.set_component(entity, "Primitive", {
primitiveType = "box",
size = { 1, 2, 1 }
})
-- =============================================================================
-- Working with Triangle Buffer Components
-- =============================================================================
ecs.set_component(entity, "TriangleBuffer", {
enabled = true,
vertexCount = 100
})
-- =============================================================================
-- Working with Character Slots
-- =============================================================================
ecs.set_component(entity, "CharacterSlots", {
slotCount = 8
})
-- =============================================================================
-- Working with Roof Components
-- =============================================================================
ecs.set_component(entity, "Roof", {
roofType = "gable",
height = 2.5,
enabled = true
})
-- =============================================================================
-- Working with ClearArea Components
-- =============================================================================
ecs.set_component(entity, "ClearArea", {
radius = 5.0,
enabled = true
})
-- =============================================================================
-- Working with Action Database Components
-- =============================================================================
ecs.set_component(entity, "ActionDatabase", {
enabled = true
})
ecs.set_component(entity, "ActionDebug", {
enabled = true,
showDebugInfo = true
})
-- =============================================================================
-- Working with Startup Menu Components
-- =============================================================================
ecs.set_component(entity, "StartupMenu", {
enabled = true,
showOnStart = true
})
-- =============================================================================
-- Component Lifecycle Summary
-- =============================================================================
-- Components can be:
-- 1. Added: ecs.add_component(entity, "ComponentName")
-- 2. Removed: ecs.remove_component(entity, "ComponentName")
-- 3. Checked: ecs.has_component(entity, "ComponentName")
-- 4. Get: ecs.get_component(entity, "ComponentName")
-- 5. Set: ecs.set_component(entity, "ComponentName", { fields... })
-- 6. Field: ecs.get_field(entity, "ComponentName", "fieldName")
-- 7. Field: ecs.set_field(entity, "ComponentName", "fieldName", value)
print("Component API examples completed successfully!")

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

@@ -0,0 +1,102 @@
-- =============================================================================
-- Dialogue: Basic Show via Event System
-- =============================================================================
-- This example demonstrates how to show a simple dialogue box using the
-- EventBus "dialogue_show" event.
--
-- The DialogueSystem listens for "dialogue_show" events and displays the
-- text on any entity that has a DialogueComponent.
--
-- Event payload parameters:
-- "text" (string) - Narration text to display
-- "speaker" (string) - Optional speaker name (shown above text)
-- "choices" (table) - Array of choice label strings (optional)
-- =============================================================================
-- ---------------------------------------------------------------------------
-- 1. Create an entity with a DialogueComponent
-- ---------------------------------------------------------------------------
-- First we need an entity that has the Dialogue component so the system
-- knows where to render the dialogue box.
local dialogue_entity = ecs.create_entity()
ecs.set_entity_name(dialogue_entity, "DialogueBox")
-- Add the Dialogue component with default settings:
ecs.add_component(dialogue_entity, "Dialogue")
-- You can also configure the dialogue box appearance:
ecs.set_component(dialogue_entity, "Dialogue", {
fontName = "Jupiteroid-Regular.ttf",
fontSize = 24.0,
speakerFontSize = 20.0,
backgroundOpacity = 0.85,
boxHeightFraction = 0.25, -- 25% of screen height
boxPositionFraction = 0.75, -- bottom quarter of screen
enabled = true
})
print("Dialogue entity created with ID: " .. dialogue_entity)
-- ---------------------------------------------------------------------------
-- 2. Show a simple narration (no choices)
-- ---------------------------------------------------------------------------
-- Send a "dialogue_show" event with just text. The dialogue box will appear
-- and the player can click anywhere to dismiss it.
ecs.send_event("dialogue_show", {
text = "Welcome to the world of World2!",
speaker = "Narrator"
})
print("Sent basic narration dialogue")
-- ---------------------------------------------------------------------------
-- 3. Show dialogue with player choices
-- ---------------------------------------------------------------------------
-- When "choices" is provided as a table, the dialogue box shows
-- buttons instead of click-to-progress. The player must pick one.
ecs.send_event("dialogue_show", {
text = "Where would you like to go?",
speaker = "Guide",
choices = { "The Forest", "The Village", "The Mountains" }
})
print("Sent dialogue with choices")
-- ---------------------------------------------------------------------------
-- 4. Show dialogue without a speaker name
-- ---------------------------------------------------------------------------
ecs.send_event("dialogue_show", {
text = "A mysterious voice echoes through the chamber..."
})
print("Sent anonymous narration")
-- ---------------------------------------------------------------------------
-- 5. Multi-line dialogue (use \n for line breaks)
-- ---------------------------------------------------------------------------
ecs.send_event("dialogue_show", {
text = "Greetings, traveler.\n\nI have been expecting you.\nThe prophecy spoke of your arrival.",
speaker = "Elder Marcus"
})
print("Sent multi-line dialogue")
-- =============================================================================
-- Summary
-- =============================================================================
-- To show dialogue from Lua:
-- 1. Ensure an entity with DialogueComponent exists (create one if needed)
-- 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" } (table of strings)
-- 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

@@ -0,0 +1,219 @@
-- =============================================================================
-- Dialogue: Direct Component API Control
-- =============================================================================
-- This example demonstrates how to control the DialogueComponent directly
-- via the ECS component API, without using the EventBus.
--
-- The DialogueComponent has methods that can be called from C++:
-- show(text, choices, speaker) - Display dialogue
-- progress() - Dismiss (no-choices mode)
-- selectChoice(index) - Select a choice (1-based)
-- isActive() - Check if dialogue is active
-- reset() - Reset to idle state
--
-- From Lua, you manipulate the component's fields directly using the
-- ecs.set_component / ecs.get_component API.
-- =============================================================================
-- ---------------------------------------------------------------------------
-- 1. Create an entity with DialogueComponent
-- ---------------------------------------------------------------------------
local dlg = ecs.create_entity()
ecs.set_entity_name(dlg, "DialogueBox")
ecs.add_component(dlg, "Dialogue")
-- ---------------------------------------------------------------------------
-- 2. Set dialogue text directly via component fields
-- ---------------------------------------------------------------------------
-- Instead of sending an event, you can set the component fields directly.
-- The DialogueSystem will pick up the state change on the next frame.
ecs.set_component(dlg, "Dialogue", {
text = "This dialogue was set directly via the component API!",
speaker = "Lua Script",
enabled = true
})
-- Note: Setting the fields directly does NOT automatically change the state
-- to Showing. You need to also set the state, or use the event system.
-- The DialogueComponent's show() method handles state transitions.
-- ---------------------------------------------------------------------------
-- 3. Read dialogue state from the component
-- ---------------------------------------------------------------------------
local comp = ecs.get_component(dlg, "Dialogue")
if comp then
print("Dialogue text: " .. (comp.text or "(empty)"))
print("Dialogue speaker: " .. (comp.speaker or "(none)"))
print("Dialogue enabled: " .. tostring(comp.enabled))
print("Font: " .. (comp.fontName or "default"))
print("Font size: " .. (comp.fontSize or 24))
end
-- ---------------------------------------------------------------------------
-- 4. Modify individual dialogue fields
-- ---------------------------------------------------------------------------
-- Change just the text:
ecs.set_field(dlg, "Dialogue", "text", "Updated dialogue text!")
-- Change just the speaker:
ecs.set_field(dlg, "Dialogue", "speaker", "Mysterious Stranger")
-- Change appearance settings:
ecs.set_field(dlg, "Dialogue", "backgroundOpacity", 0.9)
ecs.set_field(dlg, "Dialogue", "boxHeightFraction", 0.3)
ecs.set_field(dlg, "Dialogue", "boxPositionFraction", 0.7)
-- Read back the changes:
local updated_text = ecs.get_field(dlg, "Dialogue", "text")
local updated_speaker = ecs.get_field(dlg, "Dialogue", "speaker")
print("Updated text: " .. updated_text)
print("Updated speaker: " .. updated_speaker)
-- ---------------------------------------------------------------------------
-- 5. Toggle dialogue visibility
-- ---------------------------------------------------------------------------
-- Disable the dialogue box:
ecs.set_field(dlg, "Dialogue", "enabled", false)
print("Dialogue disabled")
-- Re-enable it:
ecs.set_field(dlg, "Dialogue", "enabled", true)
print("Dialogue re-enabled")
-- ---------------------------------------------------------------------------
-- 6. Check if dialogue component exists
-- ---------------------------------------------------------------------------
if ecs.has_component(dlg, "Dialogue") then
print("Entity has a Dialogue component")
end
-- ---------------------------------------------------------------------------
-- 7. Remove the dialogue component entirely
-- ---------------------------------------------------------------------------
-- ecs.remove_component(dlg, "Dialogue")
-- print("Dialogue component removed")
-- ---------------------------------------------------------------------------
-- 8. Practical: Configure dialogue appearance per-NPC
-- ---------------------------------------------------------------------------
function create_npc_with_dialogue(name, mesh, greeting_text)
local npc = ecs.create_entity()
ecs.set_entity_name(npc, name)
-- Basic NPC setup
ecs.set_component(npc, "Transform", {
position = { 0, 0, 0 },
rotation = { 1, 0, 0, 0 },
scale = { 1, 1, 1 }
})
ecs.set_component(npc, "Renderable", {
meshName = mesh or "character.mesh",
visible = true
})
-- Dialogue component with NPC-specific appearance
ecs.set_component(npc, "Dialogue", {
text = greeting_text or "Hello!",
speaker = name,
fontName = "Jupiteroid-Regular.ttf",
fontSize = 24.0,
speakerFontSize = 20.0,
backgroundOpacity = 0.85,
boxHeightFraction = 0.25,
boxPositionFraction = 0.75,
enabled = true
})
print("Created NPC with dialogue: " .. name)
return npc
end
-- Create a few NPCs with different dialogue configurations
local merchant = create_npc_with_dialogue(
"Merchant",
"merchant.mesh",
"Welcome to my shop! Best wares in town."
)
local guard = create_npc_with_dialogue(
"Guard",
"guard.mesh",
"Halt! Who goes there?"
)
-- ---------------------------------------------------------------------------
-- 9. Practical: Update dialogue based on game events
-- ---------------------------------------------------------------------------
function update_npc_dialogue(npc_entity, new_text, new_speaker)
-- Update the dialogue text and speaker
ecs.set_field(npc_entity, "Dialogue", "text", new_text)
if new_speaker then
ecs.set_field(npc_entity, "Dialogue", "speaker", new_speaker)
end
-- Show the updated dialogue via event (this triggers the state change)
ecs.send_event("dialogue_show", {
text = new_text,
speaker = new_speaker or ecs.get_field(npc_entity, "Dialogue", "speaker")
})
end
-- Update the merchant's dialogue after a transaction
update_npc_dialogue(merchant, "Thank you for your business! Come again.")
-- Update the guard's dialogue when player has high reputation
update_npc_dialogue(guard, "At ease, friend. The town is safe with you around.")
-- ---------------------------------------------------------------------------
-- 10. Practical: Dialogue with dynamic choices from component data
-- ---------------------------------------------------------------------------
function show_dialogue_with_dynamic_choices(npc_entity, base_text, choice_list)
-- choice_list is a table of strings
-- Update the component
ecs.set_field(npc_entity, "Dialogue", "text", base_text)
-- Show via event (which handles state transitions properly)
ecs.send_event("dialogue_show", {
text = base_text,
speaker = ecs.get_field(npc_entity, "Dialogue", "speaker"),
choices = choice_list
})
end
-- Example: Shop inventory as dialogue choices
local shop_items = { "Buy Sword (50 gold)", "Buy Shield (30 gold)", "Buy Potion (10 gold)", "Leave" }
show_dialogue_with_dynamic_choices(merchant, "What would you like to buy?", shop_items)
-- =============================================================================
-- Summary
-- =============================================================================
-- Direct component API vs EventBus approach:
--
-- Component API (ecs.set_component / ecs.get_component):
-- - Read/write any DialogueComponent field
-- - Configure appearance (font, size, opacity, position)
-- - Toggle enabled/disabled
-- - Does NOT trigger state transitions (Showing/AwaitingChoice/Idle)
--
-- EventBus (ecs.send_event "dialogue_show"):
-- - Triggers proper state transitions
-- - Parses choices from table of strings
-- - Best for showing dialogue to the player
--
-- Best practice: Use the EventBus to SHOW dialogue, and the component API
-- to CONFIGURE the dialogue box appearance.
-- =============================================================================
print("Dialogue component API examples completed!")

View File

@@ -0,0 +1,260 @@
-- =============================================================================
-- Dialogue: EventHandler Component Integration
-- =============================================================================
-- This example demonstrates how to use the EventHandlerComponent to trigger
-- dialogue automatically when an event is received.
--
-- The EventHandlerComponent links an event name to a GoapAction. When the
-- event fires, the action's behavior tree is executed. This allows you to
-- wire up dialogue triggers declaratively without writing Lua code.
--
-- 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.
-- =============================================================================
-- ---------------------------------------------------------------------------
-- 1. Create the dialogue entity
-- ---------------------------------------------------------------------------
local dlg = ecs.create_entity()
ecs.set_entity_name(dlg, "DialogueBox")
ecs.add_component(dlg, "Dialogue")
-- ---------------------------------------------------------------------------
-- 2. Create an NPC with EventHandler for dialogue triggers
-- ---------------------------------------------------------------------------
local npc = ecs.create_entity()
ecs.set_entity_name(npc, "QuestGiver")
ecs.set_component(npc, "Transform", {
position = { 5, 0, 5 },
rotation = { 1, 0, 0, 0 },
scale = { 1, 1, 1 }
})
ecs.set_component(npc, "Renderable", {
meshName = "character.mesh",
visible = true
})
-- Add an EventHandler that triggers dialogue when the player approaches
ecs.set_component(npc, "EventHandler", {
eventName = "player_approached",
actionName = "npc_greeting",
enabled = true
})
print("Created NPC with EventHandler for player_approached event")
-- ---------------------------------------------------------------------------
-- 3. Create an EventHandler that triggers on quest completion
-- ---------------------------------------------------------------------------
local quest_npc = ecs.create_entity()
ecs.set_entity_name(quest_npc, "QuestRewarder")
ecs.set_component(quest_npc, "EventHandler", {
eventName = "quest_completed",
actionName = "quest_reward_dialogue",
enabled = true
})
print("Created NPC with EventHandler for quest_completed event")
-- ---------------------------------------------------------------------------
-- 4. Trigger dialogue via events from other game systems
-- ---------------------------------------------------------------------------
-- Simulate a proximity trigger: when the player gets close to an NPC,
-- send an event that triggers the dialogue.
function on_player_near_npc(npc_name, distance)
print("Player is " .. distance .. "m from " .. npc_name)
if distance < 5.0 then
-- Send the event that the EventHandler is listening for
ecs.send_event("player_approached", {
npc_name = npc_name,
location = "town_square",
distance = distance
})
-- Also show dialogue directly
ecs.send_event("dialogue_show", {
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
-- Simulate the player approaching
on_player_near_npc("QuestGiver", 3.0)
-- ---------------------------------------------------------------------------
-- 5. Chain events: choice -> event -> next dialogue
-- ---------------------------------------------------------------------------
-- When the player makes a choice, we can send a new event that triggers
-- another EventHandler, creating a chain reaction.
-- Subscribe to dialogue choices
local choice_sub = ecs.subscribe_event("dialogue_choice", function(event, params)
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", {
quest_name = "The Lost Artifact",
giver = "QuestGiver",
reward_gold = 100,
reward_xp = 500
})
-- Show follow-up dialogue
ecs.send_event("dialogue_show", {
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", {
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", {
text = "Very well. The offer stands if you change your mind.",
speaker = "QuestGiver"
})
end
end)
print("Subscribed to dialogue_choice for event chaining")
-- ---------------------------------------------------------------------------
-- 6. Subscribe to custom events for game logic
-- ---------------------------------------------------------------------------
-- Listen for quest acceptance
local quest_sub = ecs.subscribe_event("quest_accepted", function(event, params)
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", {
quest_name = quest_name,
active_quests = 1
})
end)
print("Subscribed to quest_accepted events")
-- ---------------------------------------------------------------------------
-- 7. Practical: Zone entry dialogue
-- ---------------------------------------------------------------------------
-- When the player enters a new area, show contextual dialogue.
function on_zone_entered(zone_name)
local zone_dialogues = {
forest = {
text = "You enter the Dark Forest. The trees loom overhead,\nblocking out the sunlight.",
speaker = "Narrator"
},
village = {
text = "Welcome to Greenhaven Village. Smoke rises from\nchimneys and children play in the streets.",
speaker = "Narrator"
},
dungeon = {
text = "The air grows cold and damp as you descend into\nthe ancient dungeon. Somewhere, water drips.",
speaker = "Narrator"
},
beach = {
text = "The sea stretches to the horizon. Waves crash\nagainst the shore. A ship is docked nearby.",
speaker = "Narrator"
}
}
local dialogue = zone_dialogues[zone_name]
if dialogue then
ecs.send_event("dialogue_show", {
text = dialogue.text,
speaker = dialogue.speaker
})
-- Also send a zone-specific event for other systems
ecs.send_event("zone_entered", {
zone = zone_name
})
end
end
-- Simulate zone transitions
on_zone_entered("forest")
on_zone_entered("village")
on_zone_entered("dungeon")
-- ---------------------------------------------------------------------------
-- 8. Practical: Item pickup dialogue
-- ---------------------------------------------------------------------------
function on_item_picked_up(item_name, item_count)
local pickup_messages = {
health_potion = "You pick up a Health Potion. It glows with a warm light.",
ancient_key = "An ancient key, covered in rust. It must open something important.",
gold_coins = "You find " .. item_count .. " gold coins. They clink satisfyingly.",
mysterious_map = "A faded map with markings you can't decipher. Someone might know what it means.",
sword = "A fine steel sword. It feels balanced in your hand."
}
local message = pickup_messages[item_name]
if message then
ecs.send_event("dialogue_show", {
text = message,
speaker = "Narrator"
})
end
end
-- Simulate item pickups
on_item_picked_up("health_potion", 1)
on_item_picked_up("ancient_key", 1)
on_item_picked_up("gold_coins", 50)
-- =============================================================================
-- Summary
-- =============================================================================
-- EventHandler + Dialogue integration patterns:
--
-- 1. Proximity triggers:
-- Player near NPC -> send event -> EventHandler triggers action -> dialogue
--
-- 2. Choice chaining:
-- Player picks choice -> send event -> EventHandler triggers -> next dialogue
--
-- 3. Zone entry:
-- Player enters area -> send event -> dialogue shows description
--
-- 4. Item pickup:
-- Player picks up item -> send event -> contextual dialogue
--
-- 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

@@ -0,0 +1,143 @@
-- =============================================================================
-- Dialogue: Event Subscription & Choice Handling
-- =============================================================================
-- This example demonstrates how to subscribe to dialogue-related events
-- and handle player choices from Lua.
--
-- The DialogueSystem fires events when the player interacts with the
-- dialogue box. You can subscribe to these events to drive game logic.
--
-- Dialogue-related events you can subscribe to:
-- "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.
-- =============================================================================
-- ---------------------------------------------------------------------------
-- 1. Create the dialogue entity
-- ---------------------------------------------------------------------------
local dialogue_entity = ecs.create_entity()
ecs.set_entity_name(dialogue_entity, "DialogueBox")
ecs.add_component(dialogue_entity, "Dialogue")
-- ---------------------------------------------------------------------------
-- 2. Subscribe to dialogue choice events
-- ---------------------------------------------------------------------------
-- When the player selects a choice in the dialogue box, we can react to it.
-- The DialogueComponent's onChoiceSelected callback fires with the 1-based
-- choice index. We bridge this via the EventBus.
local choice_sub = ecs.subscribe_event("dialogue_choice", function(event, params)
local choice_index = params.choice_index or 0
local choice_text = params.choice_text or "unknown"
print("Player selected choice #" .. choice_index .. ": " .. choice_text)
-- React based on which choice was selected
if choice_index == 1 then
print(" -> Player chose the first option!")
elseif choice_index == 2 then
print(" -> Player chose the second option!")
elseif choice_index == 3 then
print(" -> Player chose the third option!")
end
end)
print("Subscribed to dialogue_choice events (ID: " .. choice_sub .. ")")
-- ---------------------------------------------------------------------------
-- 3. Subscribe to dialogue dismiss events
-- ---------------------------------------------------------------------------
-- When dialogue is dismissed (clicked through with no choices), we can
-- trigger follow-up actions.
local dismiss_sub = ecs.subscribe_event("dialogue_dismiss", function(event, params)
print("Dialogue was dismissed by the player")
-- You could trigger follow-up dialogue or game logic here
local next_text = params.next_text or ""
if next_text ~= "" then
print(" -> Next dialogue queued: " .. next_text)
end
end)
print("Subscribed to dialogue_dismiss events (ID: " .. dismiss_sub .. ")")
-- ---------------------------------------------------------------------------
-- 4. Subscribe to dialogue show events (for logging/tracking)
-- ---------------------------------------------------------------------------
local show_sub = ecs.subscribe_event("dialogue_show", function(event, params)
local text = params.text or ""
local speaker = params.speaker or "Unknown"
local choices = params.choices or {}
print("[Dialogue Log] " .. speaker .. ": \"" .. text .. "\"")
if #choices > 0 then
print("[Dialogue Log] Choices: " .. table.concat(choices, ", "))
end
end)
print("Subscribed to dialogue_show events for logging (ID: " .. show_sub .. ")")
-- ---------------------------------------------------------------------------
-- 5. Example: Branching dialogue with choice handling
-- ---------------------------------------------------------------------------
-- This shows a complete flow: show dialogue -> handle choice -> react
function show_branching_dialogue()
-- Step 1: Show the dialogue with choices
ecs.send_event("dialogue_show", {
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.
-- In a real scenario, you'd use a state machine or coroutine to
-- manage the flow. See dialogue_sequence.lua for a more advanced example.
end
show_branching_dialogue()
-- ---------------------------------------------------------------------------
-- 6. Example: NPC greeting with follow-up
-- ---------------------------------------------------------------------------
function npc_greeting(npc_name, greeting_text)
-- Show initial greeting
ecs.send_event("dialogue_show", {
text = greeting_text,
speaker = npc_name,
choices = { "Who are you?", "Tell me about this place", "Goodbye" }
})
-- The choice subscriber will handle the response.
-- You could extend this with a lookup table for NPC responses.
end
npc_greeting("Elder Marcus", "Ah, a new face in our village! Welcome, traveler.")
-- =============================================================================
-- Summary
-- =============================================================================
-- To handle dialogue choices from Lua:
-- 1. Subscribe to "dialogue_choice" events
-- 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

@@ -0,0 +1,320 @@
-- =============================================================================
-- Dialogue: Sequential Dialogue with Coroutines
-- =============================================================================
-- This example demonstrates how to create sequential, branching dialogue
-- using Lua coroutines. This is the most practical approach for story-driven
-- dialogue where you need to wait for player input between lines.
--
-- The pattern:
-- 1. Show dialogue with choices
-- 2. Wait for player to select a choice (via event subscription)
-- 3. React and show next dialogue based on the choice
-- 4. Repeat until the conversation ends
-- =============================================================================
-- ---------------------------------------------------------------------------
-- 1. Create the dialogue entity
-- ---------------------------------------------------------------------------
local dialogue_entity = ecs.create_entity()
ecs.set_entity_name(dialogue_entity, "DialogueBox")
ecs.add_component(dialogue_entity, "Dialogue")
-- ---------------------------------------------------------------------------
-- 2. Dialogue Queue System
-- ---------------------------------------------------------------------------
-- A simple queue that lets you chain dialogue lines and wait for player
-- input between each one.
local DialogueQueue = {}
local dialogue_queue_active = false
local dialogue_queue_pending = false
local dialogue_queue_choice = 0
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.choice_index or 0
dialogue_queue_choice_text = params.choice_text or ""
dialogue_queue_pending = false
end
end)
-- Subscribe to dismiss events to unblock the queue
local queue_dismiss_sub = ecs.subscribe_event("dialogue_dismiss", function(event, params)
if dialogue_queue_pending then
dialogue_queue_choice = -1 -- signal dismissed
dialogue_queue_pending = false
end
end)
-- ---------------------------------------------------------------------------
-- 3. Helper: Show dialogue and wait for player response
-- ---------------------------------------------------------------------------
--- Show a line of dialogue and wait for the player to respond.
--- @param text string The narration text
--- @param speaker string|nil Optional speaker name
--- @param choices table|nil Array of choice label strings (nil = click to dismiss)
--- @return number choice_index (0 if dismissed, 1+ for choices)
function show_and_wait(text, speaker, choices)
-- Send the dialogue event
ecs.send_event("dialogue_show", {
text = text,
speaker = speaker or "",
choices = choices or {}
})
-- Wait for player response
dialogue_queue_pending = true
dialogue_queue_choice = 0
-- Busy-wait loop (in a real coroutine-based system, you'd yield here)
-- This is a simplified version; see the coroutine example below.
local timeout = 1000
while dialogue_queue_pending and timeout > 0 do
-- In a real game loop, this would be a coroutine yield.
-- For this example, we simulate with a counter.
timeout = timeout - 1
if timeout <= 0 then
dialogue_queue_pending = false
print("WARNING: Dialogue wait timed out!")
end
end
return dialogue_queue_choice
end
-- ---------------------------------------------------------------------------
-- 4. Example: Simple linear conversation
-- ---------------------------------------------------------------------------
function simple_conversation()
print("=== Simple Conversation ===")
-- Line 1: Narration with no choices (click to continue)
ecs.send_event("dialogue_show", {
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.
-- For this example, we just show the pattern.
-- Line 2: NPC speaks with choices
ecs.send_event("dialogue_show", {
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)")
end
-- ---------------------------------------------------------------------------
-- 5. Example: Branching conversation tree
-- ---------------------------------------------------------------------------
-- Define a conversation tree as a table of nodes
local conversations = {
village_elder = {
greeting = {
text = "Welcome to our village, stranger. What brings you here?",
speaker = "Elder Marcus",
choices = {
{ text = "I seek adventure", next = "adventure" },
{ text = "Just passing through", next = "passing" },
{ text = "I need a place to stay", next = "lodging" }
}
},
adventure = {
text = "Adventure, you say? The old ruins to the east have been stirring.",
speaker = "Elder Marcus",
choices = {
{ text = "Tell me about the ruins", next = "ruins" },
{ text = "I'll check them out", next = "goodbye_adventure" },
{ text = "Maybe another time", next = "goodbye" }
}
},
passing = {
text = "Safe travels! The road north is clear, but beware the forest at night.",
speaker = "Elder Marcus",
choices = {
{ text = "Thank you for the warning", next = "goodbye" },
{ text = "What's in the forest?", next = "forest" }
}
},
lodging = {
text = "The inn is just down the road. Tell them Marcus sent you.",
speaker = "Elder Marcus",
choices = {
{ text = "Thank you, elder", next = "goodbye" },
{ text = "Is there work in town?", next = "work" }
}
},
ruins = {
text = "Ancient ruins, full of traps and treasure. Several have entered, few returned.",
speaker = "Elder Marcus",
choices = {
{ text = "I'm not afraid", next = "goodbye_adventure" },
{ text = "Sounds too dangerous", next = "goodbye" }
}
},
forest = {
text = "Strange creatures have been seen. Wolves the size of horses!",
speaker = "Elder Marcus",
choices = {
{ text = "I'll be careful", next = "goodbye" },
{ text = "I can handle it", next = "goodbye_adventure" }
}
},
work = {
text = "The blacksmith needs an apprentice. Ask for Henrik.",
speaker = "Elder Marcus",
choices = {
{ text = "I'll visit the blacksmith", next = "goodbye" },
{ text = "Thanks, but I'll move on", next = "goodbye" }
}
},
goodbye_adventure = {
text = "Good luck, brave one. You'll need it.",
speaker = "Elder Marcus",
choices = {
{ text = "Farewell!", next = nil }
}
},
goodbye = {
text = "May the winds guide you safely.",
speaker = "Elder Marcus",
choices = {
{ text = "Farewell!", next = nil }
}
}
}
}
--- Walk through a conversation tree.
--- @param tree table The conversation tree definition
--- @param start_node string The starting node name
function run_conversation(tree, start_node)
print("=== Starting Conversation ===")
local current_node_name = start_node
local max_steps = 20
local step = 0
while current_node_name and step < max_steps do
step = step + 1
local node = tree[current_node_name]
if not node then
print("ERROR: Unknown conversation node: " .. current_node_name)
break
end
-- Build choices table from the node's choices
local choices = {}
local choice_map = {}
if node.choices then
for i, choice in ipairs(node.choices) do
table.insert(choices, choice.text)
choice_map[i] = choice
end
end
-- Show the dialogue
ecs.send_event("dialogue_show", {
text = node.text,
speaker = node.speaker or "",
choices = choices
})
-- In a real game, you'd wait for the player's choice here.
-- For this example, we simulate by picking the first choice.
print(" [Node: " .. current_node_name .. "] " .. node.speaker .. ": \"" .. node.text .. "\"")
-- Simulate picking a choice (in real game, wait for player input)
if node.choices and #node.choices > 0 then
local chosen = node.choices[1] -- Simulate picking first choice
print(" [Player chose: " .. chosen.text .. "]")
current_node_name = chosen.next
else
current_node_name = nil
end
end
print("=== Conversation Ended ===")
end
-- Run the conversation tree
run_conversation(conversations.village_elder, "greeting")
-- ---------------------------------------------------------------------------
-- 6. Example: NPC dialogue with state tracking
-- ---------------------------------------------------------------------------
-- Track NPC dialogue state
local npc_state = {
marcus_met = false,
marcus_friendship = 0,
quest_active = false,
quest_completed = false
}
function talk_to_elder_marcus()
if not npc_state.marcus_met then
-- First meeting
npc_state.marcus_met = true
npc_state.marcus_friendship = 10
ecs.send_event("dialogue_show", {
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", {
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", {
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", {
text = "Welcome back, friend. The village is peaceful today.",
speaker = "Elder Marcus",
choices = { "Any news?", "I need supplies", "Goodbye" }
})
end
end
-- Simulate talking to Marcus a few times
print("=== NPC State Tracking ===")
talk_to_elder_marcus() -- First meeting
npc_state.quest_active = true
talk_to_elder_marcus() -- Quest active
npc_state.quest_completed = true
talk_to_elder_marcus() -- Quest completed
-- =============================================================================
-- Summary
-- =============================================================================
-- For sequential dialogue:
-- 1. Use a queue/coroutine pattern to chain dialogue lines
-- 2. Subscribe to "dialogue_choice" and "dialogue_dismiss" events
-- 3. Wait for player input between each line
-- 4. Use conversation trees for branching narratives
-- 5. Track NPC state to change dialogue based on game progress
-- =============================================================================
print("Dialogue sequence examples completed!")

View File

@@ -0,0 +1,177 @@
-- =============================================================================
-- Entity Lua API Examples
-- =============================================================================
-- This file demonstrates how to create, manage, and query entities from Lua
-- using the ecs.* Lua API.
--
-- Entities are the fundamental building blocks of the ECS world. Every object
-- in the game world is an entity with a unique numeric ID.
-- =============================================================================
-- =============================================================================
-- Creating Entities
-- =============================================================================
-- Create a new entity. Returns a numeric ID.
local player = ecs.create_entity()
print("Created entity with ID: " .. player)
-- Create multiple entities:
local npc1 = ecs.create_entity()
local npc2 = ecs.create_entity()
local item = ecs.create_entity()
-- =============================================================================
-- Entity Existence and Lifecycle
-- =============================================================================
-- Check if an entity exists:
if ecs.entity_exists(player) then
print("Player entity exists")
end
-- Check a non-existent entity:
if not ecs.entity_exists(999999) then
print("Entity 999999 does not exist")
end
-- Destroy an entity:
ecs.destroy_entity(npc2)
if not ecs.entity_exists(npc2) then
print("npc2 was destroyed")
end
-- =============================================================================
-- Entity Names
-- =============================================================================
-- Set an entity's name:
ecs.set_entity_name(player, "Hero")
ecs.set_entity_name(npc1, "Villager_01")
ecs.set_entity_name(item, "Health_Potion")
-- Get an entity's name:
local name = ecs.get_entity_name(player)
print("Player name: " .. name)
-- Look up an entity by name:
local found = ecs.get_entity_by_name("Villager_01")
if found then
print("Found entity " .. found .. " with name 'Villager_01'")
end
-- Look up a non-existent name:
local not_found = ecs.get_entity_by_name("Nonexistent")
if not_found == nil then
print("'Nonexistent' not found (returns nil)")
end
-- =============================================================================
-- Entity Hierarchy (Parent/Children)
-- =============================================================================
-- Create a parent-child hierarchy:
local house = ecs.create_entity()
ecs.set_entity_name(house, "House")
local door = ecs.create_entity()
ecs.set_entity_name(door, "Door")
local window = ecs.create_entity()
ecs.set_entity_name(window, "Window")
-- Check parent of a child (initially none):
local p = ecs.parent(door)
if p == nil then
print("Door has no parent initially")
end
-- Check children of a parent (initially none):
local kids = ecs.children(house)
print("House has " .. #kids .. " children initially")
-- =============================================================================
-- Practical Example: Creating a Scene
-- =============================================================================
-- Create a complete scene with entities:
function create_scene()
-- Create terrain
local terrain = ecs.create_entity()
ecs.set_entity_name(terrain, "Terrain")
-- Create lighting
local sun = ecs.create_entity()
ecs.set_entity_name(sun, "Sun")
-- Create buildings
local buildings = {}
for i = 1, 3 do
local bldg = ecs.create_entity()
ecs.set_entity_name(bldg, "Building_" .. i)
table.insert(buildings, bldg)
end
-- Create characters
local characters = {}
for i = 1, 5 do
local char = ecs.create_entity()
ecs.set_entity_name(char, "Character_" .. i)
table.insert(characters, char)
end
print("Scene created with:")
print(" 1 terrain entity")
print(" 1 sun entity")
print(" " .. #buildings .. " buildings")
print(" " .. #characters .. " characters")
return {
terrain = terrain,
sun = sun,
buildings = buildings,
characters = characters
}
end
local scene = create_scene()
-- =============================================================================
-- Entity ID Properties
-- =============================================================================
-- Entity IDs are positive integers:
local e = ecs.create_entity()
assert(type(e) == "number", "Entity ID should be a number")
assert(e > 0, "Entity ID should be positive")
-- Each entity gets a unique ID:
local ids = {}
for i = 1, 10 do
ids[i] = ecs.create_entity()
end
for i = 1, 10 do
for j = i + 1, 10 do
assert(ids[i] ~= ids[j], "IDs should be unique")
end
end
print("All 10 entities have unique IDs")
-- Destroyed entity IDs are not reused immediately:
local old_id = ecs.create_entity()
ecs.destroy_entity(old_id)
local new_id = ecs.create_entity()
assert(new_id ~= old_id, "New entity should have different ID")
print("Destroyed entity ID " .. old_id .. " is not reused")
-- =============================================================================
-- Error Handling
-- =============================================================================
-- These operations should not crash:
ecs.destroy_entity(999999) -- destroying non-existent entity is safe
ecs.set_entity_name(999999, "ghost") -- setting name on non-existent entity
local n = ecs.get_entity_name(999999) -- returns empty string
print("Name of non-existent entity: '" .. n .. "'")
print("Entity API examples completed successfully!")

View File

@@ -0,0 +1,296 @@
-- =============================================================================
-- Event Lua API Examples
-- =============================================================================
-- This file demonstrates how to subscribe to, send, and manage events from
-- Lua using the ecs.* Lua API.
--
-- 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"
-- =============================================================================
-- =============================================================================
-- Subscribing to Events
-- =============================================================================
-- Subscribe to an event. Returns a subscription ID (number) that can be
-- used to unsubscribe later.
local sub_id = ecs.subscribe_event("hello", function(event, params)
print("Received event: " .. event)
end)
print("Subscribed to 'hello' with ID: " .. sub_id)
-- =============================================================================
-- Sending Events
-- =============================================================================
-- Send a simple event with no parameters:
ecs.send_event("hello")
-- Send an event with parameters (flat key-value pairs):
ecs.send_event("hello", {
count = 42,
message = "Hello World!"
})
-- =============================================================================
-- Event Callback Parameters
-- =============================================================================
-- The callback receives two arguments:
-- 1. event - the event name (string)
-- 2. params - a table with the event data (or nil if no data was sent)
ecs.subscribe_event("player_damaged", function(event, params)
print("Event: " .. event)
if params then
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", {
damage = 25,
health = 75,
source = "goblin_archer",
position = { 10, 0, 20 }
})
-- =============================================================================
-- Unsubscribing from Events
-- =============================================================================
-- When you no longer need to listen to an event, unsubscribe:
local temp_sub = ecs.subscribe_event("temporary", function()
print("This should not be printed after unsubscribe")
end)
ecs.send_event("temporary") -- callback fires
ecs.unsubscribe_event(temp_sub)
ecs.send_event("temporary") -- callback does NOT fire (unsubscribed)
-- =============================================================================
-- Multiple Subscribers
-- =============================================================================
-- Multiple subscribers can listen to the same event. All of them will be
-- called when the event is sent.
local count_a = 0
local count_b = 0
local sub_a = ecs.subscribe_event("multi", function()
count_a = count_a + 1
print("Subscriber A called (total: " .. count_a .. ")")
end)
local sub_b = ecs.subscribe_event("multi", function()
count_b = count_b + 1
print("Subscriber B called (total: " .. count_b .. ")")
end)
ecs.send_event("multi") -- both A and B fire
ecs.send_event("multi") -- both fire again
-- =============================================================================
-- Event Parameter Types
-- =============================================================================
-- 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
-- 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
-- Print type metadata
if params._types then
print(" Type metadata:")
for k, t in pairs(params._types) do
print(" " .. k .. " -> " .. t)
end
end
end
end)
-- Send an event with all parameter types:
ecs.send_event("data_event", {
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" }
})
-- =============================================================================
-- Practical Example: Game Event System
-- =============================================================================
-- Create a simple game event system:
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.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.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.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.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.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.killed_by or "unknown"
print("Player was killed by " .. killer .. "!")
end)
print("All game event handlers registered")
end
register_game_handlers()
-- Simulate some game events:
ecs.send_event("quest_complete", {
quest_name = "The Lost Artifact",
reward_xp = 500
})
ecs.send_event("enemy_killed", {
enemy_type = "Goblin Warrior",
xp_reward = 50
})
ecs.send_event("item_picked_up", {
item_name = "Health Potion",
count = 2
})
ecs.send_event("dialogue_started", {
npc_name = "Elder Marcus"
})
ecs.send_event("time_changed", {
hour = 18,
minute = 30
})
-- =============================================================================
-- Cleanup: Unsubscribe All
-- =============================================================================
-- When cleaning up, unsubscribe all registered handlers:
function cleanup_event_handlers()
for name, id in pairs(event_handlers) do
ecs.unsubscribe_event(id)
print("Unsubscribed from: " .. name)
end
end
-- Uncomment to clean up:
-- cleanup_event_handlers()
-- =============================================================================
-- Event API Reference
-- =============================================================================
--
-- ecs.subscribe_event(event_name, callback)
-- Subscribe to an event. Returns a subscription ID (number).
-- Parameters:
-- event_name - string, the name of the event to listen for
-- callback - function(event, params), called when the event fires
-- Returns: subscription ID (number)
--
-- ecs.unsubscribe_event(subscription_id)
-- Unsubscribe from an event.
-- Parameters:
-- subscription_id - number, the ID returned by subscribe_event
-- Safe to call with an invalid ID (no crash).
--
-- ecs.send_event(event_name, params)
-- Send an event to all subscribers.
-- Parameters:
-- event_name - string, the name of the event to send
-- 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

@@ -0,0 +1,576 @@
#include "LuaActionApi.hpp"
#include "../components/ActionDatabase.hpp"
#include "../components/GoapBlackboard.hpp"
#include "../components/BehaviorTree.hpp"
#include <OgreLogManager.h>
#include <cstring>
namespace editScene
{
// ---------------------------------------------------------------------------
// Helper: push a BehaviorTreeNode as a Lua table
// ---------------------------------------------------------------------------
static void pushBehaviorTree(lua_State *L, const BehaviorTreeNode &node)
{
lua_newtable(L);
lua_pushstring(L, node.type.c_str());
lua_setfield(L, -2, "type");
if (!node.name.empty()) {
lua_pushstring(L, node.name.c_str());
lua_setfield(L, -2, "name");
}
if (!node.params.empty()) {
lua_pushstring(L, node.params.c_str());
lua_setfield(L, -2, "params");
}
if (!node.children.empty()) {
lua_newtable(L);
for (size_t i = 0; i < node.children.size(); i++) {
pushBehaviorTree(L, node.children[i]);
lua_rawseti(L, -2, (int)i + 1);
}
lua_setfield(L, -2, "children");
}
}
// ---------------------------------------------------------------------------
// Helper: read a BehaviorTreeNode from a Lua table at given index
// ---------------------------------------------------------------------------
static BehaviorTreeNode readBehaviorTree(lua_State *L, int idx)
{
BehaviorTreeNode node;
if (!lua_istable(L, idx))
return node;
// type (required)
lua_getfield(L, idx, "type");
if (lua_isstring(L, -1))
node.type = lua_tostring(L, -1);
lua_pop(L, 1);
// name (optional)
lua_getfield(L, idx, "name");
if (lua_isstring(L, -1))
node.name = lua_tostring(L, -1);
lua_pop(L, 1);
// params (optional)
lua_getfield(L, idx, "params");
if (lua_isstring(L, -1))
node.params = lua_tostring(L, -1);
lua_pop(L, 1);
// children (optional array)
lua_getfield(L, idx, "children");
if (lua_istable(L, -1)) {
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
// key is numeric index, value is child table
if (lua_istable(L, -1)) {
node.children.push_back(
readBehaviorTree(L, lua_gettop(L)));
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
return node;
}
// ---------------------------------------------------------------------------
// Helper: push a GoapBlackboard as a Lua table
// ---------------------------------------------------------------------------
static void pushBlackboard(lua_State *L, const GoapBlackboard &bb)
{
lua_newtable(L); // blackboard table
// Bits
lua_newtable(L); // bits table
for (int i = 0; i < 64; i++) {
if (bb.hasBit(i)) {
const char *name = GoapBlackboard::getBitName(i);
const char *key = name ? name : "";
lua_pushboolean(L, bb.getBit(i));
lua_setfield(L, -2, key);
}
}
lua_setfield(L, -2, "bits");
// Integer values
lua_newtable(L);
for (const auto &kv : bb.values) {
lua_pushinteger(L, kv.second);
lua_setfield(L, -2, kv.first.c_str());
}
lua_setfield(L, -2, "values");
// Float values
lua_newtable(L);
for (const auto &kv : bb.floatValues) {
lua_pushnumber(L, kv.second);
lua_setfield(L, -2, kv.first.c_str());
}
lua_setfield(L, -2, "floatValues");
// String values
lua_newtable(L);
for (const auto &kv : bb.stringValues) {
lua_pushstring(L, kv.second.c_str());
lua_setfield(L, -2, kv.first.c_str());
}
lua_setfield(L, -2, "stringValues");
}
// ---------------------------------------------------------------------------
// Helper: read a GoapBlackboard from a Lua table (optional, at given index)
// ---------------------------------------------------------------------------
static GoapBlackboard readBlackboard(lua_State *L, int idx)
{
GoapBlackboard bb;
if (!lua_istable(L, idx))
return bb;
// Read bits
lua_getfield(L, idx, "bits");
if (lua_istable(L, -1)) {
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
// key is the bit name (string), value is boolean
if (lua_isstring(L, -2) && lua_isboolean(L, -1)) {
const char *name = lua_tostring(L, -2);
bool val = lua_toboolean(L, -1) != 0;
// Find bit index by name (auto-registers if new)
int idx2 = GoapBlackboard::findBitByName(name);
if (idx2 < 0) {
// Find first free slot
for (int i = 0; i < 64; i++) {
if (GoapBlackboard::getBitName(
i) == nullptr) {
GoapBlackboard::setBitName(
i, name);
idx2 = i;
break;
}
}
}
if (idx2 >= 0)
bb.setBit(idx2, val);
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
// Read integer values
lua_getfield(L, idx, "values");
if (lua_istable(L, -1)) {
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
if (lua_isstring(L, -2) && lua_isinteger(L, -1))
bb.values[lua_tostring(L, -2)] =
(int)lua_tointeger(L, -1);
lua_pop(L, 1);
}
}
lua_pop(L, 1);
// Read float values
lua_getfield(L, idx, "floatValues");
if (lua_istable(L, -1)) {
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
if (lua_isstring(L, -2) && lua_isnumber(L, -1))
bb.floatValues[lua_tostring(L, -2)] =
(float)lua_tonumber(L, -1);
lua_pop(L, 1);
}
}
lua_pop(L, 1);
// Read string values
lua_getfield(L, idx, "stringValues");
if (lua_istable(L, -1)) {
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
if (lua_isstring(L, -2) && lua_isstring(L, -1))
bb.stringValues[lua_tostring(L, -2)] =
lua_tostring(L, -1);
lua_pop(L, 1);
}
}
lua_pop(L, 1);
return bb;
}
// ---------------------------------------------------------------------------
// Lua: ecs.action_db.add_action(name, cost, preconds, effects, behaviorTree)
// ---------------------------------------------------------------------------
static int luaAddAction(lua_State *L)
{
const char *name = luaL_checkstring(L, 1);
int cost = (int)luaL_optinteger(L, 2, 1);
GoapAction action(name, cost);
// Optional preconditions table (arg 3)
if (lua_gettop(L) >= 3 && lua_istable(L, 3))
action.preconditions = readBlackboard(L, 3);
// Optional effects table (arg 4)
if (lua_gettop(L) >= 4 && lua_istable(L, 4))
action.effects = readBlackboard(L, 4);
// Optional behavior tree table (arg 5)
if (lua_gettop(L) >= 5 && lua_istable(L, 5))
action.behaviorTree = readBehaviorTree(L, 5);
ActionDatabase::getSingleton().addOrReplaceAction(action);
Ogre::LogManager::getSingleton().stream()
<< "[Lua] Added action: " << name;
return 0;
}
// ---------------------------------------------------------------------------
// Lua: ecs.action_db.add_goal(name, priority, target, condition)
// ---------------------------------------------------------------------------
static int luaAddGoal(lua_State *L)
{
const char *name = luaL_checkstring(L, 1);
int priority = (int)luaL_optinteger(L, 2, 1);
GoapGoal goal(name, priority);
// Optional target blackboard (arg 3)
if (lua_gettop(L) >= 3 && lua_istable(L, 3))
goal.target = readBlackboard(L, 3);
// Optional condition string (arg 4)
if (lua_gettop(L) >= 4 && lua_isstring(L, 4))
goal.condition = lua_tostring(L, 4);
ActionDatabase::getSingleton().addOrReplaceGoal(goal);
Ogre::LogManager::getSingleton().stream()
<< "[Lua] Added goal: " << name;
return 0;
}
// ---------------------------------------------------------------------------
// Lua: ecs.action_db.remove_action(name) -> bool
// ---------------------------------------------------------------------------
static int luaRemoveAction(lua_State *L)
{
const char *name = luaL_checkstring(L, 1);
bool removed = ActionDatabase::getSingleton().removeAction(name);
lua_pushboolean(L, removed);
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.action_db.remove_goal(name) -> bool
// ---------------------------------------------------------------------------
static int luaRemoveGoal(lua_State *L)
{
const char *name = luaL_checkstring(L, 1);
bool removed = ActionDatabase::getSingleton().removeGoal(name);
lua_pushboolean(L, removed);
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.action_db.find_action(name) -> table or nil
// ---------------------------------------------------------------------------
static int luaFindAction(lua_State *L)
{
const char *name = luaL_checkstring(L, 1);
const GoapAction *action =
ActionDatabase::getSingleton().findAction(name);
if (!action) {
lua_pushnil(L);
return 1;
}
lua_newtable(L);
lua_pushstring(L, action->name.c_str());
lua_setfield(L, -2, "name");
lua_pushinteger(L, action->cost);
lua_setfield(L, -2, "cost");
pushBlackboard(L, action->preconditions);
lua_setfield(L, -2, "preconditions");
pushBlackboard(L, action->effects);
lua_setfield(L, -2, "effects");
// Behavior tree
pushBehaviorTree(L, action->behaviorTree);
lua_setfield(L, -2, "behaviorTree");
// Behavior tree name (optional reference)
if (!action->behaviorTreeName.empty()) {
lua_pushstring(L, action->behaviorTreeName.c_str());
lua_setfield(L, -2, "behaviorTreeName");
}
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.action_db.find_goal(name) -> table or nil
// ---------------------------------------------------------------------------
static int luaFindGoal(lua_State *L)
{
const char *name = luaL_checkstring(L, 1);
const GoapGoal *goal = ActionDatabase::getSingleton().findGoal(name);
if (!goal) {
lua_pushnil(L);
return 1;
}
lua_newtable(L);
lua_pushstring(L, goal->name.c_str());
lua_setfield(L, -2, "name");
lua_pushinteger(L, goal->priority);
lua_setfield(L, -2, "priority");
pushBlackboard(L, goal->target);
lua_setfield(L, -2, "target");
lua_pushstring(L, goal->condition.c_str());
lua_setfield(L, -2, "condition");
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.action_db.list_actions() -> table of action names
// ---------------------------------------------------------------------------
static int luaListActions(lua_State *L)
{
const auto &db = ActionDatabase::getSingleton();
lua_newtable(L);
int idx = 1;
for (const auto &action : db.actions) {
lua_pushstring(L, action.name.c_str());
lua_rawseti(L, -2, idx++);
}
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.action_db.list_goals() -> table of goal names
// ---------------------------------------------------------------------------
static int luaListGoals(lua_State *L)
{
const auto &db = ActionDatabase::getSingleton();
lua_newtable(L);
int idx = 1;
for (const auto &goal : db.goals) {
lua_pushstring(L, goal.name.c_str());
lua_rawseti(L, -2, idx++);
}
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.action_db.clear() -> nil
// ---------------------------------------------------------------------------
static int luaClear(lua_State *L)
{
(void)L;
ActionDatabase::getSingleton().clear();
return 0;
}
// ---------------------------------------------------------------------------
// Lua: ecs.action_db.set_bit_name(index, name) -> nil
// ---------------------------------------------------------------------------
static int luaSetBitName(lua_State *L)
{
int index = (int)luaL_checkinteger(L, 1);
const char *name = luaL_checkstring(L, 2);
if (index < 0 || index >= 64)
luaL_error(L, "bit index must be 0-63, got %d", index);
GoapBlackboard::setBitName(index, name);
return 0;
}
// ---------------------------------------------------------------------------
// Lua: ecs.action_db.find_bit_by_name(name) -> index or nil
// ---------------------------------------------------------------------------
static int luaFindBitByName(lua_State *L)
{
const char *name = luaL_checkstring(L, 1);
int index = GoapBlackboard::findBitByName(name);
if (index < 0) {
lua_pushnil(L);
} else {
lua_pushinteger(L, index);
}
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.action_db.get_bit_name(index) -> name or nil
// ---------------------------------------------------------------------------
static int luaGetBitName(lua_State *L)
{
int index = (int)luaL_checkinteger(L, 1);
if (index < 0 || index >= 64) {
lua_pushnil(L);
return 1;
}
const char *name = GoapBlackboard::getBitName(index);
if (name) {
lua_pushstring(L, name);
} else {
lua_pushnil(L);
}
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.action_db.list_bit_names() -> table of { index, name }
// ---------------------------------------------------------------------------
static int luaListBitNames(lua_State *L)
{
lua_newtable(L);
int idx = 1;
for (int i = 0; i < 64; i++) {
const char *name = GoapBlackboard::getBitName(i);
if (name) {
lua_newtable(L);
lua_pushinteger(L, i);
lua_setfield(L, -2, "index");
lua_pushstring(L, name);
lua_setfield(L, -2, "name");
lua_rawseti(L, -2, idx++);
}
}
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.action_db.auto_assign_bit(name) -> index
// ---------------------------------------------------------------------------
// Finds a bit by name, or auto-assigns the first free slot if not found.
// Returns the bit index, or -1 if all 64 slots are full.
static int luaAutoAssignBit(lua_State *L)
{
const char *name = luaL_checkstring(L, 1);
int index = GoapBlackboard::findBitByName(name);
if (index >= 0) {
lua_pushinteger(L, index);
return 1;
}
// Find first free slot
for (int i = 0; i < 64; i++) {
if (GoapBlackboard::getBitName(i) == nullptr) {
GoapBlackboard::setBitName(i, name);
lua_pushinteger(L, i);
return 1;
}
}
lua_pushinteger(L, -1);
return 1;
}
// ---------------------------------------------------------------------------
// Registration
// ---------------------------------------------------------------------------
void registerLuaActionApi(lua_State *L)
{
// Get or create the "ecs" global table
lua_getglobal(L, "ecs");
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_newtable(L);
}
// Create the action_db sub-table
lua_newtable(L);
lua_pushcfunction(L, luaAddAction);
lua_setfield(L, -2, "add_action");
lua_pushcfunction(L, luaAddGoal);
lua_setfield(L, -2, "add_goal");
lua_pushcfunction(L, luaRemoveAction);
lua_setfield(L, -2, "remove_action");
lua_pushcfunction(L, luaRemoveGoal);
lua_setfield(L, -2, "remove_goal");
lua_pushcfunction(L, luaFindAction);
lua_setfield(L, -2, "find_action");
lua_pushcfunction(L, luaFindGoal);
lua_setfield(L, -2, "find_goal");
lua_pushcfunction(L, luaListActions);
lua_setfield(L, -2, "list_actions");
lua_pushcfunction(L, luaListGoals);
lua_setfield(L, -2, "list_goals");
lua_pushcfunction(L, luaClear);
lua_setfield(L, -2, "clear");
// Bit name management
lua_pushcfunction(L, luaSetBitName);
lua_setfield(L, -2, "set_bit_name");
lua_pushcfunction(L, luaFindBitByName);
lua_setfield(L, -2, "find_bit_by_name");
lua_pushcfunction(L, luaGetBitName);
lua_setfield(L, -2, "get_bit_name");
lua_pushcfunction(L, luaListBitNames);
lua_setfield(L, -2, "list_bit_names");
lua_pushcfunction(L, luaAutoAssignBit);
lua_setfield(L, -2, "auto_assign_bit");
// Set action_db as a field of ecs
lua_setfield(L, -2, "action_db");
// Ensure ecs is global
lua_setglobal(L, "ecs");
}
} // namespace editScene

View File

@@ -0,0 +1,101 @@
#ifndef EDITSCENE_LUA_ACTION_API_HPP
#define EDITSCENE_LUA_ACTION_API_HPP
#pragma once
#include <lua.hpp>
/**
* @file LuaActionApi.hpp
* @brief Lua API for the ActionDatabase singleton.
*
* Provides Lua functions to create, query, and manage GOAP actions
* and goals in the global ActionDatabase singleton.
*
* Exposed Lua globals (under the "ecs" table):
* ecs.action_db.add_action(name, cost, preconds, effects, behaviorTree) -> nil
* ecs.action_db.add_goal(name, priority, target, condition) -> nil
* ecs.action_db.remove_action(name) -> bool
* ecs.action_db.remove_goal(name) -> bool
* ecs.action_db.find_action(name) -> table or nil
* ecs.action_db.find_goal(name) -> table or nil
* ecs.action_db.list_actions() -> table of action names
* ecs.action_db.list_goals() -> table of goal names
* ecs.action_db.clear() -> nil
*
* Bit Name Management:
* ecs.action_db.set_bit_name(index, name) -> nil
* Assign a human-readable name to a bit slot (0-63).
* Example: ecs.action_db.set_bit_name(0, "has_axe")
*
* ecs.action_db.find_bit_by_name(name) -> index or nil
* Look up which bit index a name is assigned to.
* Returns nil if the name hasn't been assigned yet.
*
* ecs.action_db.get_bit_name(index) -> name or nil
* Get the name assigned to a bit slot, or nil if unassigned.
*
* ecs.action_db.list_bit_names() -> table of { index, name }
* Returns an array of all assigned bit names with their indices.
*
* ecs.action_db.auto_assign_bit(name) -> index
* Find a bit by name, or auto-assign the first free slot.
* Returns the bit index, or -1 if all 64 slots are full.
* This is the same logic used internally by readBlackboard()
* when it encounters an unknown bit name in preconditions/effects.
*
* Bit Name Convention:
* Bit names are global across the entire game session. They map
* human-readable names (like "has_axe", "is_hungry") to bit indices
* (0-63) used in GoapBlackboard preconditions and effects.
*
* You can define bits explicitly at startup:
* ecs.action_db.set_bit_name(0, "has_axe")
* ecs.action_db.set_bit_name(1, "has_wood")
* ecs.action_db.set_bit_name(2, "is_hungry")
*
* Or let them be auto-assigned when you use them in actions:
* ecs.action_db.add_action("chop_wood", 2,
* { bits = { has_axe = true } }, -- "has_axe" auto-assigned if new
* { bits = { has_wood = true } })
*
* Use auto_assign_bit() to explicitly reserve a name:
* local idx = ecs.action_db.auto_assign_bit("my_flag")
* -- idx is now the bit index for "my_flag"
*
* Use list_bit_names() to see all currently assigned names:
* local bits = ecs.action_db.list_bit_names()
* for _, b in ipairs(bits) do
* print(b.index .. ": " .. b.name)
* end
*
* Behavior Tree Table Format (arg 5 of add_action):
* {
* type = "sequence", -- node type: sequence, selector, invert, task, check, etc.
* name = "optional_name", -- action/condition name, animation state, etc.
* params = "optional_params", -- extra parameters (e.g. delay seconds, bit index)
* children = { -- array of child nodes (for sequence/selector/invert)
* { type = "task", name = "myAction" },
* { type = "setAnimationState", name = "SM/Walk" },
* { type = "delay", params = "2.0" }
* }
* }
*
* See BehaviorTree.hpp for full list of node types.
*/
namespace editScene
{
/**
* @brief Register all ActionDatabase-related Lua API functions.
*
* Adds action_db sub-table to the "ecs" global table.
* Must be called after LuaState is constructed.
*
* @param L The Lua state.
*/
void registerLuaActionApi(lua_State *L);
} // namespace editScene
#endif // EDITSCENE_LUA_ACTION_API_HPP

View File

@@ -0,0 +1,471 @@
#include "LuaBehaviorTreeApi.hpp"
#include "LuaEntityApi.hpp"
#include "../components/GoapBlackboard.hpp"
#include <OgreLogManager.h>
#include <unordered_map>
#include <string>
#include <vector>
namespace editScene
{
// ---------------------------------------------------------------------------
// Global registry of Lua node handlers
// ---------------------------------------------------------------------------
// Maps node name -> Lua registry reference for the callback function.
// The callback is stored as a Lua reference in the registry so it persists
// across Lua state resets and can be called from C++.
static std::unordered_map<std::string, int> g_luaNodeHandlers;
// Global Lua state pointer, set during registerLuaBehaviorTreeApi().
// Used by callLuaBehaviorTreeNode() which is invoked from the C++
// behavior tree system without direct access to the Lua state.
static lua_State *g_luaState = nullptr;
// ---------------------------------------------------------------------------
// Helper: push a GoapBlackboard as a Lua table (reused from LuaActionApi)
// ---------------------------------------------------------------------------
static void pushBlackboard(lua_State *L, const GoapBlackboard &bb)
{
lua_newtable(L); // blackboard table
// Bits
lua_newtable(L); // bits table
for (int i = 0; i < 64; i++) {
if (bb.hasBit(i)) {
const char *name = GoapBlackboard::getBitName(i);
const char *key = name ? name : "";
lua_pushboolean(L, bb.getBit(i));
lua_setfield(L, -2, key);
}
}
lua_setfield(L, -2, "bits");
// Integer values
lua_newtable(L);
for (const auto &kv : bb.values) {
lua_pushinteger(L, kv.second);
lua_setfield(L, -2, kv.first.c_str());
}
lua_setfield(L, -2, "values");
// Float values
lua_newtable(L);
for (const auto &kv : bb.floatValues) {
lua_pushnumber(L, kv.second);
lua_setfield(L, -2, kv.first.c_str());
}
lua_setfield(L, -2, "floatValues");
// String values
lua_newtable(L);
for (const auto &kv : bb.stringValues) {
lua_pushstring(L, kv.second.c_str());
lua_setfield(L, -2, kv.first.c_str());
}
lua_setfield(L, -2, "stringValues");
}
// ---------------------------------------------------------------------------
// Helper: parse "key=val,key2=val2" params into a Lua table
// ---------------------------------------------------------------------------
static void pushParamsTable(lua_State *L, const std::string &params)
{
lua_newtable(L); // params table
if (params.empty())
return;
const char *s = params.c_str();
while (*s) {
// Skip whitespace
while (*s == ' ' || *s == '\t')
s++;
if (!*s)
break;
// Find key
const char *keyStart = s;
while (*s && *s != '=' && *s != ',')
s++;
std::string key(keyStart, static_cast<size_t>(s - keyStart));
// Trim trailing spaces from key
while (!key.empty() &&
(key.back() == ' ' || key.back() == '\t'))
key.pop_back();
if (*s != '=') {
while (*s && *s != ',')
s++;
if (*s == ',')
s++;
continue;
}
s++; // skip '='
// Skip whitespace before value
while (*s == ' ' || *s == '\t')
s++;
// Find value end (next comma or end)
const char *valStart = s;
bool inQuotes = false;
while (*s && (*s != ',' || inQuotes)) {
if (*s == '"')
inQuotes = !inQuotes;
s++;
}
std::string val(valStart, static_cast<size_t>(s - valStart));
// Trim trailing spaces from value
while (!val.empty() &&
(val.back() == ' ' || val.back() == '\t'))
val.pop_back();
// Strip quotes if present
if (val.size() >= 2 && val.front() == '"' &&
val.back() == '"') {
val = val.substr(1, val.size() - 2);
lua_pushstring(L, val.c_str());
lua_setfield(L, -2, key.c_str());
} else {
// Try numeric
char *end = nullptr;
long iVal = strtol(val.c_str(), &end, 10);
if (end != val.c_str() && *end == '\0') {
lua_pushinteger(L, (int)iVal);
lua_setfield(L, -2, key.c_str());
} else {
// Try float
end = nullptr;
float fVal = strtof(val.c_str(), &end);
if (end != val.c_str() && *end == '\0') {
lua_pushnumber(L, fVal);
lua_setfield(L, -2, key.c_str());
} else {
// Fallback to string
lua_pushstring(L, val.c_str());
lua_setfield(L, -2, key.c_str());
}
}
}
if (*s == ',')
s++;
}
}
// ---------------------------------------------------------------------------
// Public API: Call a registered Lua node handler
// ---------------------------------------------------------------------------
// Returns: 0 = success, 1 = failure, 2 = running, -1 = not found/error
int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName,
flecs::entity entity, const std::string &params)
{
// Use the global Lua state if none was passed
lua_State *L = static_cast<lua_State *>(L_);
if (!L)
L = g_luaState;
if (!L)
return -1;
auto it = g_luaNodeHandlers.find(nodeName);
if (it == g_luaNodeHandlers.end())
return -1;
int ref = it->second;
// Push the callback function
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
// Push entity ID as first argument
lua_pushinteger(L, luaEntityToId(entity));
// Push params table as second argument
pushParamsTable(L, params);
// Call the function
if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
Ogre::LogManager::getSingleton().stream()
<< "[Lua BT] Error calling node '" << nodeName
<< "': " << lua_tostring(L, -1);
lua_pop(L, 1);
return -1;
}
// Read the result
if (!lua_isstring(L, -1)) {
lua_pop(L, 1);
return -1;
}
const char *result = lua_tostring(L, -1);
lua_pop(L, 1);
if (strcmp(result, "success") == 0)
return 0;
if (strcmp(result, "failure") == 0)
return 1;
if (strcmp(result, "running") == 0)
return 2;
Ogre::LogManager::getSingleton().stream()
<< "[Lua BT] Node '" << nodeName
<< "' returned invalid result: " << result;
return -1;
}
// ---------------------------------------------------------------------------
// Convenience overload: call with raw entity ID (for tests)
// ---------------------------------------------------------------------------
int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName,
uint64_t entityId, const std::string &params)
{
// Use the global Lua state if none was passed
lua_State *L = static_cast<lua_State *>(L_);
if (!L)
L = g_luaState;
if (!L)
return -1;
auto it = g_luaNodeHandlers.find(nodeName);
if (it == g_luaNodeHandlers.end())
return -1;
int ref = it->second;
// Push the callback function
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
// Push entity ID as first argument
lua_pushinteger(L, (lua_Integer)entityId);
// Push params table as second argument
pushParamsTable(L, params);
// Call the function
if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
Ogre::LogManager::getSingleton().stream()
<< "[Lua BT] Error calling node '" << nodeName
<< "': " << lua_tostring(L, -1);
lua_pop(L, 1);
return -1;
}
// Read the result
if (!lua_isstring(L, -1)) {
lua_pop(L, 1);
return -1;
}
const char *result = lua_tostring(L, -1);
lua_pop(L, 1);
if (strcmp(result, "success") == 0)
return 0;
if (strcmp(result, "failure") == 0)
return 1;
if (strcmp(result, "running") == 0)
return 2;
Ogre::LogManager::getSingleton().stream()
<< "[Lua BT] Node '" << nodeName
<< "' returned invalid result: " << result;
return -1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.behavior_tree.register_node(name, function)
// ---------------------------------------------------------------------------
static int luaRegisterNode(lua_State *L)
{
const char *name = luaL_checkstring(L, 1);
if (!lua_isfunction(L, 2))
luaL_error(L, "Expected function as second argument");
// Remove any existing handler with the same name
auto it = g_luaNodeHandlers.find(name);
if (it != g_luaNodeHandlers.end()) {
luaL_unref(L, LUA_REGISTRYINDEX, it->second);
}
// Store the function reference
lua_pushvalue(L, 2);
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
g_luaNodeHandlers[name] = ref;
Ogre::LogManager::getSingleton().stream()
<< "[Lua BT] Registered node handler: " << name;
return 0;
}
// ---------------------------------------------------------------------------
// Lua: ecs.behavior_tree.unregister_node(name) -> bool
// ---------------------------------------------------------------------------
static int luaUnregisterNode(lua_State *L)
{
const char *name = luaL_checkstring(L, 1);
auto it = g_luaNodeHandlers.find(name);
if (it == g_luaNodeHandlers.end()) {
lua_pushboolean(L, false);
return 1;
}
luaL_unref(L, LUA_REGISTRYINDEX, it->second);
g_luaNodeHandlers.erase(it);
lua_pushboolean(L, true);
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.behavior_tree.list_nodes() -> table of names
// ---------------------------------------------------------------------------
static int luaListNodeHandlers(lua_State *L)
{
lua_newtable(L);
int idx = 1;
for (const auto &kv : g_luaNodeHandlers) {
lua_pushstring(L, kv.first.c_str());
lua_rawseti(L, -2, idx++);
}
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.behavior_tree.create_node(type, name, params) -> table
// ecs.behavior_tree.create_node(type, params) -> table
// ecs.behavior_tree.create_node(luaHandlerName, params) -> table (backward compat)
// ---------------------------------------------------------------------------
// Creates a behavior tree node table suitable for use in action behavior trees.
//
// If the first argument matches a registered Lua node handler name, it creates
// a luaTask node (backward compatible behavior).
// Otherwise, the first argument is treated as the node type string.
//
// Examples:
// ecs.behavior_tree.create_node("setAnimationState", "locomotion/walk")
// -> { type = "setAnimationState", name = "locomotion/walk" }
//
// ecs.behavior_tree.create_node("delay", "2.0")
// -> { type = "delay", params = "2.0" }
//
// ecs.behavior_tree.create_node("delay", "", "2.0")
// -> { type = "delay", params = "2.0" }
//
// ecs.behavior_tree.create_node("say_hello", "message=Hi")
// -> { type = "luaTask", name = "say_hello", params = "message=Hi" }
static int luaCreateNode(lua_State *L)
{
const char *arg1 = luaL_checkstring(L, 1);
// Check if arg1 is a registered Lua node handler name (backward compat)
bool isLuaHandler = g_luaNodeHandlers.find(arg1) !=
g_luaNodeHandlers.end();
// Save arg2 and arg3 before pushing the result table (which shifts indices)
const char *arg2 = NULL;
const char *arg3 = NULL;
if (lua_gettop(L) >= 2 && lua_isstring(L, 2))
arg2 = lua_tostring(L, 2);
if (lua_gettop(L) >= 3 && lua_isstring(L, 3))
arg3 = lua_tostring(L, 3);
lua_newtable(L);
if (isLuaHandler) {
// Backward compatible: create a luaTask node
lua_pushstring(L, "luaTask");
lua_setfield(L, -2, "type");
lua_pushstring(L, arg1);
lua_setfield(L, -2, "name");
if (arg2 && arg2[0]) {
lua_pushstring(L, arg2);
lua_setfield(L, -2, "params");
}
} else {
// arg1 is the node type
lua_pushstring(L, arg1);
lua_setfield(L, -2, "type");
// arg2 is either name or params depending on the node type
if (arg2 && arg2[0]) {
lua_pushstring(L, arg2);
lua_setfield(L, -2, "name");
}
// arg3 is params (optional)
if (arg3 && arg3[0]) {
lua_pushstring(L, arg3);
lua_setfield(L, -2, "params");
}
}
return 1;
}
// ---------------------------------------------------------------------------
// Public C++ API: get list of registered Lua node names
// ---------------------------------------------------------------------------
std::vector<std::string> getRegisteredLuaNodeNames()
{
std::vector<std::string> names;
names.reserve(g_luaNodeHandlers.size());
for (const auto &kv : g_luaNodeHandlers)
names.push_back(kv.first);
return names;
}
// ---------------------------------------------------------------------------
// Registration
// ---------------------------------------------------------------------------
void registerLuaBehaviorTreeApi(lua_State *L)
{
// Store the Lua state globally so callLuaBehaviorTreeNode can use it
g_luaState = L;
// Get or create the "ecs" global table
lua_getglobal(L, "ecs");
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_newtable(L);
}
// Create the behavior_tree sub-table
lua_newtable(L);
lua_pushcfunction(L, luaRegisterNode);
lua_setfield(L, -2, "register_node");
lua_pushcfunction(L, luaUnregisterNode);
lua_setfield(L, -2, "unregister_node");
lua_pushcfunction(L, luaListNodeHandlers);
lua_setfield(L, -2, "list_nodes");
lua_pushcfunction(L, luaCreateNode);
lua_setfield(L, -2, "create_node");
// Set behavior_tree as a field of ecs
lua_setfield(L, -2, "behavior_tree");
// Ensure ecs is global
lua_setglobal(L, "ecs");
}
} // namespace editScene

View File

@@ -0,0 +1,149 @@
#ifndef EDITSCENE_LUA_BEHAVIOR_TREE_API_HPP
#define EDITSCENE_LUA_BEHAVIOR_TREE_API_HPP
#pragma once
#include <lua.hpp>
#include <vector>
#include <string>
#include <cstdint>
#include <flecs.h>
/**
* @file LuaBehaviorTreeApi.hpp
* @brief Lua API for creating behavior tree nodes that run Lua functions.
*
* Provides Lua bindings to register named Lua functions as behavior tree
* node handlers, and to create behavior tree nodes that invoke those
* functions during tree evaluation.
*
* Exposed Lua globals (under the "ecs" table):
* ecs.behavior_tree.register_node(name, function) -> nil
* Register a Lua function as a behavior tree node handler.
* The function receives (entity_id, params_table) and must return
* "success", "failure", or "running".
*
* ecs.behavior_tree.unregister_node(name) -> bool
* Remove a previously registered node handler.
*
* ecs.behavior_tree.list_nodes() -> table of registered node names
* Returns an array of all registered node handler names.
*
* ecs.behavior_tree.create_node(type, name, params) -> table
* Creates a behavior tree node table suitable for use in
* ecs.action_db.add_action() behavior trees.
*
* If the first argument matches a registered Lua node handler name,
* it creates a luaTask node (backward compatible).
* Otherwise, the first argument is the node type string.
*
* Examples:
* ecs.behavior_tree.create_node("setAnimationState", "locomotion/walk")
* -> { type = "setAnimationState", name = "locomotion/walk" }
*
* ecs.behavior_tree.create_node("delay", "", "2.0")
* -> { type = "delay", params = "2.0" }
*
* ecs.behavior_tree.create_node("say_hello", "message=Hi")
* -> { type = "luaTask", name = "say_hello", params = "message=Hi" }
*
* Supported node types (see BehaviorTree.hpp for full list):
* sequence, selector, invert, task, check, debugPrint,
* setAnimationState, isAnimationEnded, setBit, checkBit,
* setValue, checkValue, blackboardDump, delay, teleportToChild,
* disablePhysics, enablePhysics, sendEvent, hasItem, hasItemByName,
* countItem, pickupItem, dropItem, useItem, addItemToInventory,
* luaTask
*
* Example:
* -- Register a Lua node handler
* ecs.behavior_tree.register_node("say_hello", function(entity_id, params)
* print("Hello from entity " .. entity_id .. "! " .. (params.message or ""))
* return "success"
* end)
*
* -- Build a behavior tree using both built-in C++ nodes and Lua nodes
* ecs.action_db.add_action("greet", 1, {}, {}, {
* type = "sequence",
* children = {
* ecs.behavior_tree.create_node("setAnimationState", "locomotion/walk"),
* ecs.behavior_tree.create_node("delay", "", "2.0"),
* ecs.behavior_tree.create_node("setAnimationState", "locomotion/idle"),
* ecs.behavior_tree.create_node("say_hello", "message=Hello World")
* }
* })
*
* -- A Lua node that runs for a while:
* ecs.behavior_tree.register_node("wait_random", function(entity_id, params)
* local bb = ecs.get_component(entity_id, "GoapBlackboard")
* if not bb then return "failure" end
*
* local key = "wait_timer_" .. params.timer_key
* local dt = ecs.get_delta_time()
* bb.floatValues[key] = (bb.floatValues[key] or 0) + dt
*
* local duration = tonumber(params.duration) or 1.0
* if bb.floatValues[key] >= duration then
* bb.floatValues[key] = nil
* ecs.set_component(entity_id, "GoapBlackboard", bb)
* return "success"
* end
* ecs.set_component(entity_id, "GoapBlackboard", bb)
* return "running"
* end)
*/
namespace editScene
{
/**
* @brief Register all behavior tree Lua API functions into the "ecs" table.
*
* Adds a behavior_tree sub-table to the "ecs" global table with functions
* for registering Lua node handlers and creating behavior tree nodes.
*
* @param L The Lua state.
*/
void registerLuaBehaviorTreeApi(lua_State *L);
/**
* @brief Get the list of registered Lua node handler names.
*
* Used by the behavior tree editor UI to show registered Lua nodes
* in a dropdown instead of requiring manual entry.
*
* @return A vector of registered node handler names.
*/
std::vector<std::string> getRegisteredLuaNodeNames();
/**
* @brief Call a registered Lua behavior tree node handler.
*
* This is the primary evaluation function used by the behavior tree system.
* It looks up the registered handler by name, pushes entity and params
* as arguments, calls the Lua function, and interprets the result.
*
* @param L_ Pointer to the Lua state (can be nullptr to use global state).
* @param nodeName The name of the registered node handler.
* @param entity The flecs entity executing this node.
* @param params The params string (key=val,key2=val2 format).
* @return 0 = success, 1 = failure, 2 = running, -1 = error/not found.
*/
int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName,
flecs::entity entity, const std::string &params);
/**
* @brief Convenience overload for calling a registered Lua node handler
* with a raw entity ID (for use in tests or contexts without flecs).
*
* @param L_ Pointer to the Lua state (can be nullptr to use global state).
* @param nodeName The name of the registered node handler.
* @param entityId The raw entity ID.
* @param params The params string (key=val,key2=val2 format).
* @return 0 = success, 1 = failure, 2 = running, -1 = error/not found.
*/
int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName,
uint64_t entityId, const std::string &params);
} // namespace editScene
#endif // EDITSCENE_LUA_BEHAVIOR_TREE_API_HPP

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
#ifndef EDITSCENE_LUA_COMPONENT_API_HPP
#define EDITSCENE_LUA_COMPONENT_API_HPP
#pragma once
#include <lua.hpp>
/**
* @file LuaComponentApi.hpp
* @brief Lua API for adding, removing, and modifying ECS components.
*
* Provides a generic component API where Lua scripts can add, get, set,
* and remove components on entities. Each component type is registered
* with a name string and a set of field accessors.
*
* Exposed Lua globals (in the "ecs" table):
* ecs.add_component(id, "ComponentName") -> nil
* ecs.remove_component(id, "ComponentName") -> nil
* ecs.has_component(id, "ComponentName") -> bool
* ecs.get_component(id, "ComponentName") -> table (field -> value)
* ecs.set_component(id, "ComponentName", table) -> nil
* ecs.get_field(id, "ComponentName", "fieldName") -> value
* ecs.set_field(id, "ComponentName", "fieldName", value) -> nil
*
* Supported component names (case-sensitive):
* "EntityName", "Transform", "Renderable", "Light", "Camera",
* "RigidBody", "PhysicsCollider", "Character", "CharacterSlots",
* "AnimationTree", "AnimationTreeTemplate", "BehaviorTree",
* "GoapBlackboard", "GoapAction", "GoapGoal", "ActionDatabase",
* "ActionDebug", "SmartObject", "Actuator", "EventHandler",
* "GoapPlanner", "GoapRunner", "PathFollowing", "NavMesh",
* "NavMeshGeometrySource", "NavMeshAgent", "Item", "Inventory",
* "Lod", "LodSettings", "StaticGeometry", "StaticGeometryMember",
* "ProceduralTexture", "ProceduralMaterial", "Primitive",
* "TriangleBuffer", "Sun", "Skybox", "WaterPlane", "WaterPhysics",
* "BuoyancyInfo", "InWater", "StartupMenu", "Dialogue",
* "PlayerController", "CellGrid", "Room", "ClearArea", "Roof",
* "Lot", "District", "Town", "FurnitureTemplate", "PrefabInstance",
* "EditorMarker", "GeneratedPhysicsTag", "ParentComponent",
* "ModifiedComponent"
*/
namespace editScene
{
/**
* @brief Register all component Lua API functions into the "ecs" global table.
*
* Adds functions for component manipulation (add, remove, has, get, set,
* get_field, set_field) to the existing "ecs" table.
*
* @param L The Lua state.
*/
void registerLuaComponentApi(lua_State *L);
} // namespace editScene
#endif // EDITSCENE_LUA_COMPONENT_API_HPP

View File

@@ -0,0 +1,236 @@
#include "LuaEntityApi.hpp"
#include "components/EditorMarker.hpp"
#include <OgreLogManager.h>
#include <iostream>
namespace editScene
{
// Global entity ID map
LuaEntityIdMap g_luaEntityIdMap;
int luaEntityToId(flecs::entity e)
{
if (!e.is_valid())
return -1;
return g_luaEntityIdMap.addEntity(e);
}
flecs::entity luaIdToEntity(int id)
{
return g_luaEntityIdMap.getEntity(id);
}
// ---------------------------------------------------------------------------
// Helper: get the Flecs world from the Lua registry
// ---------------------------------------------------------------------------
static flecs::world getWorld(lua_State *L)
{
lua_getfield(L, LUA_REGISTRYINDEX, "EditSceneFlecsWorld");
OgreAssert(lua_islightuserdata(L, -1), "Flecs world not registered");
flecs::world *world =
static_cast<flecs::world *>(lua_touserdata(L, -1));
lua_pop(L, 1);
return *world;
}
// ---------------------------------------------------------------------------
// Lua: ecs.create_entity() -> int (entity ID)
// Creates a new entity with EditorMarkerComponent and returns its Lua ID.
// ---------------------------------------------------------------------------
static int luaCreateEntity(lua_State *L)
{
flecs::world world = getWorld(L);
flecs::entity e = world.entity();
// Add EditorMarkerComponent so it appears in the editor hierarchy
// (the component is forward-declared; we use a tag approach)
e.add<EditorMarkerComponent>();
int id = g_luaEntityIdMap.addEntity(e);
lua_pushinteger(L, id);
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.destroy_entity(id) -> nil
// Destroys an entity and removes it from the ID map.
// ---------------------------------------------------------------------------
static int luaDestroyEntity(lua_State *L)
{
luaL_checktype(L, 1, LUA_TNUMBER);
int id = lua_tointeger(L, 1);
flecs::entity e = g_luaEntityIdMap.getEntity(id);
if (e.is_alive()) {
g_luaEntityIdMap.removeEntity(e);
e.destruct();
}
return 0;
}
// ---------------------------------------------------------------------------
// Lua: ecs.entity_exists(id) -> bool
// ---------------------------------------------------------------------------
static int luaEntityExists(lua_State *L)
{
luaL_checktype(L, 1, LUA_TNUMBER);
int id = lua_tointeger(L, 1);
lua_pushboolean(L, g_luaEntityIdMap.hasId(id) ? 1 : 0);
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.get_player_entity() -> int (entity ID) or nil
// Looks up the entity named "player" in the Flecs world.
// ---------------------------------------------------------------------------
static int luaGetPlayerEntity(lua_State *L)
{
flecs::world world = getWorld(L);
flecs::entity e = world.lookup("player");
if (e.is_valid()) {
int id = g_luaEntityIdMap.addEntity(e);
lua_pushinteger(L, id);
} else {
lua_pushnil(L);
}
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.get_entity_by_name(name) -> int (entity ID) or nil
// ---------------------------------------------------------------------------
static int luaGetEntityByName(lua_State *L)
{
luaL_checktype(L, 1, LUA_TSTRING);
const char *name = lua_tostring(L, 1);
flecs::world world = getWorld(L);
flecs::entity e = world.lookup(name);
if (e.is_valid()) {
int id = g_luaEntityIdMap.addEntity(e);
lua_pushinteger(L, id);
} else {
lua_pushnil(L);
}
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.set_entity_name(id, name) -> nil
// ---------------------------------------------------------------------------
static int luaSetEntityName(lua_State *L)
{
luaL_checktype(L, 1, LUA_TNUMBER);
luaL_checktype(L, 2, LUA_TSTRING);
int id = lua_tointeger(L, 1);
const char *name = lua_tostring(L, 2);
flecs::entity e = g_luaEntityIdMap.getEntity(id);
e.set_name(name);
return 0;
}
// ---------------------------------------------------------------------------
// Lua: ecs.get_entity_name(id) -> string or nil
// ---------------------------------------------------------------------------
static int luaGetEntityName(lua_State *L)
{
luaL_checktype(L, 1, LUA_TNUMBER);
int id = lua_tointeger(L, 1);
flecs::entity e = g_luaEntityIdMap.getEntity(id);
const char *name = e.name();
if (name && name[0]) {
lua_pushstring(L, name);
} else {
lua_pushnil(L);
}
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.parent(id) -> int (parent ID) or nil
// ---------------------------------------------------------------------------
static int luaGetParent(lua_State *L)
{
luaL_checktype(L, 1, LUA_TNUMBER);
int id = lua_tointeger(L, 1);
flecs::entity e = g_luaEntityIdMap.getEntity(id);
flecs::entity parent = e.parent();
if (parent.is_valid()) {
int parentId = g_luaEntityIdMap.addEntity(parent);
lua_pushinteger(L, parentId);
} else {
lua_pushnil(L);
}
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.children(id) -> table of child IDs
// ---------------------------------------------------------------------------
static int luaGetChildren(lua_State *L)
{
luaL_checktype(L, 1, LUA_TNUMBER);
int id = lua_tointeger(L, 1);
flecs::entity e = g_luaEntityIdMap.getEntity(id);
lua_newtable(L); // result table
int index = 1;
e.children([&](flecs::entity child) {
int childId = g_luaEntityIdMap.addEntity(child);
lua_pushinteger(L, childId);
lua_rawseti(L, -2, index);
index++;
});
return 1;
}
// ---------------------------------------------------------------------------
// Register all entity API functions
// ---------------------------------------------------------------------------
void registerLuaEntityApi(lua_State *L)
{
// Create the "ecs" global table
lua_newtable(L);
// Entity management
lua_pushcfunction(L, luaCreateEntity);
lua_setfield(L, -2, "create_entity");
lua_pushcfunction(L, luaDestroyEntity);
lua_setfield(L, -2, "destroy_entity");
lua_pushcfunction(L, luaEntityExists);
lua_setfield(L, -2, "entity_exists");
lua_pushcfunction(L, luaGetPlayerEntity);
lua_setfield(L, -2, "get_player_entity");
lua_pushcfunction(L, luaGetEntityByName);
lua_setfield(L, -2, "get_entity_by_name");
lua_pushcfunction(L, luaSetEntityName);
lua_setfield(L, -2, "set_entity_name");
lua_pushcfunction(L, luaGetEntityName);
lua_setfield(L, -2, "get_entity_name");
// Hierarchy
lua_pushcfunction(L, luaGetParent);
lua_setfield(L, -2, "parent");
lua_pushcfunction(L, luaGetChildren);
lua_setfield(L, -2, "children");
// Set the global
lua_setglobal(L, "ecs");
}
} // namespace editScene

View File

@@ -0,0 +1,126 @@
#ifndef EDITSCENE_LUA_ENTITY_API_HPP
#define EDITSCENE_LUA_ENTITY_API_HPP
#pragma once
#include <flecs.h>
#include <lua.hpp>
#include <Ogre.h>
#include <unordered_map>
/**
* @file LuaEntityApi.hpp
* @brief Lua API for entity creation, destruction, and ID mapping.
*
* Provides a bidirectional mapping between Lua integer IDs and
* Flecs entity handles. Lua scripts reference entities by integer
* IDs rather than raw Flecs handles for safety and simplicity.
*
* Exposed Lua globals:
* ecs.create_entity() -> int (entity ID)
* ecs.destroy_entity(id) -> nil
* ecs.entity_exists(id) -> bool
* ecs.get_player_entity() -> int (entity ID)
* ecs.get_entity_by_name(name) -> int (entity ID) or nil
* ecs.set_entity_name(id, name)-> nil
* ecs.get_entity_name(id) -> string or nil
* ecs.parent(id) -> int (parent ID) or nil
* ecs.children(id) -> table of child IDs
*/
namespace editScene
{
/**
* @brief Global bidirectional mapping between Lua integer IDs and Flecs entities.
*
* This is a singleton-like global so that all Lua API functions can
* access it without needing to pass it through every closure.
*/
struct LuaEntityIdMap {
std::unordered_map<int, flecs::entity> id2entity;
std::unordered_map<flecs::entity_t, int> entity2id;
int nextId = 0;
/** @brief Get the next available integer ID. */
int getNextId()
{
nextId++;
return nextId;
}
/**
* @brief Add an entity to the map, returning its integer ID.
* If the entity is already mapped, returns the existing ID.
*/
int addEntity(flecs::entity e)
{
if (entity2id.find(e.id()) != entity2id.end())
return entity2id[e.id()];
int id = getNextId();
id2entity[id] = e;
entity2id[e.id()] = id;
return id;
}
/**
* @brief Get the Flecs entity for an integer ID.
* Asserts if the ID is not found or the entity is invalid.
*/
flecs::entity getEntity(int id)
{
auto it = id2entity.find(id);
OgreAssert(it != id2entity.end(), "Invalid entity ID");
OgreAssert(it->second.is_valid(), "Entity is no longer valid");
return it->second;
}
/**
* @brief Remove an entity from the map.
*/
void removeEntity(flecs::entity e)
{
auto it = entity2id.find(e.id());
if (it != entity2id.end()) {
id2entity.erase(it->second);
entity2id.erase(it);
}
}
/**
* @brief Check if an integer ID is valid.
*/
bool hasId(int id) const
{
auto it = id2entity.find(id);
return it != id2entity.end() && it->second.is_valid();
}
};
/** @brief Global entity ID map instance. */
extern LuaEntityIdMap g_luaEntityIdMap;
/**
* @brief Convert a Flecs entity to a Lua integer ID.
* @return The integer ID, or -1 if the entity is invalid.
*/
int luaEntityToId(flecs::entity e);
/**
* @brief Convert a Lua integer ID to a Flecs entity.
* Asserts if the ID is invalid.
*/
flecs::entity luaIdToEntity(int id);
/**
* @brief Register all entity-related Lua API functions into the global table.
*
* Creates the "ecs" global table (or adds to it) with entity management
* functions. Must be called after LuaState is constructed.
*
* @param L The Lua state.
*/
void registerLuaEntityApi(lua_State *L);
} // namespace editScene
#endif // EDITSCENE_LUA_ENTITY_API_HPP

View File

@@ -0,0 +1,471 @@
#include "LuaEventApi.hpp"
#include "LuaEntityApi.hpp"
#include "../systems/EventBus.hpp"
#include "../components/EventParams.hpp"
#include <OgreLogManager.h>
#include <unordered_map>
namespace editScene
{
// ---------------------------------------------------------------------------
// Internal: subscription ID tracking for Lua-managed subscriptions
// ---------------------------------------------------------------------------
/**
* @brief Map from Lua subscription IDs to EventBus ListenerIds.
*
* When Lua subscribes to an event, we store the mapping so that
* unsubscribe_event() can remove the correct listener.
*/
static std::unordered_map<int, EventBus::ListenerId> s_luaSubscriptions;
static int s_nextLuaSubId = 1;
// ---------------------------------------------------------------------------
// Helper: push an EventValue as a Lua value
// ---------------------------------------------------------------------------
/**
* @brief Push a single EventValue onto the Lua stack.
*
* @param L Lua state.
* @param val The EventValue to push.
*/
static void pushEventValue(lua_State *L, const EventValue &val)
{
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);
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;
}
}
}
/**
* @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.
*
* 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 value.
* @return EventValue representing the Lua value.
*/
static EventValue readLuaValue(lua_State *L, int idx)
{
int absIdx = lua_absindex(L, idx);
int type = lua_type(L, absIdx);
switch (type) {
case LUA_TNIL:
return EventValue();
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);
}
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;
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;
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);
}
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);
}
}
// 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;
}
// ---------------------------------------------------------------------------
// Lua: ecs.send_event("eventName", [params_table])
// ---------------------------------------------------------------------------
/**
* @brief Lua: ecs.send_event("eventName", [params_table]) -> nil
*
* Sends an event through the global EventBus. The optional params table
* 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);
EventParams params;
if (lua_gettop(L) >= 2 && lua_istable(L, 2)) {
params = readEventParams(L, 2);
}
EventBus::getInstance().send(eventName, params);
return 0;
}
// ---------------------------------------------------------------------------
// Lua: ecs.subscribe_event("eventName", callback_fn) -> int (subscription id)
// ---------------------------------------------------------------------------
/**
* @brief Lua: ecs.subscribe_event("eventName", callback_fn) -> int
*
* Subscribes a Lua function to an event. The callback receives:
* function(event_name, params_table)
*
* Returns a subscription ID that can be used with unsubscribe_event().
*
* Usage:
* local sub_id = ecs.subscribe_event("collision", function(event, params)
* print("Collision event received!")
* print("entity_id: " .. params.entity_id)
* end)
*/
static int luaSubscribeEvent(lua_State *L)
{
const char *eventName = luaL_checkstring(L, 1);
luaL_checktype(L, 2, LUA_TFUNCTION);
// Create a reference to the Lua callback function
lua_pushvalue(L, 2);
int callbackRef = luaL_ref(L, LUA_REGISTRYINDEX);
// 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 EventParams &params) {
// Push the Lua callback function
lua_rawgeti(L, LUA_REGISTRYINDEX, callbackRef);
// Push event name
lua_pushstring(L, eventName.c_str());
// Push params as a Lua table
pushEventParams(L, params);
// Call the Lua function (2 args, 0 results)
if (lua_pcall(L, 2, 0, 0) != LUA_OK) {
Ogre::LogManager::getSingleton().stream()
<< "Lua event callback error: "
<< lua_tostring(L, -1);
lua_pop(L, 1);
}
});
// Store the mapping from Lua subscription ID to EventBus listener ID
int luaSubId = s_nextLuaSubId++;
s_luaSubscriptions[luaSubId] = listenerId;
lua_pushinteger(L, luaSubId);
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.unsubscribe_event(subscription_id) -> nil
// ---------------------------------------------------------------------------
/**
* @brief Lua: ecs.unsubscribe_event(subscription_id) -> nil
*
* Unsubscribes a previously registered event subscription.
*
* Usage:
* ecs.unsubscribe_event(sub_id)
*/
static int luaUnsubscribeEvent(lua_State *L)
{
int luaSubId = (int)luaL_checkinteger(L, 1);
auto it = s_luaSubscriptions.find(luaSubId);
if (it != s_luaSubscriptions.end()) {
EventBus::getInstance().unsubscribe(it->second);
s_luaSubscriptions.erase(it);
}
return 0;
}
// ---------------------------------------------------------------------------
// Public registration function
// ---------------------------------------------------------------------------
void registerLuaEventApi(lua_State *L)
{
// Get or create the "ecs" global table
lua_getglobal(L, "ecs");
if (lua_isnil(L, -1)) {
lua_newtable(L);
lua_setglobal(L, "ecs");
lua_getglobal(L, "ecs");
}
// Register event functions
lua_pushcfunction(L, luaSendEvent);
lua_setfield(L, -2, "send_event");
lua_pushcfunction(L, luaSubscribeEvent);
lua_setfield(L, -2, "subscribe_event");
lua_pushcfunction(L, luaUnsubscribeEvent);
lua_setfield(L, -2, "unsubscribe_event");
// Pop the ecs table
lua_pop(L, 1);
}
} // namespace editScene

View File

@@ -0,0 +1,65 @@
#ifndef EDITSCENE_LUA_EVENT_API_HPP
#define EDITSCENE_LUA_EVENT_API_HPP
#pragma once
#include <lua.hpp>
/**
* @file LuaEventApi.hpp
* @brief Lua API for the EventBus system.
*
* Provides Lua bindings for the global EventBus singleton, allowing
* Lua scripts to subscribe to events, send events, and manage
* event subscriptions.
*
* The EventBus is a synchronous publish/subscribe system. Events are
* 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
* ecs.send_event("eventName", {key=value, ...}) -> nil
* ecs.subscribe_event("eventName", callback_fn) -> int (subscription id)
* ecs.unsubscribe_event(subscription_id) -> nil
*
* The callback function receives (event_name, params_table):
* ecs.subscribe_event("collision", function(event, params)
* print(event .. " occurred")
* print("entity_id: " .. params.entity_id)
* end)
*
* 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
{
/**
* @brief Register all event-related Lua API functions into the "ecs" table.
*
* Adds functions for event subscription, unsubscription, and sending.
* Must be called after LuaState is constructed and the "ecs" table exists.
*
* @param L The Lua state.
*/
void registerLuaEventApi(lua_State *L);
} // namespace editScene
#endif // EDITSCENE_LUA_EVENT_API_HPP

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

@@ -0,0 +1,187 @@
#include "LuaState.hpp"
#include <OgreResourceGroupManager.h>
#include <OgreLogManager.h>
#include <OgreDataStream.h>
#include <iostream>
extern "C" {
int luaopen_lpeg(lua_State *L);
}
namespace editScene
{
// ---------------------------------------------------------------------------
// Custom library loader: loads .lua files from OGRE resource groups
// ---------------------------------------------------------------------------
int LuaState::luaLibraryLoader(lua_State *L)
{
if (!lua_isstring(L, 1)) {
luaL_error(
L,
"luaLibraryLoader: Expected string for first parameter");
}
std::string libraryFile = lua_tostring(L, 1);
// Translate '.' to '/' for OGRE resource path compatibility
while (libraryFile.find('.') != std::string::npos)
libraryFile.replace(libraryFile.find('.'), 1, "/");
libraryFile += ".lua";
Ogre::DataStreamPtr stream =
Ogre::ResourceGroupManager::getSingleton().openResource(
libraryFile, "LuaScripts");
Ogre::String script = stream->getAsString();
if (luaL_loadbuffer(L, script.c_str(), script.length(),
libraryFile.c_str())) {
luaL_error(
L,
"Error loading library '%s' from resource archive.\n%s",
libraryFile.c_str(), lua_tostring(L, -1));
}
return 1;
}
// ---------------------------------------------------------------------------
// Install the custom loader into package.searchers
// ---------------------------------------------------------------------------
void LuaState::installLibraryLoader()
{
lua_getglobal(L, "table");
lua_getfield(L, -1, "insert");
lua_remove(L, -2); // table
lua_getglobal(L, "package");
lua_getfield(L, -1, "searchers");
lua_remove(L, -2); // package
lua_pushnumber(L, 1); // insert at position 1 (highest priority)
lua_pushcfunction(L, luaLibraryLoader);
if (lua_pcall(L, 3, 0, 0))
Ogre::LogManager::getSingleton().stream() << lua_tostring(L, 1);
}
// ---------------------------------------------------------------------------
// Constructor: create Lua state and open standard libraries
// ---------------------------------------------------------------------------
LuaState::LuaState()
: L(luaL_newstate())
{
luaopen_base(L);
luaopen_table(L);
luaopen_package(L);
luaL_requiref(L, "table", luaopen_table, 1);
lua_pop(L, 1);
luaL_requiref(L, "math", luaopen_math, 1);
lua_pop(L, 1);
luaL_requiref(L, "package", luaopen_package, 1);
lua_pop(L, 1);
luaL_requiref(L, "string", luaopen_string, 1);
lua_pop(L, 1);
luaL_requiref(L, "io", luaopen_io, 1);
lua_pop(L, 1);
luaL_requiref(L, "lpeg", luaopen_lpeg, 1);
lua_pop(L, 1);
installLibraryLoader();
lua_pop(L, 1);
}
// ---------------------------------------------------------------------------
// Destructor
// ---------------------------------------------------------------------------
LuaState::~LuaState()
{
lua_close(L);
}
// ---------------------------------------------------------------------------
// Handler registration
// ---------------------------------------------------------------------------
int LuaState::setupHandler()
{
luaL_checktype(L, 1, LUA_TFUNCTION);
lua_pushvalue(L, 1);
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
setupHandlers.push_back(ref);
return 0;
}
// ---------------------------------------------------------------------------
// Call all handlers with an event name (no entities)
// ---------------------------------------------------------------------------
int LuaState::callHandler(const Ogre::String &event)
{
for (int ref : setupHandlers) {
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
lua_pushstring(L, event.c_str());
lua_pushinteger(L, -1);
lua_pushinteger(L, -1);
if (lua_pcall(L, 3, 0, 0) != LUA_OK) {
Ogre::LogManager::getSingleton().stream()
<< lua_tostring(L, -1);
OgreAssert(false, "Lua error");
}
}
return 0;
}
// ---------------------------------------------------------------------------
// Call all handlers with an event name and two entities
// ---------------------------------------------------------------------------
int LuaState::callHandler(const Ogre::String &event, flecs::entity e1,
flecs::entity e2)
{
// Entity IDs are mapped through the global idmap (see LuaEntityApi.cpp)
extern int luaEntityToId(flecs::entity e);
extern flecs::entity luaIdToEntity(int id);
for (int ref : setupHandlers) {
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
lua_pushstring(L, event.c_str());
lua_pushinteger(L, luaEntityToId(e1));
lua_pushinteger(L, luaEntityToId(e2));
if (lua_pcall(L, 3, 0, 0) != LUA_OK) {
Ogre::LogManager::getSingleton().stream()
<< lua_tostring(L, -1);
OgreAssert(false, "Lua error");
}
}
return 0;
}
// ---------------------------------------------------------------------------
// Late setup: load data.lua and run initialisation
// ---------------------------------------------------------------------------
void LuaState::lateSetup()
{
Ogre::DataStreamPtr stream =
Ogre::ResourceGroupManager::getSingleton().openResource(
"data2.lua", "LuaScripts");
std::cout << "stream: " << stream->getAsString() << "\n";
if (luaL_dostring(L, stream->getAsString().c_str()) != LUA_OK) {
std::cout << "error: " << lua_tostring(L, -1) << "\n";
OgreAssert(false, "Script failure");
}
const char *lua_code = "\n\
function stuff()\n\
return 4\n\
end\n\
x = stuff()\n\
";
luaL_dostring(L, lua_code);
lua_getglobal(L, "x");
int x = lua_tonumber(L, 1);
std::cout << "lua: " << x << "\n";
}
} // namespace editScene

View File

@@ -0,0 +1,137 @@
#ifndef EDITSCENE_LUA_STATE_HPP
#define EDITSCENE_LUA_STATE_HPP
#pragma once
#include <Ogre.h>
#include <lua.hpp>
#include <flecs.h>
#include <string>
#include <vector>
/**
* @file LuaState.hpp
* @brief Lua state management for the editScene editor.
*
* Manages the Lua virtual machine instance, library loading,
* script loading from OGRE resource groups, and the custom
* package.searchers entry that loads Lua modules from
* the "LuaScripts" resource group.
*
* Usage:
* LuaState lua;
* lua.installLibraryLoader(); // Register custom searcher
* lua.lateSetup(); // Load data.lua and run startup
* lua.callHandler("event_name", e1, e2);
*/
namespace editScene
{
/**
* @brief Manages a single lua_State instance.
*
* Opens standard Lua libraries (base, table, math, package, string, io)
* plus LPEG. Installs a custom package.searchers entry that loads
* .lua files from OGRE's "LuaScripts" resource group.
*
* Provides a handler/callback system: Lua scripts can register
* callback functions via setup_handler(), and C++ code can invoke
* them with event names and optional entity parameters.
*/
class LuaState {
public:
/**
* @brief Construct a new Lua state.
*
* Opens all standard libraries and installs the custom
* library loader for OGRE resource-based Lua modules.
*/
LuaState();
/**
* @brief Destroy the Lua state and close the VM.
*/
~LuaState();
/**
* @brief Get the underlying lua_State pointer.
*/
lua_State *getState() const
{
return L;
}
/**
* @brief Install the custom library loader into package.searchers.
*
* Inserts luaLibraryLoader at position 1 of the searchers table
* so it takes precedence over the default file-system loader.
* This allows Lua's require() to load modules from OGRE resource
* archives.
*/
void installLibraryLoader();
/**
* @brief Late setup: load data.lua and run initialisation.
*
* Opens "data.lua" from the "LuaScripts" resource group and
* executes it. Also runs a simple inline Lua test snippet.
* Called once after the ECS world is fully initialised.
*/
void lateSetup();
/**
* @brief Register a Lua callback function for event handling.
*
* Expects a Lua function on the stack (at index 1).
* Stores a reference to it in the registry so it can be
* called later via callHandler().
*
* @return int Reference index (stored internally).
*/
int setupHandler();
/**
* @brief Call all registered handlers with an event name.
*
* @param event The event name string.
* @return int 0 on success.
*/
int callHandler(const Ogre::String &event);
/**
* @brief Call all registered handlers with an event name and
* two entity IDs.
*
* Entity IDs are mapped through the global idmap (see LuaEntityApi).
*
* @param event The event name string.
* @param e1 First entity (e.g. subject).
* @param e2 Second entity (e.g. object).
* @return int 0 on success.
*/
int callHandler(const Ogre::String &event, flecs::entity e1,
flecs::entity e2);
private:
lua_State *L;
/** Registry references for registered Lua callback functions. */
std::vector<int> setupHandlers;
/**
* @brief Custom Lua loader function for OGRE resource-based modules.
*
* Registered in package.searchers. Translates dots to path
* separators, appends ".lua", and loads the file from the
* "LuaScripts" OGRE resource group.
*
* @param L Lua state.
* @return int 1 (pushes loaded chunk onto stack) or error.
*/
static int luaLibraryLoader(lua_State *L);
};
} // namespace editScene
#endif // EDITSCENE_LUA_STATE_HPP

View File

@@ -790,10 +790,10 @@ public:
node->_setDerivedOrientation(JoltPhysics::convert(q));
}
for (JPH::Character *ch : characters) {
if (body_interface.IsAdded(ch->GetBodyID())) {
JPH::BodyID bID = ch->GetBodyID();
if (body_interface.IsAdded(bID)) {
ch->PostSimulation(0.1f);
Ogre::SceneNode *node =
id2node[ch->GetBodyID()];
Ogre::SceneNode *node = id2node[bID];
if (node)
node->_setDerivedPosition(
JoltPhysics::convert(
@@ -1574,11 +1574,10 @@ public:
MyCollector collector(&physics_system, surface_point,
JPH::Vec3::sAxisY(), dt);
// Apply buoyancy to all bodies that intersect with the water
// Create a symmetrical box around the surface point:
// 1000 units in X/Z, 1.0 unit above and below in Y
// This detects bodies slightly above and well below water surface
JPH::AABox water_box(-JPH::Vec3(1000, 1.0f, 1000),
JPH::Vec3(1000, 1000, 1000));
// Detects bodies up to 0.1 units above the surface point
// and up to 1000 units below (deep underwater)
JPH::AABox water_box(-JPH::Vec3(1000, 1000, 1000),
JPH::Vec3(1000, 0.1f, 1000));
water_box.Translate(JPH::Vec3(surface_point));
physics_system.GetBroadPhaseQuery().CollideAABox(
water_box, collector,
@@ -1634,6 +1633,13 @@ public:
return it->second;
return nullptr;
}
void setRootMotionCharacter(JPH::BodyID id, bool enabled)
{
(void)id;
(void)enabled;
/* No longer needed - root motion drives physics velocity,
* and physics writes position back to scene node normally. */
}
};
void physics()
@@ -1977,5 +1983,10 @@ JoltPhysicsWrapper::getSceneNodeFromBodyID(JPH::BodyID id) const
return phys->getSceneNodeFromBodyID(id);
}
void JoltPhysicsWrapper::setRootMotionCharacter(JPH::BodyID id, bool enabled)
{
phys->setRootMotionCharacter(id, enabled);
}
template <>
JoltPhysicsWrapper *Ogre::Singleton<JoltPhysicsWrapper>::msSingleton = 0;

View File

@@ -230,5 +230,12 @@ public:
bool bodyIsCharacter(JPH::BodyID id) const;
void destroyCharacter(std::shared_ptr<JPH::Character> ch);
Ogre::SceneNode *getSceneNodeFromBodyID(JPH::BodyID id) const;
/* Mark a character body as root-motion-driven.
* When true, Physics::update() will NOT write the character's
* position back to the scene node after the physics step,
* because the scene node position is driven by root motion
* from AnimationTreeSystem. */
void setRootMotionCharacter(JPH::BodyID id, bool enabled);
};
#endif

View File

@@ -0,0 +1,279 @@
//
// Copyright (c) 2009-2010 Mikko Mononen memon@inside.org
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.
//
#include "PartitionedMesh.hpp"
#include <cmath>
#include <cstdlib>
struct IndexedBounds
{
float bmin[2];
float bmax[2];
int index;
};
namespace
{
int compareMinX(const void* va, const void* vb)
{
return static_cast<int>(static_cast<const IndexedBounds*>(va)->bmin[0] - static_cast<const IndexedBounds*>(vb)->bmin[0]);
}
int compareMinY(const void* va, const void* vb)
{
return static_cast<int>(static_cast<const IndexedBounds*>(va)->bmin[1] - static_cast<const IndexedBounds*>(vb)->bmin[1]);
}
/// Calculates the total extent of all bounds in the given index range
void calcTotalBounds(const std::vector<IndexedBounds> bounds, const int start, const int end, float* outBMin, float* outBMax)
{
outBMin[0] = bounds[start].bmin[0];
outBMin[1] = bounds[start].bmin[1];
outBMax[0] = bounds[start].bmax[0];
outBMax[1] = bounds[start].bmax[1];
for (int boundIndex = start + 1; boundIndex < end; ++boundIndex)
{
const IndexedBounds& it = bounds[boundIndex];
outBMin[0] = std::min(it.bmin[0], outBMin[0]);
outBMin[1] = std::min(it.bmin[1], outBMin[1]);
outBMax[0] = std::max(it.bmax[0], outBMax[0]);
outBMax[1] = std::max(it.bmax[1], outBMax[1]);
}
}
void subdivide(
std::vector<IndexedBounds> triBounds,
int imin,
int imax,
int trisPerChunk,
int& curNode,
PartitionedMesh::Node* nodes,
const int maxNodes,
int& curTri,
int* outTris,
const int* inTris)
{
const int numTriBoundsInRange = imax - imin;
const int icur = curNode;
if (curNode >= maxNodes)
{
return;
}
PartitionedMesh::Node& node = nodes[curNode];
curNode++;
if (numTriBoundsInRange <= trisPerChunk) // Leaf
{
// Get total bounds of all triangles
calcTotalBounds(triBounds, imin, imax, node.bmin, node.bmax);
// Copy triangles.
node.triIndex = curTri;
node.numTris = numTriBoundsInRange;
for (int triIndex = imin; triIndex < imax; ++triIndex)
{
const int* src = &inTris[triBounds[triIndex].index * 3];
int* dst = &outTris[curTri * 3];
curTri++;
dst[0] = src[0];
dst[1] = src[1];
dst[2] = src[2];
}
}
else
{
// Split
calcTotalBounds(triBounds, imin, imax, node.bmin, node.bmax);
float xLength = node.bmax[0] - node.bmin[0];
float yLength = node.bmax[1] - node.bmin[1];
// Sort along the longest axis
qsort(
triBounds.data() + imin,
static_cast<size_t>(numTriBoundsInRange),
sizeof(IndexedBounds),
(xLength >= yLength) ? compareMinX : compareMinY);
int isplit = imin + numTriBoundsInRange / 2;
// Left
subdivide(triBounds, imin, isplit, trisPerChunk, curNode, nodes, maxNodes, curTri, outTris, inTris);
// Right
subdivide(triBounds, isplit, imax, trisPerChunk, curNode, nodes, maxNodes, curTri, outTris, inTris);
// Negative index means escape.
node.triIndex = icur - curNode;
}
}
bool checkOverlapRect(const float amin[2], const float amax[2], const float bmin[2], const float bmax[2])
{
return amin[0] <= bmax[0] && amax[0] >= bmin[0] && amin[1] <= bmax[1] && amax[1] >= bmin[1];
}
bool checkOverlapSegment(const float p[2], const float q[2], const float bmin[2], const float bmax[2])
{
float tmin = 0;
float tmax = 1;
float d[]{q[0] - p[0], q[1] - p[1]};
for (int i = 0; i < 2; i++)
{
static const float EPSILON = 1e-6f;
if (fabsf(d[i]) < EPSILON)
{
// Ray is parallel to slab. No hit if origin not within slab
if (p[i] < bmin[i] || p[i] > bmax[i])
{
return false;
}
}
else
{
// Compute intersection t value of ray with near and far plane of slab
float ood = 1.0f / d[i];
float t1 = (bmin[i] - p[i]) * ood;
float t2 = (bmax[i] - p[i]) * ood;
if (t1 > t2)
{
float tmp = t1;
t1 = t2;
t2 = tmp;
}
if (t1 > tmin)
{
tmin = t1;
}
if (t2 < tmax)
{
tmax = t2;
}
if (tmin > tmax)
{
return false;
}
}
}
return true;
}
}
void PartitionedMesh::PartitionMesh(const float* verts, const int* tris, int numTris, int trisPerChunk)
{
// Calculate the XZ bounds of every triangle.
std::vector<IndexedBounds> triBounds;
triBounds.resize(numTris);
for (int triIndex = 0; triIndex < numTris; triIndex++)
{
const int* tri = &tris[triIndex * 3];
IndexedBounds& bound = triBounds[triIndex];
bound.index = triIndex;
bound.bmin[0] = bound.bmax[0] = verts[tri[0] * 3 + 0];
bound.bmin[1] = bound.bmax[1] = verts[tri[0] * 3 + 2];
for (int vertIndex = 1; vertIndex < 3; ++vertIndex)
{
const float x = verts[tri[vertIndex] * 3 + 0];
bound.bmin[0] = std::min(x, bound.bmin[0]);
bound.bmax[0] = std::max(x, bound.bmax[0]);
const float z = verts[tri[vertIndex] * 3 + 2];
bound.bmin[1] = std::min(z, bound.bmin[1]);
bound.bmax[1] = std::max(z, bound.bmax[1]);
}
}
// Build tree
int numChunks = static_cast<int>(ceilf(static_cast<float>(numTris) / static_cast<float>(trisPerChunk)));
nodes.resize(numChunks * 4);
this->tris.resize(numTris * 3);
int curTri = 0;
int curNode = 0;
subdivide(triBounds, 0, numTris, trisPerChunk, curNode, nodes.data(), numChunks * 4, curTri, this->tris.data(), tris);
nnodes = curNode;
// Calc max tris per chunk.
maxTrisPerChunk = 0;
for (auto& node : nodes)
{
// Skip if it's not a leaf node
if (node.triIndex < 0)
{
continue;
}
maxTrisPerChunk = std::max(maxTrisPerChunk, node.numTris);
}
}
void PartitionedMesh::GetNodesOverlappingRect(float bmin[2], float bmax[2], std::vector<int>& outNodes) const
{
// Traverse tree
for (int nodeIndex = 0; nodeIndex < this->nnodes;)
{
const Node* node = &this->nodes[nodeIndex];
const bool overlap = checkOverlapRect(bmin, bmax, node->bmin, node->bmax);
const bool isLeafNode = node->triIndex >= 0;
if (isLeafNode && overlap)
{
outNodes.emplace_back(nodeIndex);
}
if (overlap || isLeafNode)
{
nodeIndex++;
}
else
{
// escape index
nodeIndex -= node->triIndex;
}
}
}
void PartitionedMesh::GetNodesOverlappingSegment(float start[2], float end[2], std::vector<int>& outNodes) const
{
// Traverse tree
for (int nodeIndex = 0; nodeIndex < this->nnodes;)
{
const Node* node = &this->nodes[nodeIndex];
const bool overlap = checkOverlapSegment(start, end, node->bmin, node->bmax);
const bool isLeafNode = node->triIndex >= 0;
if (isLeafNode && overlap)
{
outNodes.emplace_back(nodeIndex);
}
if (overlap || isLeafNode)
{
nodeIndex++;
}
else
{
// escape index
nodeIndex -= node->triIndex;
}
}
}

View File

@@ -0,0 +1,50 @@
//
// Copyright (c) 2009-2010 Mikko Mononen memon@inside.org
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.
//
#pragma once
#include <vector>
/// A spatially-partitioned mesh (k/d tree),
/// where each node contains at max trisPerChunk triangles.
struct PartitionedMesh
{
struct Node
{
// xy bounds
float bmin[2];
float bmax[2];
int triIndex;
int numTris;
};
std::vector<Node> nodes{};
int nnodes = 0;
std::vector<int> tris{};
int maxTrisPerChunk = 0;
void PartitionMesh(const float* verts, const int* tris, int numTris, int trisPerChunk);
/// Finds the chunk indices that overlap the input rectangle.
void GetNodesOverlappingRect(float bmin[2], float bmax[2], std::vector<int>& outNodes) const;
/// Returns the chunk indices which overlap the input segment.
void GetNodesOverlappingSegment(float start[2], float end[2], std::vector<int>& outNodes) const;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,120 @@
#ifndef EDITSCENE_TILECACHE_NAVMESH_HPP
#define EDITSCENE_TILECACHE_NAVMESH_HPP
#pragma once
#include <Ogre.h>
#include <vector>
#include <memory>
// Forward declarations
struct rcContext;
struct dtNavMesh;
struct dtNavMeshQuery;
struct dtQueryFilter;
struct dtTileCache;
struct dtTileCacheAlloc;
struct dtTileCacheCompressor;
struct dtTileCacheMeshProcess;
class PartitionedMesh;
/**
* Self-contained DetourTileCache navmesh builder and query engine.
*
* Builds a tiled navmesh from Ogre::Entity geometry. Supports partial
* tile rebuilds when geometry in a specific area changes.
*/
class TileCacheNavMesh {
public:
struct BuildParams {
float cellSize = 0.3f;
float cellHeight = 0.2f;
float agentHeight = 2.5f;
float agentRadius = 0.5f;
float agentMaxClimb = 1.0f;
float agentMaxSlope = 20.0f;
float edgeMaxLen = 12.0f;
float edgeMaxError = 1.3f;
float regionMinSize = 20.0f;
float regionMergeSize = 20.0f;
int vertsPerPoly = 6;
float detailSampleDist = 6.0f;
float detailSampleMaxError = 1.0f;
int tileSize = 48; // voxels per tile
};
TileCacheNavMesh(Ogre::SceneManager *sceneMgr,
const BuildParams &params);
~TileCacheNavMesh();
TileCacheNavMesh(const TileCacheNavMesh &) = delete;
TileCacheNavMesh &operator=(const TileCacheNavMesh &) = delete;
// --- Build ---
bool build(const std::vector<Ogre::Entity *> &entities);
bool isBuilt() const { return m_navMesh != nullptr; }
// --- Partial rebuild ---
void rebuildTilesInArea(const Ogre::AxisAlignedBox &area);
// --- Queries ---
bool findPath(const Ogre::Vector3 &start,
const Ogre::Vector3 &end,
std::vector<Ogre::Vector3> &path);
bool findNearestPoint(const Ogre::Vector3 &pos,
Ogre::Vector3 &out);
Ogre::Vector3 getRandomPoint();
// --- Debug ---
void drawNavMesh();
void clearDebugDraw();
private:
Ogre::SceneManager *m_sceneMgr;
BuildParams m_params;
// Input geometry
std::vector<float> m_verts;
std::vector<int> m_tris;
float m_bmin[3];
float m_bmax[3];
std::unique_ptr<PartitionedMesh> m_partitionedMesh;
// Tile cache
dtTileCacheAlloc *m_talloc;
dtTileCacheCompressor *m_tcomp;
dtTileCacheMeshProcess *m_tmproc;
dtTileCache *m_tileCache;
// Detour output
dtNavMesh *m_navMesh;
dtNavMeshQuery *m_navQuery;
dtQueryFilter *m_filter;
// Tile grid dimensions
int m_tw = 0;
int m_th = 0;
float m_cellSize = 0;
// Recast context
rcContext *m_ctx;
// Debug draw
Ogre::ManualObject *m_debugMO;
Ogre::SceneNode *m_debugNode;
// Extents for nearest-poly searches
float m_extents[3];
// Internal helpers
bool extractMeshData(const std::vector<Ogre::Entity *> &entities);
bool initTileCache();
int rasterizeTileLayers(int tx, int ty,
struct TileCacheData *tiles,
int maxTiles);
bool buildTile(int tx, int ty);
bool removeTile(int tx, int ty);
void getTileCoords(const float *pos, int &tx, int &ty);
};
#endif // EDITSCENE_TILECACHE_NAVMESH_HPP

View File

@@ -0,0 +1,556 @@
/*
FastLZ - lightning-fast lossless compression library
Copyright (C) 2007 Ariya Hidayat (ariya@kde.org)
Copyright (C) 2006 Ariya Hidayat (ariya@kde.org)
Copyright (C) 2005 Ariya Hidayat (ariya@kde.org)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#if !defined(FASTLZ__COMPRESSOR) && !defined(FASTLZ_DECOMPRESSOR)
/*
* Always check for bound when decompressing.
* Generally it is best to leave it defined.
*/
#define FASTLZ_SAFE
/*
* Give hints to the compiler for branch prediction optimization.
*/
#if defined(__GNUC__) && (__GNUC__ > 2)
#define FASTLZ_EXPECT_CONDITIONAL(c) (__builtin_expect((c), 1))
#define FASTLZ_UNEXPECT_CONDITIONAL(c) (__builtin_expect((c), 0))
#else
#define FASTLZ_EXPECT_CONDITIONAL(c) (c)
#define FASTLZ_UNEXPECT_CONDITIONAL(c) (c)
#endif
/*
* Use inlined functions for supported systems.
*/
#if defined(__GNUC__) || defined(__DMC__) || defined(__POCC__) || defined(__WATCOMC__) || defined(__SUNPRO_C)
#define FASTLZ_INLINE inline
#elif defined(__BORLANDC__) || defined(_MSC_VER) || defined(__LCC__)
#define FASTLZ_INLINE __inline
#else
#define FASTLZ_INLINE
#endif
/*
* Prevent accessing more than 8-bit at once, except on x86 architectures.
*/
#if !defined(FASTLZ_STRICT_ALIGN)
#define FASTLZ_STRICT_ALIGN
#if defined(__i386__) || defined(__386) /* GNU C, Sun Studio */
#undef FASTLZ_STRICT_ALIGN
#elif defined(__i486__) || defined(__i586__) || defined(__i686__) /* GNU C */
#undef FASTLZ_STRICT_ALIGN
#elif defined(_M_IX86) /* Intel, MSVC */
#undef FASTLZ_STRICT_ALIGN
#elif defined(__386)
#undef FASTLZ_STRICT_ALIGN
#elif defined(_X86_) /* MinGW */
#undef FASTLZ_STRICT_ALIGN
#elif defined(__I86__) /* Digital Mars */
#undef FASTLZ_STRICT_ALIGN
#endif
#endif
/*
* FIXME: use preprocessor magic to set this on different platforms!
*/
typedef unsigned char flzuint8;
typedef unsigned short flzuint16;
typedef unsigned int flzuint32;
/* Disable "conversion from A to B, possible loss of data" warning when using MSVC */
#if defined(_MSC_VER)
#pragma warning(disable: 4244)
#endif
/* prototypes */
int fastlz_compress(const void* input, int length, void* output);
int fastlz_compress_level(int level, const void* input, int length, void* output);
int fastlz_decompress(const void* input, int length, void* output, int maxout);
#define MAX_COPY 32
#define MAX_LEN 264 /* 256 + 8 */
#define MAX_DISTANCE 8192
#if !defined(FASTLZ_STRICT_ALIGN)
#define FASTLZ_READU16(p) *((const flzuint16*)(p))
#else
#define FASTLZ_READU16(p) ((p)[0] | (p)[1]<<8)
#endif
#define HASH_LOG 13
#define HASH_SIZE (1<< HASH_LOG)
#define HASH_MASK (HASH_SIZE-1)
#define HASH_FUNCTION(v,p) { v = FASTLZ_READU16(p); v ^= FASTLZ_READU16(p+1)^(v>>(16-HASH_LOG));v &= HASH_MASK; }
#undef FASTLZ_LEVEL
#define FASTLZ_LEVEL 1
#undef FASTLZ_COMPRESSOR
#undef FASTLZ_DECOMPRESSOR
#define FASTLZ_COMPRESSOR fastlz1_compress
#define FASTLZ_DECOMPRESSOR fastlz1_decompress
static FASTLZ_INLINE int FASTLZ_COMPRESSOR(const void* input, int length, void* output);
static FASTLZ_INLINE int FASTLZ_DECOMPRESSOR(const void* input, int length, void* output, int maxout);
#include "fastlz.c"
#undef FASTLZ_LEVEL
#define FASTLZ_LEVEL 2
#undef MAX_DISTANCE
#define MAX_DISTANCE 8191
#define MAX_FARDISTANCE (65535+MAX_DISTANCE-1)
#undef FASTLZ_COMPRESSOR
#undef FASTLZ_DECOMPRESSOR
#define FASTLZ_COMPRESSOR fastlz2_compress
#define FASTLZ_DECOMPRESSOR fastlz2_decompress
static FASTLZ_INLINE int FASTLZ_COMPRESSOR(const void* input, int length, void* output);
static FASTLZ_INLINE int FASTLZ_DECOMPRESSOR(const void* input, int length, void* output, int maxout);
#include "fastlz.c"
int fastlz_compress(const void* input, int length, void* output)
{
/* for short block, choose fastlz1 */
if(length < 65536)
return fastlz1_compress(input, length, output);
/* else... */
return fastlz2_compress(input, length, output);
}
int fastlz_decompress(const void* input, int length, void* output, int maxout)
{
/* magic identifier for compression level */
int level = ((*(const flzuint8*)input) >> 5) + 1;
if(level == 1)
return fastlz1_decompress(input, length, output, maxout);
if(level == 2)
return fastlz2_decompress(input, length, output, maxout);
/* unknown level, trigger error */
return 0;
}
int fastlz_compress_level(int level, const void* input, int length, void* output)
{
if(level == 1)
return fastlz1_compress(input, length, output);
if(level == 2)
return fastlz2_compress(input, length, output);
return 0;
}
#else /* !defined(FASTLZ_COMPRESSOR) && !defined(FASTLZ_DECOMPRESSOR) */
static FASTLZ_INLINE int FASTLZ_COMPRESSOR(const void* input, int length, void* output)
{
const flzuint8* ip = (const flzuint8*) input;
const flzuint8* ip_bound = ip + length - 2;
const flzuint8* ip_limit = ip + length - 12;
flzuint8* op = (flzuint8*) output;
const flzuint8* htab[HASH_SIZE];
const flzuint8** hslot;
flzuint32 hval;
flzuint32 copy;
/* sanity check */
if(FASTLZ_UNEXPECT_CONDITIONAL(length < 4))
{
if(length)
{
/* create literal copy only */
*op++ = length-1;
ip_bound++;
while(ip <= ip_bound)
*op++ = *ip++;
return length+1;
}
else
return 0;
}
/* initializes hash table */
for (hslot = htab; hslot < htab + HASH_SIZE; hslot++)
*hslot = ip;
/* we start with literal copy */
copy = 2;
*op++ = MAX_COPY-1;
*op++ = *ip++;
*op++ = *ip++;
/* main loop */
while(FASTLZ_EXPECT_CONDITIONAL(ip < ip_limit))
{
const flzuint8* ref;
flzuint32 distance;
/* minimum match length */
flzuint32 len = 3;
/* comparison starting-point */
const flzuint8* anchor = ip;
/* check for a run */
#if FASTLZ_LEVEL==2
if(ip[0] == ip[-1] && FASTLZ_READU16(ip-1)==FASTLZ_READU16(ip+1))
{
distance = 1;
ip += 3;
ref = anchor - 1 + 3;
goto match;
}
#endif
/* find potential match */
HASH_FUNCTION(hval,ip);
hslot = htab + hval;
ref = htab[hval];
/* calculate distance to the match */
distance = anchor - ref;
/* update hash table */
*hslot = anchor;
/* is this a match? check the first 3 bytes */
if(distance==0 ||
#if FASTLZ_LEVEL==1
(distance >= MAX_DISTANCE) ||
#else
(distance >= MAX_FARDISTANCE) ||
#endif
*ref++ != *ip++ || *ref++!=*ip++ || *ref++!=*ip++)
goto literal;
#if FASTLZ_LEVEL==2
/* far, needs at least 5-byte match */
if(distance >= MAX_DISTANCE)
{
if(*ip++ != *ref++ || *ip++!= *ref++)
goto literal;
len += 2;
}
match:
#endif
/* last matched byte */
ip = anchor + len;
/* distance is biased */
distance--;
if(!distance)
{
/* zero distance means a run */
flzuint8 x = ip[-1];
while(ip < ip_bound)
if(*ref++ != x) break; else ip++;
}
else
for(;;)
{
/* safe because the outer check against ip limit */
if(*ref++ != *ip++) break;
if(*ref++ != *ip++) break;
if(*ref++ != *ip++) break;
if(*ref++ != *ip++) break;
if(*ref++ != *ip++) break;
if(*ref++ != *ip++) break;
if(*ref++ != *ip++) break;
if(*ref++ != *ip++) break;
while(ip < ip_bound)
if(*ref++ != *ip++) break;
break;
}
/* if we have copied something, adjust the copy count */
if(copy)
/* copy is biased, '0' means 1 byte copy */
*(op-copy-1) = copy-1;
else
/* back, to overwrite the copy count */
op--;
/* reset literal counter */
copy = 0;
/* length is biased, '1' means a match of 3 bytes */
ip -= 3;
len = ip - anchor;
/* encode the match */
#if FASTLZ_LEVEL==2
if(distance < MAX_DISTANCE)
{
if(len < 7)
{
*op++ = (len << 5) + (distance >> 8);
*op++ = (distance & 255);
}
else
{
*op++ = (7 << 5) + (distance >> 8);
for(len-=7; len >= 255; len-= 255)
*op++ = 255;
*op++ = len;
*op++ = (distance & 255);
}
}
else
{
/* far away, but not yet in the another galaxy... */
if(len < 7)
{
distance -= MAX_DISTANCE;
*op++ = (len << 5) + 31;
*op++ = 255;
*op++ = distance >> 8;
*op++ = distance & 255;
}
else
{
distance -= MAX_DISTANCE;
*op++ = (7 << 5) + 31;
for(len-=7; len >= 255; len-= 255)
*op++ = 255;
*op++ = len;
*op++ = 255;
*op++ = distance >> 8;
*op++ = distance & 255;
}
}
#else
if(FASTLZ_UNEXPECT_CONDITIONAL(len > MAX_LEN-2))
while(len > MAX_LEN-2)
{
*op++ = (7 << 5) + (distance >> 8);
*op++ = MAX_LEN - 2 - 7 -2;
*op++ = (distance & 255);
len -= MAX_LEN-2;
}
if(len < 7)
{
*op++ = (len << 5) + (distance >> 8);
*op++ = (distance & 255);
}
else
{
*op++ = (7 << 5) + (distance >> 8);
*op++ = len - 7;
*op++ = (distance & 255);
}
#endif
/* update the hash at match boundary */
HASH_FUNCTION(hval,ip);
htab[hval] = ip++;
HASH_FUNCTION(hval,ip);
htab[hval] = ip++;
/* assuming literal copy */
*op++ = MAX_COPY-1;
continue;
literal:
*op++ = *anchor++;
ip = anchor;
copy++;
if(FASTLZ_UNEXPECT_CONDITIONAL(copy == MAX_COPY))
{
copy = 0;
*op++ = MAX_COPY-1;
}
}
/* left-over as literal copy */
ip_bound++;
while(ip <= ip_bound)
{
*op++ = *ip++;
copy++;
if(copy == MAX_COPY)
{
copy = 0;
*op++ = MAX_COPY-1;
}
}
/* if we have copied something, adjust the copy length */
if(copy)
*(op-copy-1) = copy-1;
else
op--;
#if FASTLZ_LEVEL==2
/* marker for fastlz2 */
*(flzuint8*)output |= (1 << 5);
#endif
return op - (flzuint8*)output;
}
static FASTLZ_INLINE int FASTLZ_DECOMPRESSOR(const void* input, int length, void* output, int maxout)
{
const flzuint8* ip = (const flzuint8*) input;
const flzuint8* ip_limit = ip + length;
flzuint8* op = (flzuint8*) output;
flzuint8* op_limit = op + maxout;
flzuint32 ctrl = (*ip++) & 31;
int loop = 1;
do
{
const flzuint8* ref = op;
flzuint32 len = ctrl >> 5;
flzuint32 ofs = (ctrl & 31) << 8;
if(ctrl >= 32)
{
#if FASTLZ_LEVEL==2
flzuint8 code;
#endif
len--;
ref -= ofs;
if (len == 7-1)
#if FASTLZ_LEVEL==1
len += *ip++;
ref -= *ip++;
#else
do
{
code = *ip++;
len += code;
} while (code==255);
code = *ip++;
ref -= code;
/* match from 16-bit distance */
if(FASTLZ_UNEXPECT_CONDITIONAL(code==255))
if(FASTLZ_EXPECT_CONDITIONAL(ofs==(31 << 8)))
{
ofs = (*ip++) << 8;
ofs += *ip++;
ref = op - ofs - MAX_DISTANCE;
}
#endif
#ifdef FASTLZ_SAFE
if (FASTLZ_UNEXPECT_CONDITIONAL(op + len + 3 > op_limit))
return 0;
if (FASTLZ_UNEXPECT_CONDITIONAL(ref-1 < (flzuint8 *)output))
return 0;
#endif
if(FASTLZ_EXPECT_CONDITIONAL(ip < ip_limit))
ctrl = *ip++;
else
loop = 0;
if(ref == op)
{
/* optimize copy for a run */
flzuint8 b = ref[-1];
*op++ = b;
*op++ = b;
*op++ = b;
for(; len; --len)
*op++ = b;
}
else
{
#if !defined(FASTLZ_STRICT_ALIGN)
const flzuint16* p;
flzuint16* q;
#endif
/* copy from reference */
ref--;
*op++ = *ref++;
*op++ = *ref++;
*op++ = *ref++;
#if !defined(FASTLZ_STRICT_ALIGN)
/* copy a byte, so that now it's word aligned */
if(len & 1)
{
*op++ = *ref++;
len--;
}
/* copy 16-bit at once */
q = (flzuint16*) op;
op += len;
p = (const flzuint16*) ref;
for(len>>=1; len > 4; len-=4)
{
*q++ = *p++;
*q++ = *p++;
*q++ = *p++;
*q++ = *p++;
}
for(; len; --len)
*q++ = *p++;
#else
for(; len; --len)
*op++ = *ref++;
#endif
}
}
else
{
ctrl++;
#ifdef FASTLZ_SAFE
if (FASTLZ_UNEXPECT_CONDITIONAL(op + ctrl > op_limit))
return 0;
if (FASTLZ_UNEXPECT_CONDITIONAL(ip + ctrl > ip_limit))
return 0;
#endif
*op++ = *ip++;
for(--ctrl; ctrl; ctrl--)
*op++ = *ip++;
loop = FASTLZ_EXPECT_CONDITIONAL(ip < ip_limit);
if(loop)
ctrl = *ip++;
}
}
while(FASTLZ_EXPECT_CONDITIONAL(loop));
return op - (flzuint8*)output;
}
#endif /* !defined(FASTLZ_COMPRESSOR) && !defined(FASTLZ_DECOMPRESSOR) */

View File

@@ -0,0 +1,100 @@
/*
FastLZ - lightning-fast lossless compression library
Copyright (C) 2007 Ariya Hidayat (ariya@kde.org)
Copyright (C) 2006 Ariya Hidayat (ariya@kde.org)
Copyright (C) 2005 Ariya Hidayat (ariya@kde.org)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#ifndef FASTLZ_H
#define FASTLZ_H
#define FASTLZ_VERSION 0x000100
#define FASTLZ_VERSION_MAJOR 0
#define FASTLZ_VERSION_MINOR 0
#define FASTLZ_VERSION_REVISION 0
#define FASTLZ_VERSION_STRING "0.1.0"
#if defined (__cplusplus)
extern "C" {
#endif
/**
Compress a block of data in the input buffer and returns the size of
compressed block. The size of input buffer is specified by length. The
minimum input buffer size is 16.
The output buffer must be at least 5% larger than the input buffer
and can not be smaller than 66 bytes.
If the input is not compressible, the return value might be larger than
length (input buffer size).
The input buffer and the output buffer can not overlap.
*/
int fastlz_compress(const void* input, int length, void* output);
/**
Decompress a block of compressed data and returns the size of the
decompressed block. If error occurs, e.g. the compressed data is
corrupted or the output buffer is not large enough, then 0 (zero)
will be returned instead.
The input buffer and the output buffer can not overlap.
Decompression is memory safe and guaranteed not to write the output buffer
more than what is specified in maxout.
*/
int fastlz_decompress(const void* input, int length, void* output, int maxout);
/**
Compress a block of data in the input buffer and returns the size of
compressed block. The size of input buffer is specified by length. The
minimum input buffer size is 16.
The output buffer must be at least 5% larger than the input buffer
and can not be smaller than 66 bytes.
If the input is not compressible, the return value might be larger than
length (input buffer size).
The input buffer and the output buffer can not overlap.
Compression level can be specified in parameter level. At the moment,
only level 1 and level 2 are supported.
Level 1 is the fastest compression and generally useful for short data.
Level 2 is slightly slower but it gives better compression ratio.
Note that the compressed data, regardless of the level, can always be
decompressed using the function fastlz_decompress above.
*/
int fastlz_compress_level(int level, const void* input, int length, void* output);
#if defined (__cplusplus)
}
#endif
#endif /* FASTLZ_H */

View File

@@ -0,0 +1,86 @@
---
# clang-format settings
Language: Cpp
BasedOnStyle: LLVM
Standard: Auto
ForEachMacros: [ for ]
# indentation
TabWidth: 4
IndentWidth: 4
UseTab: AlignWithSpaces
AccessModifierOffset: -4
ContinuationIndentWidth: 4
IndentCaseLabels: false
# whitespace
SpaceAfterCStyleCast: false
SpacesBeforeTrailingComments: 2
KeepEmptyLines:
AtEndOfFile: false
AtStartOfBlock: false
AtStartOfFile: false
# line breaks
AllowShortFunctionsOnASingleLine: Inline
AllowShortBlocksOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowAllParametersOfDeclarationOnNextLine: false
AlwaysBreakBeforeMultilineStrings: true
AlwaysBreakTemplateDeclarations: true
BreakAfterReturnType: Automatic
PenaltyReturnTypeOnItsOwnLine: 999999
# constructor initializer lists
PackConstructorInitializers: CurrentLine
BreakConstructorInitializers: BeforeComma
ConstructorInitializerIndentWidth: 0
# function calls
BinPackArguments: false
AllowAllArgumentsOnNextLine: false
# function declarations
BinPackParameters: false
AlignAfterOpenBracket: AlwaysBreak
BreakBeforeBraces: Allman
# style
InsertBraces: true
PointerAlignment: Left
CompactNamespaces: true
ColumnLimit: 128
AlignEscapedNewlines: LeftWithLastLine
AlignArrayOfStructures: Left
FixNamespaceComments: false
# includes & preprocessor
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '.*(PCH).*'
Priority: -1
- Regex: '".*"'
Priority: 1
- Regex: '^<.*\.(h)>'
Priority: 3
- Regex: '^<.*>'
Priority: 4
IncludeIsMainRegex: '([-_](test|unittest))?$'
IndentPPDirectives: AfterHash
# Hints for detecting supported languages code blocks in raw strings
RawStringFormats:
- Language: Cpp
Delimiters:
- cc
- CC
- cpp
- Cpp
- CPP
- 'c++'
- 'C++'
CanonicalDelimiter: ''
BasedOnStyle: google
...

View File

@@ -0,0 +1,3 @@
Checks: 'clang-analyzer-*'
WarningsAsErrors: '*'
HeaderFilterRegex: '.*'

View File

@@ -0,0 +1,12 @@
# editorconfig.org
# top-most EditorConfig file
root = true
[*]
indent_size = 4
indent_style = tab
[*.yml]
indent_size = 2
indent_style = space

View File

@@ -0,0 +1,11 @@
name: 'Setup SDL2 on Linux'
description: 'Installs SDL2 and OpenGL development libraries for Linux builds'
runs:
using: 'composite'
steps:
- name: Install SDL2 and OpenGL
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y libgl1-mesa-dev libglu1-mesa-dev libsdl2-dev

View File

@@ -0,0 +1,16 @@
name: 'Setup SDL2 on macOS'
description: 'Downloads and installs SDL2 framework for macOS builds'
runs:
using: 'composite'
steps:
- name: Download & install SDL
shell: bash
run: |
curl -L -o SDL2.dmg https://github.com/libsdl-org/SDL/releases/download/release-2.32.10/SDL2-2.32.10.dmg
hdiutil attach SDL2.dmg
cp -r /Volumes/SDL2/SDL2.framework ${{github.workspace}}/RecastDemo/Bin/SDL2.framework
hdiutil detach /Volumes/SDL2
rm SDL2.dmg
# Create symlink so <SDL2/SDL.h> style includes work (used internally by SDL headers)
ln -s SDL2.framework/Headers ${{github.workspace}}/RecastDemo/Bin/SDL2

View File

@@ -0,0 +1,14 @@
name: 'Setup SDL2 on Windows'
description: 'Downloads and installs SDL2 development libraries for Windows builds'
runs:
using: 'composite'
steps:
- name: Download & install SDL2
shell: pwsh
run: |
$sdlZip = "${{github.workspace}}/RecastDemo/Contrib/SDL.zip"
(New-Object System.Net.WebClient).DownloadFile("https://github.com/libsdl-org/SDL/releases/download/release-2.32.10/SDL2-devel-2.32.10-VC.zip", $sdlZip)
Expand-Archive -Path $sdlZip -DestinationPath "${{github.workspace}}/RecastDemo/Contrib"
Rename-Item -Path "${{github.workspace}}/RecastDemo/Contrib/SDL2-2.32.10" -NewName "SDL"
Remove-Item $sdlZip

View File

@@ -0,0 +1,193 @@
name: Build
on:
push:
branches: [ "**" ]
pull_request:
branches: [ "**" ]
jobs:
macOS-premake:
strategy:
matrix:
conf:
- Debug
- Release
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup SDL2
uses: ./.github/actions/setup-sdl2-macos
- name: Download & install premake
working-directory: RecastDemo
run: |
curl -L -o premake.tar.gz https://github.com/premake/premake-core/releases/download/v5.0.0-beta8/premake-5.0.0-beta8-macosx.tar.gz
tar -xzf premake.tar.gz
rm premake.tar.gz
chmod 777 ./premake5
- name: Run premake
working-directory: RecastDemo
run: ./premake5 xcode4
- name: Build With Xcode
working-directory: RecastDemo/Build/xcode4/
run: xcodebuild -scheme RecastDemo -configuration ${{matrix.conf}} -project RecastDemo.xcodeproj build
- name: Build Unit Tests With Xcode
working-directory: RecastDemo/Build/xcode4/
run: xcodebuild -scheme Tests -configuration ${{matrix.conf}} -project Tests.xcodeproj build
macos-cmake:
strategy:
matrix:
conf:
- Debug
- Release
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup SDL2
uses: ./.github/actions/setup-sdl2-macos
- name: Configure CMake
run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{matrix.conf}}
- name: Build
run: cmake --build ${{github.workspace}}/build --config ${{matrix.conf}}
linux-premake:
strategy:
matrix:
conf:
- debug
- release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup SDL2
uses: ./.github/actions/setup-sdl2-linux
- name: Install clang
run: |
sudo apt-get install -y clang
clang --version
- name: Download & Install premake
working-directory: RecastDemo
run: |
curl -L -o premake.tar.gz https://github.com/premake/premake-core/releases/download/v5.0.0-beta8/premake-5.0.0-beta8-linux.tar.gz
tar -xzf premake.tar.gz
rm premake.tar.gz
chmod 777 ./premake5
- name: Run premake
working-directory: RecastDemo
run: ./premake5 --cc=clang gmake
- name: Build
working-directory: RecastDemo/Build/gmake
run: make config=${{matrix.conf}} verbose=true
linux-cmake:
strategy:
matrix:
conf:
- Debug
- Release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup SDL2
uses: ./.github/actions/setup-sdl2-linux
- name: Install clang
run: |
sudo apt-get install -y clang
clang --version
- name: Configure CMake
run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{matrix.conf}} -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
- name: Build
run: cmake --build ${{github.workspace}}/build --config ${{matrix.conf}}
windows-premake:
strategy:
matrix:
conf:
- Debug
- Release
vs-version:
- vs2022
include:
- vs-version: vs2022
version-range: '17.0'
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v1.1
with:
vs-version: ${{matrix.version-range}}
- name: Setup SDL2
uses: ./.github/actions/setup-sdl2-windows
- name: Download and Install Premake
working-directory: RecastDemo
shell: pwsh
run: |
(new-object System.Net.WebClient).DownloadFile("https://github.com/premake/premake-core/releases/download/v5.0.0-beta8/premake-5.0.0-beta8-windows.zip","${{github.workspace}}/RecastDemo/premake.zip")
tar -xf premake.zip
del premake.zip
- name: Run Premake
working-directory: RecastDemo
run: ./premake5.exe ${{matrix.vs-version}}
- name: Build
working-directory: RecastDemo/Build/${{matrix.vs-version}}
run: msbuild RecastDemo.vcxproj -property:Configuration=${{matrix.conf}} -property:Platform=x64
windows-cmake:
strategy:
matrix:
conf:
- Debug
- Release
vs-version:
- vs2022
include:
- vs-version: vs2022
cmake-generator: Visual Studio 17 2022
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Setup SDL2
uses: ./.github/actions/setup-sdl2-windows
- name: Configure CMake
run: cmake -G "${{matrix.cmake-generator}}" -B ${{github.workspace}}/build -D CMAKE_BUILD_TYPE=${{matrix.conf}} -D CMAKE_INSTALL_PREFIX=${{github.workspace}}/build
- name: Build with CMake
run: cmake --build ${{github.workspace}}/build --config ${{matrix.conf}}

View File

@@ -0,0 +1,29 @@
name: Publish Docs
permissions:
contents: write
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Install Doxygen
run: sudo apt-get install -y doxygen
- name: Build Doxygen Documentation
run: doxygen ./Doxyfile
- name: Deploy Documentation
uses: JamesIves/github-pages-deploy-action@v4
with:
folder: ./Docs/html
branch: gh-pages

View File

@@ -0,0 +1,103 @@
name: Tests
on:
push:
branches: [ "**" ]
pull_request:
branches: [ "**" ]
jobs:
macos-tests:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup SDL2
uses: ./.github/actions/setup-sdl2-macos
- name: Download & install premake
working-directory: RecastDemo
run: |
curl -L -o premake.tar.gz https://github.com/premake/premake-core/releases/download/v5.0.0-beta8/premake-5.0.0-beta8-macosx.tar.gz
tar -xzf premake.tar.gz
rm premake.tar.gz
chmod 777 ./premake5
- name: Run premake
working-directory: RecastDemo
run: ./premake5 xcode4
- name: Build Unit Tests With Xcode
working-directory: RecastDemo/Build/xcode4/
run: xcodebuild -scheme Tests -configuration Debug -project Tests.xcodeproj build
- name: Run unit tests
working-directory: RecastDemo/Bin
run: ./Tests --verbosity high --success
linux-tests:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3
- name: Setup SDL2
uses: ./.github/actions/setup-sdl2-linux
- name: Install clang
run: |
sudo apt-get install -y clang
clang --version
- name: Download & Install premake
working-directory: RecastDemo
run: |
curl -L -o premake.tar.gz https://github.com/premake/premake-core/releases/download/v5.0.0-beta8/premake-5.0.0-beta8-linux.tar.gz
tar -xzf premake.tar.gz
rm premake.tar.gz
chmod 777 ./premake5
- name: Run premake
working-directory: RecastDemo
run: ./premake5 --cc=clang gmake
- name: Build
working-directory: RecastDemo/Build/gmake
run: make config=debug verbose=true
- name: Run Tests
working-directory: RecastDemo/Bin
run: ./Tests --verbosity high --success
windows-tests:
runs-on: windows-2022
steps:
- uses: actions/checkout@v3
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v1.1
- name: Setup SDL2
uses: ./.github/actions/setup-sdl2-windows
- name: Download and Install Premake
working-directory: RecastDemo
shell: pwsh
run: |
(new-object System.Net.WebClient).DownloadFile("https://github.com/premake/premake-core/releases/download/v5.0.0-beta8/premake-5.0.0-beta8-windows.zip","${{github.workspace}}/RecastDemo/premake.zip")
tar -xf premake.zip
del premake.zip
- name: Run Premake
working-directory: RecastDemo
run: ./premake5.exe vs2022
- name: Build
working-directory: RecastDemo/Build/vs2022
run: msbuild Tests.vcxproj -property:Configuration=Debug -property:Platform=x64
- name: Run Tests
working-directory: RecastDemo/Bin/
run: ./Tests.exe --verbosity high --success

View File

@@ -0,0 +1,62 @@
## Compiled source #
*.com
*.class
*.dll
*.exe
*.ilk
*.o
*.pdb
*.so
*.idb
# clangd
.cache/
compile_commands.json
## Linux exes have no extension
RecastDemo/Bin/RecastDemo
RecastDemo/Bin/Tests
# Build directory
RecastDemo/Build
# XCode debug symbols archive
RecastDemo/Bin/*.dSYM
# Ignore meshes
RecastDemo/Bin/Meshes/*
# Dear IMGUI state
RecastDemo/Bin/imgui.ini
## Logs and databases #
*.log
*.sql
*.sqlite
## OS generated files #
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
*.swp
*.swo
## xcode specific
*xcuserdata*
## SDL contrib
RecastDemo/Contrib/SDL/*
## Generated doc files
Docs/html
## CMake build cache
build
## IDE files
.idea/
cmake-build-*/

View File

@@ -0,0 +1,100 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
<h2>[Unreleased](https://github.com/recastnavigation/recastnavigation/compare/1.6.0...HEAD)</h2>
<h2>[1.6.0](https://github.com/recastnavigation/recastnavigation/compare/1.5.1...1.6.0) - 2023-05-21</h2>
### Added
- CMake build support
- Unit testing with Catch2 (#147)
- Support for AABB and OBB obstacles in `dtTileCache` (#215, #278)
- `dtTileCache` supports timesliced updates (#203)
- Support for custom assertion functions (#250)
- Variant of `findNearestPoly` that exposes distance and isOverPoly (#448)
- `dtNavMeshQuery::getPathFromDijkstraSearch` gets a path from the explored nodes in a navmesh search (#211)
- A version of `dtPolyQuery::queryPolygon` that operates on batches of polygons rather than just 128 (#175) (Fixes #107)
- `rcNew`/`rcDelete` to match `rcAlloc`/`rcFree` (#324)
- Better error reporting and input sanitization (#179, #303)
- Better debug draw (#253, #254, #255, #256)
- Improved docstrings, documentation
- (RecastDemo) Load/Save navmesh data (#258)
### Fixed
- Improved robustness, speed and accuracy of navmesh point queries (#205, #208, #228, #231, #364, #381, #560)
- Incorrect rasterization at tile borders (#476)
- Off-mesh links in tiles were sometimes added twice (#202)
- Potential heap corruption when collecting region layers (#214)
- `findPath` returns `DT_OUT_OF_NODES` appropriately (#222)
- Spans are filtered if there is just enough height (#626)
- Increased epsilon in detour common segment polygon intersection test (#612)
- Array overrun in `removeVertex` in `DetourTileCacheBuilder` (#601)
- Potential rounding error computing bounding box size in `dtNavMesh::connectExtLinks` (#428)
- An indexing error in updating agents in `DetourCrowd` (#450)
- Allocation perf issues in rcVectorBase (#467)
- Dead website links in comments
- RecastDemo bugs (#180, #184, #186, #187, #200)
- Uninitialized class member values, small memory leaks, rule-of-three violations, other minor issues
### Changed
- Updated stb_image (#184)
- Updated stb_truetype (#183)
### Removed
- Use of _USE_MATH_DEFINES directive (#596)
## [1.5.1](https://github.com/recastnavigation/recastnavigation/compare/1.5.0...1.5.1) - 2016-02-22
Patch release; one bug has been fixed, which would cause silent failure if too many nodes were requested and used in a dtNavMeshQuery.
- Fail when too many nodes are requested (#179)
## 1.5.0 - 2016-01-24
This is the first release of the Recast and Detour libraries since August 2009, containing all fixes and enhancements made since then. As you can imagine, this includes a huge number of commits, so we will forego the list of changes for this release - future releases will contain at least a summary of changes.
We have decided to use Semantic Versioning for version numbers from now onwards - beginning at 1.5.0 rather than 1.0.0 since the last old release on Google Code was 1.4.
## 1.4.0 - 2009-08-24
(Release 1.4 and earlier can be found on the old [archived google code repository](https://code.google.com/archive/p/recastnavigation/))
- Added detail height mesh generation (RecastDetailMesh.cpp) for single, tiled statmeshes as well as tilemesh.
- Added feature to contour tracing which detects extra vertices along tile edges which should be removed later.
- Changed the tiled stat mesh preprocess, so that it first generated polymeshes per tile and finally combines them.
- Fixed bug in the GUI code where invisible buttons could be pressed.
## 1.3.1 - 2009-07-24
- Better cost and heuristic functions.
- Fixed tile navmesh raycast on tile borders.
## 1.3.1 - 2009-07-14
- Added dtTileNavMesh which allows dynamically adding and removing navmesh pieces at runtime.
- Renamed stat navmesh types to dtStat* (i.e. dtPoly is now dtStatPoly).
- Moved common code used by tile and stat navmesh to DetourNode.h/cpp and DetourCommon.h/cpp.
- Refactor the demo code.
## 1.2.0 - 2009-06-17
- Added tiled mesh generation. The tiled generation allows to generate navigation for much larger worlds, it removes some of the artifacts that comes from distance fields in open areas, and allows later streaming and dynamic runtime generation
- Improved and added some debug draw modes
- API change: The helper function rcBuildNavMesh does not exists anymore, had to change few internal things to cope with the tiled processing, similar API functionality will be added later once the tiled process matures
- The demo is getting way too complicated, need to split demos
- Fixed several filtering functions so that the mesh is tighter to the geometry, sometimes there could be up error up to tow voxel units close to walls, now it should be just one.
## 1.1.0 - 2009-04-11
This is the first release of Detour.
## 1.0.0 - 2009-03-29
This is the first release of Recast.
The process is not always as robust as I would wish. The watershed phase sometimes swallows tiny islands which are close to edges. These droppings are handled in rcBuildContours, but the code is not particularly robust either.
Another non-robust case is when portal contours (contours shared between two regions) are always assumed to be straight. That can lead to overlapping contours specially when the level has large open areas.

View File

@@ -0,0 +1,25 @@
# Minimal RecastNavigation build for editScene
# No demos, no tests, no -fno-rtti / -fno-exceptions
cmake_minimum_required(VERSION 3.13)
project(recastnavigation-editscene LANGUAGES C CXX)
# Version info for the libraries
set(SOVERSION 1)
set(LIB_VERSION 1.6.0)
# We want RTTI and exceptions to be compatible with the rest of the C++ project.
# The upstream CMakeLists adds -fno-rtti -fno-exceptions; we skip that here.
# Disable unwanted parts
set(RECASTNAVIGATION_DEMO OFF CACHE BOOL "" FORCE)
set(RECASTNAVIGATION_TESTS OFF CACHE BOOL "" FORCE)
set(RECASTNAVIGATION_EXAMPLES OFF CACHE BOOL "" FORCE)
set(RECASTNAVIGATION_ENABLE_ASSERTS "$<CONFIG:Debug>" CACHE STRING "" FORCE)
# Build the core libraries only
add_subdirectory(Recast)
add_subdirectory(Detour)
add_subdirectory(DetourTileCache)
add_subdirectory(DetourCrowd)
add_subdirectory(DebugUtils)

Some files were not shown because too many files have changed in this diff Show More