Compare commits
42 Commits
d55bf970e0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 11530dd7fc | |||
| 3fd167ebff | |||
| 5952a96ee6 | |||
| 76c3ead4a8 | |||
| 39a053d4ee | |||
| c5da977857 | |||
| 3e7b0169d5 | |||
| f918c5cefb | |||
| 976ced3731 | |||
| 0fd8deaf53 | |||
| 4d843c18c7 | |||
| 0ed83966da | |||
| 998984f75a | |||
| 02fa78764a | |||
| abe6eef6b3 | |||
| cca732b41b | |||
| 8507a3a501 | |||
| b9cce0248a | |||
| fa49bb5005 | |||
| 37441aa8fd | |||
| a1b74aa2d5 | |||
| c80d9c96e6 | |||
| a75db85027 | |||
| 7563937ab8 | |||
| 425bb8411d | |||
| 9b29b68b33 | |||
| 7557c710fb | |||
| ce2f6c1306 | |||
| e0e8e316d4 | |||
| abd2dc22d3 | |||
| a5df60769f | |||
| 75ba39895f | |||
| 2cff982473 | |||
| 3bd2801d1d | |||
| 2e358275f0 | |||
| 5ed7552164 | |||
| 2b3482da88 | |||
| 1d2c330481 | |||
| a0d2561587 | |||
| e95b904f4e | |||
| 9d4fad1d10 | |||
| 4335a8cb05 |
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
37
src/features/editScene/GameMode.cpp
Normal file
37
src/features/editScene/GameMode.cpp
Normal 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
|
||||
101
src/features/editScene/GameMode.hpp
Normal file
101
src/features/editScene/GameMode.hpp
Normal 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
|
||||
515
src/features/editScene/components/ActionDatabase.cpp
Normal file
515
src/features/editScene/components/ActionDatabase.cpp
Normal 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();
|
||||
});
|
||||
}
|
||||
114
src/features/editScene/components/ActionDatabase.hpp
Normal file
114
src/features/editScene/components/ActionDatabase.hpp
Normal 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
|
||||
47
src/features/editScene/components/ActionDatabaseModule.cpp
Normal file
47
src/features/editScene/components/ActionDatabaseModule.cpp
Normal 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>();
|
||||
});
|
||||
}
|
||||
39
src/features/editScene/components/ActionDebug.hpp
Normal file
39
src/features/editScene/components/ActionDebug.hpp
Normal 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
|
||||
21
src/features/editScene/components/ActionDebugModule.cpp
Normal file
21
src/features/editScene/components/ActionDebugModule.cpp
Normal 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>();
|
||||
});
|
||||
}
|
||||
44
src/features/editScene/components/Actuator.hpp
Normal file
44
src/features/editScene/components/Actuator.hpp
Normal 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
|
||||
19
src/features/editScene/components/ActuatorModule.cpp
Normal file
19
src/features/editScene/components/ActuatorModule.cpp
Normal 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>();
|
||||
});
|
||||
}
|
||||
130
src/features/editScene/components/BehaviorTree.hpp
Normal file
130
src/features/editScene/components/BehaviorTree.hpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
131
src/features/editScene/components/DialogueComponent.hpp
Normal file
131
src/features/editScene/components/DialogueComponent.hpp
Normal 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
|
||||
@@ -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>();
|
||||
}
|
||||
});
|
||||
}
|
||||
21
src/features/editScene/components/EventHandler.hpp
Normal file
21
src/features/editScene/components/EventHandler.hpp
Normal 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
|
||||
21
src/features/editScene/components/EventHandlerModule.cpp
Normal file
21
src/features/editScene/components/EventHandlerModule.cpp
Normal 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>();
|
||||
});
|
||||
}
|
||||
739
src/features/editScene/components/EventParams.hpp
Normal file
739
src/features/editScene/components/EventParams.hpp
Normal 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
|
||||
74
src/features/editScene/components/GoapAction.hpp
Normal file
74
src/features/editScene/components/GoapAction.hpp
Normal 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
|
||||
227
src/features/editScene/components/GoapBlackboard.cpp
Normal file
227
src/features/editScene/components/GoapBlackboard.cpp
Normal 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 ®istry = 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;
|
||||
}
|
||||
236
src/features/editScene/components/GoapBlackboard.hpp
Normal file
236
src/features/editScene/components/GoapBlackboard.hpp
Normal 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
|
||||
21
src/features/editScene/components/GoapBlackboardModule.cpp
Normal file
21
src/features/editScene/components/GoapBlackboardModule.cpp
Normal 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>();
|
||||
});
|
||||
}
|
||||
283
src/features/editScene/components/GoapExpression.cpp
Normal file
283
src/features/editScene/components/GoapExpression.cpp
Normal 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();
|
||||
}
|
||||
85
src/features/editScene/components/GoapExpression.hpp
Normal file
85
src/features/editScene/components/GoapExpression.hpp
Normal 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
|
||||
24
src/features/editScene/components/GoapGoal.cpp
Normal file
24
src/features/editScene/components/GoapGoal.cpp
Normal 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;
|
||||
}
|
||||
43
src/features/editScene/components/GoapGoal.hpp
Normal file
43
src/features/editScene/components/GoapGoal.hpp
Normal 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
|
||||
99
src/features/editScene/components/GoapPlanner.hpp
Normal file
99
src/features/editScene/components/GoapPlanner.hpp
Normal 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
|
||||
21
src/features/editScene/components/GoapPlannerModule.cpp
Normal file
21
src/features/editScene/components/GoapPlannerModule.cpp
Normal 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>();
|
||||
});
|
||||
}
|
||||
50
src/features/editScene/components/GoapRunner.hpp
Normal file
50
src/features/editScene/components/GoapRunner.hpp
Normal 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
|
||||
21
src/features/editScene/components/GoapRunnerModule.cpp
Normal file
21
src/features/editScene/components/GoapRunnerModule.cpp
Normal 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>();
|
||||
});
|
||||
}
|
||||
165
src/features/editScene/components/Inventory.hpp
Normal file
165
src/features/editScene/components/Inventory.hpp
Normal 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
|
||||
20
src/features/editScene/components/InventoryModule.cpp
Normal file
20
src/features/editScene/components/InventoryModule.cpp
Normal 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>();
|
||||
});
|
||||
}
|
||||
59
src/features/editScene/components/Item.hpp
Normal file
59
src/features/editScene/components/Item.hpp
Normal 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
|
||||
19
src/features/editScene/components/ItemModule.cpp
Normal file
19
src/features/editScene/components/ItemModule.cpp
Normal 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>();
|
||||
});
|
||||
}
|
||||
59
src/features/editScene/components/NavMesh.hpp
Normal file
59
src/features/editScene/components/NavMesh.hpp
Normal 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
|
||||
38
src/features/editScene/components/NavMeshModule.cpp
Normal file
38
src/features/editScene/components/NavMeshModule.cpp
Normal 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>();
|
||||
});
|
||||
}
|
||||
77
src/features/editScene/components/PathFollowing.hpp
Normal file
77
src/features/editScene/components/PathFollowing.hpp
Normal 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
|
||||
21
src/features/editScene/components/PathFollowingModule.cpp
Normal file
21
src/features/editScene/components/PathFollowingModule.cpp
Normal 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>();
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
|
||||
24
src/features/editScene/components/PrefabInstance.hpp
Normal file
24
src/features/editScene/components/PrefabInstance.hpp
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
58
src/features/editScene/components/Skybox.hpp
Normal file
58
src/features/editScene/components/Skybox.hpp
Normal 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
|
||||
24
src/features/editScene/components/SkyboxModule.cpp
Normal file
24
src/features/editScene/components/SkyboxModule.cpp
Normal 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>();
|
||||
}
|
||||
});
|
||||
}
|
||||
38
src/features/editScene/components/SmartObject.hpp
Normal file
38
src/features/editScene/components/SmartObject.hpp
Normal 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
|
||||
20
src/features/editScene/components/SmartObjectModule.cpp
Normal file
20
src/features/editScene/components/SmartObjectModule.cpp
Normal 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>();
|
||||
});
|
||||
}
|
||||
70
src/features/editScene/components/Sun.hpp
Normal file
70
src/features/editScene/components/Sun.hpp
Normal 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
|
||||
24
src/features/editScene/components/SunModule.cpp
Normal file
24
src/features/editScene/components/SunModule.cpp
Normal 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>();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
409
src/features/editScene/gizmo/Cursor3D.cpp
Normal file
409
src/features/editScene/gizmo/Cursor3D.cpp
Normal 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;
|
||||
}
|
||||
96
src/features/editScene/gizmo/Cursor3D.hpp
Normal file
96
src/features/editScene/gizmo/Cursor3D.hpp
Normal 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
|
||||
418
src/features/editScene/lua-examples/action_db_example.lua
Normal file
418
src/features/editScene/lua-examples/action_db_example.lua
Normal 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
|
||||
-- =============================================================================
|
||||
514
src/features/editScene/lua-examples/behavior_tree_example.lua
Normal file
514
src/features/editScene/lua-examples/behavior_tree_example.lua
Normal 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.
|
||||
-- =============================================================================
|
||||
711
src/features/editScene/lua-examples/component_example.lua
Normal file
711
src/features/editScene/lua-examples/component_example.lua
Normal 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!")
|
||||
28
src/features/editScene/lua-examples/debug_crash_example.lua
Normal file
28
src/features/editScene/lua-examples/debug_crash_example.lua
Normal 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)")
|
||||
102
src/features/editScene/lua-examples/dialogue_basic_show.lua
Normal file
102
src/features/editScene/lua-examples/dialogue_basic_show.lua
Normal 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!")
|
||||
219
src/features/editScene/lua-examples/dialogue_component_api.lua
Normal file
219
src/features/editScene/lua-examples/dialogue_component_api.lua
Normal 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!")
|
||||
260
src/features/editScene/lua-examples/dialogue_event_handler.lua
Normal file
260
src/features/editScene/lua-examples/dialogue_event_handler.lua
Normal 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!")
|
||||
143
src/features/editScene/lua-examples/dialogue_event_subscribe.lua
Normal file
143
src/features/editScene/lua-examples/dialogue_event_subscribe.lua
Normal 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!")
|
||||
320
src/features/editScene/lua-examples/dialogue_sequence.lua
Normal file
320
src/features/editScene/lua-examples/dialogue_sequence.lua
Normal 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!")
|
||||
177
src/features/editScene/lua-examples/entity_example.lua
Normal file
177
src/features/editScene/lua-examples/entity_example.lua
Normal 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!")
|
||||
296
src/features/editScene/lua-examples/event_example.lua
Normal file
296
src/features/editScene/lua-examples/event_example.lua
Normal 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!")
|
||||
135
src/features/editScene/lua-examples/game_mode_example.lua
Normal file
135
src/features/editScene/lua-examples/game_mode_example.lua
Normal 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!")
|
||||
576
src/features/editScene/lua/LuaActionApi.cpp
Normal file
576
src/features/editScene/lua/LuaActionApi.cpp
Normal 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
|
||||
101
src/features/editScene/lua/LuaActionApi.hpp
Normal file
101
src/features/editScene/lua/LuaActionApi.hpp
Normal 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
|
||||
471
src/features/editScene/lua/LuaBehaviorTreeApi.cpp
Normal file
471
src/features/editScene/lua/LuaBehaviorTreeApi.cpp
Normal 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 ¶ms)
|
||||
{
|
||||
lua_newtable(L); // params table
|
||||
|
||||
if (params.empty())
|
||||
return;
|
||||
|
||||
const char *s = params.c_str();
|
||||
while (*s) {
|
||||
// Skip whitespace
|
||||
while (*s == ' ' || *s == '\t')
|
||||
s++;
|
||||
if (!*s)
|
||||
break;
|
||||
|
||||
// Find key
|
||||
const char *keyStart = s;
|
||||
while (*s && *s != '=' && *s != ',')
|
||||
s++;
|
||||
std::string key(keyStart, static_cast<size_t>(s - keyStart));
|
||||
// Trim trailing spaces from key
|
||||
while (!key.empty() &&
|
||||
(key.back() == ' ' || key.back() == '\t'))
|
||||
key.pop_back();
|
||||
|
||||
if (*s != '=') {
|
||||
while (*s && *s != ',')
|
||||
s++;
|
||||
if (*s == ',')
|
||||
s++;
|
||||
continue;
|
||||
}
|
||||
s++; // skip '='
|
||||
|
||||
// Skip whitespace before value
|
||||
while (*s == ' ' || *s == '\t')
|
||||
s++;
|
||||
|
||||
// Find value end (next comma or end)
|
||||
const char *valStart = s;
|
||||
bool inQuotes = false;
|
||||
while (*s && (*s != ',' || inQuotes)) {
|
||||
if (*s == '"')
|
||||
inQuotes = !inQuotes;
|
||||
s++;
|
||||
}
|
||||
std::string val(valStart, static_cast<size_t>(s - valStart));
|
||||
// Trim trailing spaces from value
|
||||
while (!val.empty() &&
|
||||
(val.back() == ' ' || val.back() == '\t'))
|
||||
val.pop_back();
|
||||
|
||||
// Strip quotes if present
|
||||
if (val.size() >= 2 && val.front() == '"' &&
|
||||
val.back() == '"') {
|
||||
val = val.substr(1, val.size() - 2);
|
||||
lua_pushstring(L, val.c_str());
|
||||
lua_setfield(L, -2, key.c_str());
|
||||
} else {
|
||||
// Try numeric
|
||||
char *end = nullptr;
|
||||
long iVal = strtol(val.c_str(), &end, 10);
|
||||
if (end != val.c_str() && *end == '\0') {
|
||||
lua_pushinteger(L, (int)iVal);
|
||||
lua_setfield(L, -2, key.c_str());
|
||||
} else {
|
||||
// Try float
|
||||
end = nullptr;
|
||||
float fVal = strtof(val.c_str(), &end);
|
||||
if (end != val.c_str() && *end == '\0') {
|
||||
lua_pushnumber(L, fVal);
|
||||
lua_setfield(L, -2, key.c_str());
|
||||
} else {
|
||||
// Fallback to string
|
||||
lua_pushstring(L, val.c_str());
|
||||
lua_setfield(L, -2, key.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (*s == ',')
|
||||
s++;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API: Call a registered Lua node handler
|
||||
// ---------------------------------------------------------------------------
|
||||
// Returns: 0 = success, 1 = failure, 2 = running, -1 = not found/error
|
||||
|
||||
int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName,
|
||||
flecs::entity entity, const std::string ¶ms)
|
||||
{
|
||||
// Use the global Lua state if none was passed
|
||||
lua_State *L = static_cast<lua_State *>(L_);
|
||||
if (!L)
|
||||
L = g_luaState;
|
||||
if (!L)
|
||||
return -1;
|
||||
|
||||
auto it = g_luaNodeHandlers.find(nodeName);
|
||||
if (it == g_luaNodeHandlers.end())
|
||||
return -1;
|
||||
|
||||
int ref = it->second;
|
||||
|
||||
// Push the callback function
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
|
||||
|
||||
// Push entity ID as first argument
|
||||
lua_pushinteger(L, luaEntityToId(entity));
|
||||
|
||||
// Push params table as second argument
|
||||
pushParamsTable(L, params);
|
||||
|
||||
// Call the function
|
||||
if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "[Lua BT] Error calling node '" << nodeName
|
||||
<< "': " << lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read the result
|
||||
if (!lua_isstring(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *result = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
if (strcmp(result, "success") == 0)
|
||||
return 0;
|
||||
if (strcmp(result, "failure") == 0)
|
||||
return 1;
|
||||
if (strcmp(result, "running") == 0)
|
||||
return 2;
|
||||
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "[Lua BT] Node '" << nodeName
|
||||
<< "' returned invalid result: " << result;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Convenience overload: call with raw entity ID (for tests)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName,
|
||||
uint64_t entityId, const std::string ¶ms)
|
||||
{
|
||||
// Use the global Lua state if none was passed
|
||||
lua_State *L = static_cast<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
|
||||
149
src/features/editScene/lua/LuaBehaviorTreeApi.hpp
Normal file
149
src/features/editScene/lua/LuaBehaviorTreeApi.hpp
Normal 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 ¶ms);
|
||||
|
||||
/**
|
||||
* @brief Convenience overload for calling a registered Lua node handler
|
||||
* with a raw entity ID (for use in tests or contexts without flecs).
|
||||
*
|
||||
* @param L_ Pointer to the Lua state (can be nullptr to use global state).
|
||||
* @param nodeName The name of the registered node handler.
|
||||
* @param entityId The raw entity ID.
|
||||
* @param params The params string (key=val,key2=val2 format).
|
||||
* @return 0 = success, 1 = failure, 2 = running, -1 = error/not found.
|
||||
*/
|
||||
int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName,
|
||||
uint64_t entityId, const std::string ¶ms);
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_LUA_BEHAVIOR_TREE_API_HPP
|
||||
1992
src/features/editScene/lua/LuaComponentApi.cpp
Normal file
1992
src/features/editScene/lua/LuaComponentApi.cpp
Normal file
File diff suppressed because it is too large
Load Diff
57
src/features/editScene/lua/LuaComponentApi.hpp
Normal file
57
src/features/editScene/lua/LuaComponentApi.hpp
Normal 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
|
||||
236
src/features/editScene/lua/LuaEntityApi.cpp
Normal file
236
src/features/editScene/lua/LuaEntityApi.cpp
Normal 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
|
||||
126
src/features/editScene/lua/LuaEntityApi.hpp
Normal file
126
src/features/editScene/lua/LuaEntityApi.hpp
Normal 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
|
||||
471
src/features/editScene/lua/LuaEventApi.cpp
Normal file
471
src/features/editScene/lua/LuaEventApi.cpp
Normal 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 ¶ms)
|
||||
{
|
||||
lua_newtable(L);
|
||||
|
||||
// Push _types sub-table
|
||||
lua_newtable(L);
|
||||
|
||||
for (EventParams::ConstIterator it = params.begin(); it != params.end();
|
||||
++it) {
|
||||
const std::string &key = it->first;
|
||||
const EventValue &val = it->second;
|
||||
|
||||
// Push the value
|
||||
pushEventValue(L, val);
|
||||
lua_setfield(L, -3, key.c_str());
|
||||
|
||||
// Push the type name into _types
|
||||
lua_pushstring(L, eventValueTypeName(val.getType()));
|
||||
lua_setfield(L, -2, key.c_str());
|
||||
}
|
||||
|
||||
// Set _types sub-table
|
||||
lua_setfield(L, -2, "_types");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: read a Lua value as an EventValue
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Read a Lua value at the given index as an EventValue.
|
||||
*
|
||||
* Type inference rules:
|
||||
* - integer -> INT
|
||||
* - number (non-integer) -> DOUBLE
|
||||
* - string -> STRING
|
||||
* - table with all integer keys -> array (type inferred from elements)
|
||||
* - table with string keys -> not supported at value level
|
||||
*
|
||||
* @param L Lua state.
|
||||
* @param idx Stack index of the 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 ¶ms) {
|
||||
// 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
|
||||
65
src/features/editScene/lua/LuaEventApi.hpp
Normal file
65
src/features/editScene/lua/LuaEventApi.hpp
Normal 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
|
||||
171
src/features/editScene/lua/LuaGameModeApi.cpp
Normal file
171
src/features/editScene/lua/LuaGameModeApi.cpp
Normal 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
|
||||
39
src/features/editScene/lua/LuaGameModeApi.hpp
Normal file
39
src/features/editScene/lua/LuaGameModeApi.hpp
Normal 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
|
||||
187
src/features/editScene/lua/LuaState.cpp
Normal file
187
src/features/editScene/lua/LuaState.cpp
Normal 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
|
||||
137
src/features/editScene/lua/LuaState.hpp
Normal file
137
src/features/editScene/lua/LuaState.hpp
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
279
src/features/editScene/recast/PartitionedMesh.cpp
Normal file
279
src/features/editScene/recast/PartitionedMesh.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/features/editScene/recast/PartitionedMesh.hpp
Normal file
50
src/features/editScene/recast/PartitionedMesh.hpp
Normal 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;
|
||||
};
|
||||
1018
src/features/editScene/recast/TileCacheNavMesh.cpp
Normal file
1018
src/features/editScene/recast/TileCacheNavMesh.cpp
Normal file
File diff suppressed because it is too large
Load Diff
120
src/features/editScene/recast/TileCacheNavMesh.hpp
Normal file
120
src/features/editScene/recast/TileCacheNavMesh.hpp
Normal 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 ¶ms);
|
||||
~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
|
||||
556
src/features/editScene/recast/fastlz.c
Normal file
556
src/features/editScene/recast/fastlz.c
Normal 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) */
|
||||
100
src/features/editScene/recast/fastlz.h
Normal file
100
src/features/editScene/recast/fastlz.h
Normal 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 */
|
||||
86
src/features/editScene/recastnavigation/.clang-format
Normal file
86
src/features/editScene/recastnavigation/.clang-format
Normal 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
|
||||
...
|
||||
3
src/features/editScene/recastnavigation/.clang-tidy
Normal file
3
src/features/editScene/recastnavigation/.clang-tidy
Normal file
@@ -0,0 +1,3 @@
|
||||
Checks: 'clang-analyzer-*'
|
||||
WarningsAsErrors: '*'
|
||||
HeaderFilterRegex: '.*'
|
||||
12
src/features/editScene/recastnavigation/.editorconfig
Normal file
12
src/features/editScene/recastnavigation/.editorconfig
Normal 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
|
||||
11
src/features/editScene/recastnavigation/.github/actions/setup-sdl2-linux/action.yaml
vendored
Normal file
11
src/features/editScene/recastnavigation/.github/actions/setup-sdl2-linux/action.yaml
vendored
Normal 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
|
||||
16
src/features/editScene/recastnavigation/.github/actions/setup-sdl2-macos/action.yaml
vendored
Normal file
16
src/features/editScene/recastnavigation/.github/actions/setup-sdl2-macos/action.yaml
vendored
Normal 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
|
||||
14
src/features/editScene/recastnavigation/.github/actions/setup-sdl2-windows/action.yaml
vendored
Normal file
14
src/features/editScene/recastnavigation/.github/actions/setup-sdl2-windows/action.yaml
vendored
Normal 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
|
||||
193
src/features/editScene/recastnavigation/.github/workflows/Build.yaml
vendored
Normal file
193
src/features/editScene/recastnavigation/.github/workflows/Build.yaml
vendored
Normal 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}}
|
||||
29
src/features/editScene/recastnavigation/.github/workflows/Docs.yaml
vendored
Normal file
29
src/features/editScene/recastnavigation/.github/workflows/Docs.yaml
vendored
Normal 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
|
||||
103
src/features/editScene/recastnavigation/.github/workflows/Tests.yaml
vendored
Normal file
103
src/features/editScene/recastnavigation/.github/workflows/Tests.yaml
vendored
Normal 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
|
||||
62
src/features/editScene/recastnavigation/.gitignore
vendored
Normal file
62
src/features/editScene/recastnavigation/.gitignore
vendored
Normal 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-*/
|
||||
100
src/features/editScene/recastnavigation/CHANGELOG.md
Normal file
100
src/features/editScene/recastnavigation/CHANGELOG.md
Normal 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.
|
||||
25
src/features/editScene/recastnavigation/CMakeLists.txt
Normal file
25
src/features/editScene/recastnavigation/CMakeLists.txt
Normal 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
Reference in New Issue
Block a user