Compare commits
46 Commits
7e4e8f6638
...
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 | |||
| d55bf970e0 | |||
| 30814ea35a | |||
| 35f50f7f51 | |||
| ca5b5b3052 |
@@ -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,13 +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
|
||||
@@ -23,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
|
||||
@@ -46,6 +88,7 @@ set(EDITSCENE_SOURCES
|
||||
ui/TriangleBufferEditor.cpp
|
||||
ui/CharacterSlotsEditor.cpp
|
||||
ui/AnimationTreeEditor.cpp
|
||||
ui/AnimationTreeTemplateEditor.cpp
|
||||
ui/CharacterEditor.cpp
|
||||
ui/CellGridEditor.cpp
|
||||
ui/LotEditor.cpp
|
||||
@@ -59,7 +102,23 @@ set(EDITSCENE_SOURCES
|
||||
ui/FurnitureTemplateEditor.cpp
|
||||
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
|
||||
@@ -70,6 +129,7 @@ set(EDITSCENE_SOURCES
|
||||
components/TriangleBufferModule.cpp
|
||||
components/CharacterSlotsModule.cpp
|
||||
components/AnimationTreeModule.cpp
|
||||
components/AnimationTreeTemplateModule.cpp
|
||||
components/AnimationTree.cpp
|
||||
components/CharacterModule.cpp
|
||||
components/CellGridModule.cpp
|
||||
@@ -77,9 +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
|
||||
@@ -90,6 +166,11 @@ set(EDITSCENE_HEADERS
|
||||
components/Relationship.hpp
|
||||
components/PhysicsCollider.hpp
|
||||
components/RigidBody.hpp
|
||||
components/BuoyancyInfo.hpp
|
||||
components/WaterPhysics.hpp
|
||||
components/WaterPlane.hpp
|
||||
components/Sun.hpp
|
||||
components/Skybox.hpp
|
||||
components/Light.hpp
|
||||
components/Camera.hpp
|
||||
components/Lod.hpp
|
||||
@@ -106,21 +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
|
||||
@@ -143,6 +265,7 @@ set(EDITSCENE_HEADERS
|
||||
ui/TriangleBufferEditor.hpp
|
||||
ui/CharacterSlotsEditor.hpp
|
||||
ui/AnimationTreeEditor.hpp
|
||||
ui/AnimationTreeTemplateEditor.hpp
|
||||
ui/CharacterEditor.hpp
|
||||
ui/CellGridEditor.hpp
|
||||
ui/LotEditor.hpp
|
||||
@@ -156,9 +279,35 @@ set(EDITSCENE_HEADERS
|
||||
ui/FurnitureTemplateEditor.hpp
|
||||
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})
|
||||
@@ -175,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.)
|
||||
@@ -201,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,7 +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"
|
||||
@@ -11,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"
|
||||
@@ -25,6 +39,12 @@
|
||||
#include "components/PhysicsCollider.hpp"
|
||||
#include "components/RigidBody.hpp"
|
||||
#include "components/GeneratedPhysicsTag.hpp"
|
||||
#include "components/BuoyancyInfo.hpp"
|
||||
#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"
|
||||
@@ -37,13 +57,41 @@
|
||||
#include "components/TriangleBuffer.hpp"
|
||||
#include "components/CharacterSlots.hpp"
|
||||
#include "components/AnimationTree.hpp"
|
||||
#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
|
||||
@@ -79,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 &&
|
||||
@@ -87,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(
|
||||
@@ -132,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();
|
||||
@@ -212,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>(
|
||||
@@ -219,6 +291,29 @@ void EditorApp::setup()
|
||||
m_physicsSystem->initialize();
|
||||
m_uiSystem->setPhysicsSystem(m_physicsSystem.get());
|
||||
|
||||
// Setup buoyancy system (requires physics system)
|
||||
// Get the physics wrapper from the physics system
|
||||
m_buoyancySystem = std::make_unique<BuoyancySystem>(
|
||||
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);
|
||||
}
|
||||
|
||||
// Set buoyancy system in UI system for configuration
|
||||
if (m_uiSystem) {
|
||||
m_uiSystem->setBuoyancySystem(m_buoyancySystem.get());
|
||||
}
|
||||
|
||||
// Setup light system
|
||||
m_lightSystem = std::make_unique<EditorLightSystem>(m_world,
|
||||
m_sceneMgr);
|
||||
@@ -264,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);
|
||||
@@ -282,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 {
|
||||
@@ -300,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)
|
||||
@@ -326,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;
|
||||
|
||||
@@ -344,20 +558,55 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
void EditorApp::setDebugBuoyancy(bool enabled)
|
||||
{
|
||||
m_debugBuoyancy = enabled;
|
||||
|
||||
// Apply debug setting to buoyancy system if it exists
|
||||
if (m_buoyancySystem) {
|
||||
m_buoyancySystem->setDebugEnabled(enabled);
|
||||
}
|
||||
|
||||
// Log the debug mode change only if OGRE is initialized
|
||||
// (setDebugBuoyancy may be called before OGRE setup)
|
||||
try {
|
||||
if (Ogre::LogManager::getSingletonPtr()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Buoyancy debug mode: " +
|
||||
Ogre::StringConverter::toString(enabled));
|
||||
}
|
||||
} catch (...) {
|
||||
// Ignore if OGRE not initialized yet
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -392,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());
|
||||
@@ -413,6 +667,9 @@ void EditorApp::setupECS()
|
||||
m_world.component<PhysicsColliderComponent>();
|
||||
m_world.component<GeneratedPhysicsTag>();
|
||||
m_world.component<RigidBodyComponent>();
|
||||
m_world.component<BuoyancyInfo>();
|
||||
m_world.component<WaterPhysics>();
|
||||
m_world.component<WaterPlane>();
|
||||
|
||||
// Register light and camera components
|
||||
m_world.component<LightComponent>();
|
||||
@@ -442,15 +699,59 @@ void EditorApp::setupECS()
|
||||
// Register AnimationTree component
|
||||
m_world.component<AnimationTreeComponent>();
|
||||
|
||||
// Register AnimationTreeTemplate component
|
||||
m_world.component<AnimationTreeTemplate>();
|
||||
|
||||
// Register Character component
|
||||
m_world.component<CharacterComponent>();
|
||||
|
||||
// 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()
|
||||
@@ -597,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();
|
||||
@@ -610,17 +916,82 @@ 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);
|
||||
}
|
||||
|
||||
/* --- Buoyancy system (before physics so impulse is integrated) --- */
|
||||
if (m_buoyancySystem) {
|
||||
// Update camera position for water detection area
|
||||
if (m_camera) {
|
||||
Ogre::Vector3 cameraPos = m_camera->getPosition();
|
||||
m_buoyancySystem->setCameraPosition(cameraPos);
|
||||
}
|
||||
m_buoyancySystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Main physics step --- */
|
||||
if (m_physicsSystem) {
|
||||
m_physicsSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- 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,11 +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;
|
||||
|
||||
/**
|
||||
@@ -127,6 +143,13 @@ public:
|
||||
{
|
||||
return m_gameMode;
|
||||
}
|
||||
|
||||
// Debug buoyancy
|
||||
void setDebugBuoyancy(bool enabled);
|
||||
bool getDebugBuoyancy() const
|
||||
{
|
||||
return m_debugBuoyancy;
|
||||
}
|
||||
GamePlayState getGamePlayState() const
|
||||
{
|
||||
return m_gamePlayState;
|
||||
@@ -167,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;
|
||||
@@ -187,6 +222,10 @@ private:
|
||||
std::unique_ptr<EditorCamera> m_camera;
|
||||
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;
|
||||
@@ -196,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
|
||||
@@ -210,6 +260,10 @@ private:
|
||||
GamePlayState m_gamePlayState = GamePlayState::Menu;
|
||||
GameInputState m_gameInput;
|
||||
bool m_setupComplete = false;
|
||||
bool m_debugBuoyancy = false;
|
||||
|
||||
// Lua scripting
|
||||
editScene::LuaState m_lua;
|
||||
|
||||
// Editor visualization nodes
|
||||
Ogre::SceneNode *m_gridNode = 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
|
||||
@@ -15,8 +15,7 @@
|
||||
*/
|
||||
class EditorCamera {
|
||||
public:
|
||||
EditorCamera(Ogre::SceneManager *sceneMgr,
|
||||
Ogre::RenderWindow *window);
|
||||
EditorCamera(Ogre::SceneManager *sceneMgr, Ogre::RenderWindow *window);
|
||||
~EditorCamera();
|
||||
|
||||
/**
|
||||
@@ -35,7 +34,10 @@ public:
|
||||
/**
|
||||
* Get the camera
|
||||
*/
|
||||
Ogre::Camera *getCamera() const { return m_camera; }
|
||||
Ogre::Camera *getCamera() const
|
||||
{
|
||||
return m_camera;
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus camera on a point
|
||||
@@ -47,6 +49,14 @@ public:
|
||||
*/
|
||||
void setPosition(const Ogre::Vector3 &pos);
|
||||
|
||||
/**
|
||||
* Get camera position
|
||||
*/
|
||||
Ogre::Vector3 getPosition() const
|
||||
{
|
||||
return m_position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ray from mouse position
|
||||
*/
|
||||
@@ -55,7 +65,10 @@ public:
|
||||
/**
|
||||
* Check if in FPS mode
|
||||
*/
|
||||
bool isFPSMode() const { return m_fpsMode; }
|
||||
bool isFPSMode() const
|
||||
{
|
||||
return m_fpsMode;
|
||||
}
|
||||
|
||||
private:
|
||||
void updateCameraPosition();
|
||||
@@ -65,7 +78,7 @@ private:
|
||||
Ogre::Camera *m_camera;
|
||||
Ogre::SceneNode *m_cameraNode;
|
||||
Ogre::SceneNode *m_targetNode;
|
||||
|
||||
|
||||
// Use OgreBites::CameraMan for proper camera control
|
||||
std::unique_ptr<OgreBites::CameraMan> m_cameraMan;
|
||||
|
||||
|
||||
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>();
|
||||
});
|
||||
}
|
||||
@@ -150,6 +150,12 @@ struct AnimationTreeComponent {
|
||||
bool useRootMotion = false;
|
||||
bool dirty = true;
|
||||
|
||||
/* If set, the tree root is copied from the named template */
|
||||
Ogre::String templateName;
|
||||
|
||||
/* Runtime: last copied template version (not serialized) */
|
||||
uint64_t templateVersion = 0;
|
||||
|
||||
/* Runtime: current state of each state machine (not serialized) */
|
||||
std::unordered_map<Ogre::String, Ogre::String> currentStates;
|
||||
|
||||
|
||||
20
src/features/editScene/components/AnimationTreeTemplate.hpp
Normal file
20
src/features/editScene/components/AnimationTreeTemplate.hpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#ifndef EDITSCENE_ANIMATIONTREETEMPLATE_HPP
|
||||
#define EDITSCENE_ANIMATIONTREETEMPLATE_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* Template marker for reusable animation trees.
|
||||
*
|
||||
* Entities with this component serve as shared animation tree templates.
|
||||
* They should also have an AnimationTreeComponent for editing the tree.
|
||||
* Other entities reference the template by name via
|
||||
* AnimationTreeComponent::templateName.
|
||||
*/
|
||||
struct AnimationTreeTemplate {
|
||||
Ogre::String name;
|
||||
uint64_t version = 1;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ANIMATIONTREETEMPLATE_HPP
|
||||
@@ -0,0 +1,23 @@
|
||||
#include "AnimationTreeTemplate.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/AnimationTreeTemplateEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Animation Tree Template", "Animation",
|
||||
AnimationTreeTemplate, AnimationTreeTemplateEditor)
|
||||
{
|
||||
registry.registerComponent<AnimationTreeTemplate>(
|
||||
AnimationTreeTemplate_name, AnimationTreeTemplate_group,
|
||||
std::make_unique<AnimationTreeTemplateEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<AnimationTreeTemplate>()) {
|
||||
e.set<AnimationTreeTemplate>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<AnimationTreeTemplate>()) {
|
||||
e.remove<AnimationTreeTemplate>();
|
||||
}
|
||||
});
|
||||
}
|
||||
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
|
||||
46
src/features/editScene/components/BuoyancyInfo.hpp
Normal file
46
src/features/editScene/components/BuoyancyInfo.hpp
Normal file
@@ -0,0 +1,46 @@
|
||||
#ifndef EDITSCENE_BUOYANCYINFO_HPP
|
||||
#define EDITSCENE_BUOYANCYINFO_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* BuoyancyInfo component
|
||||
* Provides per-entity buoyancy settings for water physics
|
||||
* If an entity has this component, it will use these settings
|
||||
* Otherwise, default settings from the buoyancy system will be used
|
||||
*/
|
||||
struct BuoyancyInfo {
|
||||
// Enable/disable buoyancy for this entity
|
||||
bool enabled = true;
|
||||
|
||||
// Buoyancy strength (0 = no buoyancy, 1 = neutral buoyancy, >1 = floats)
|
||||
float buoyancy = 1.0f;
|
||||
|
||||
// Linear drag when submerged (0 = no drag, 1 = full drag)
|
||||
float linearDrag = 0.1f;
|
||||
|
||||
// Angular drag when submerged (0 = no drag, 1 = full drag)
|
||||
float angularDrag = 0.05f;
|
||||
|
||||
// Water surface Y level for this entity (world space)
|
||||
// If not set (0), uses global water level from buoyancy system
|
||||
float waterSurfaceY = 0.0f;
|
||||
|
||||
// Submergedness threshold (0-1) - how much of the body must be submerged
|
||||
// before buoyancy is applied (0 = any contact, 1 = fully submerged)
|
||||
float submergedThreshold = 0.3f;
|
||||
|
||||
// Use custom water surface level (if false, uses global water level)
|
||||
bool useCustomWaterLevel = false;
|
||||
|
||||
// Mark component as dirty (needs update)
|
||||
bool dirty = true;
|
||||
|
||||
void markDirty()
|
||||
{
|
||||
dirty = true;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_BUOYANCYINFO_HPP
|
||||
23
src/features/editScene/components/BuoyancyInfoModule.cpp
Normal file
23
src/features/editScene/components/BuoyancyInfoModule.cpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#include "BuoyancyInfo.hpp"
|
||||
#include "Transform.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/BuoyancyInfoEditor.hpp"
|
||||
|
||||
// Register BuoyancyInfo component
|
||||
REGISTER_COMPONENT("Buoyancy Info", BuoyancyInfo, BuoyancyInfoEditor)
|
||||
{
|
||||
registry.registerComponent<BuoyancyInfo>(
|
||||
"Buoyancy Info", std::make_unique<BuoyancyInfoEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<BuoyancyInfo>()) {
|
||||
e.set<BuoyancyInfo>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<BuoyancyInfo>()) {
|
||||
e.remove<BuoyancyInfo>();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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>();
|
||||
});
|
||||
}
|
||||
12
src/features/editScene/components/InWater.hpp
Normal file
12
src/features/editScene/components/InWater.hpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#ifndef EDITSCENE_INWATER_HPP
|
||||
#define EDITSCENE_INWATER_HPP
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Tag component indicating the entity is currently in water.
|
||||
* Automatically added/removed by BuoyancySystem.
|
||||
*/
|
||||
struct InWater {
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_INWATER_HPP
|
||||
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>();
|
||||
});
|
||||
}
|
||||
@@ -21,6 +21,22 @@ struct PlayerControllerComponent {
|
||||
Ogre::String idleState = "idle";
|
||||
Ogre::String walkState = "walking";
|
||||
Ogre::String runState = "running";
|
||||
|
||||
/* Swim animation states (used when character is in water) */
|
||||
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>();
|
||||
}
|
||||
});
|
||||
}
|
||||
43
src/features/editScene/components/WaterPhysics.hpp
Normal file
43
src/features/editScene/components/WaterPhysics.hpp
Normal file
@@ -0,0 +1,43 @@
|
||||
#ifndef WATER_PHYSICS_HPP
|
||||
#define WATER_PHYSICS_HPP
|
||||
|
||||
#include <Ogre.h>
|
||||
|
||||
struct WaterPhysics {
|
||||
// Water surface Y level (world space)
|
||||
float waterSurfaceY = -0.1f;
|
||||
|
||||
// Default buoyancy parameters (used when entity has no BuoyancyInfo)
|
||||
float defaultBuoyancy = 1.5f; // 1.0 = neutral, >1 floats (Jolt convention)
|
||||
float defaultLinearDrag = 0.5f; // Jolt recommends ~0.5
|
||||
float defaultAngularDrag = 0.01f; // Jolt recommends ~0.01
|
||||
float defaultSubmergedThreshold =
|
||||
0.1f; // Minimum submerged fraction to apply buoyancy
|
||||
|
||||
// Water density (kg/m³)
|
||||
float waterDensity = 1000.0f;
|
||||
|
||||
// Gravity acceleration (m/s²)
|
||||
float gravity = 9.81f;
|
||||
|
||||
// Enable/disable water physics globally
|
||||
bool enabled = true;
|
||||
|
||||
WaterPhysics() = default;
|
||||
|
||||
WaterPhysics(float surfaceY, float buoyancy, float linearDrag,
|
||||
float angularDrag, float submergedThreshold, float density,
|
||||
float grav, bool enable)
|
||||
: waterSurfaceY(surfaceY)
|
||||
, defaultBuoyancy(buoyancy)
|
||||
, defaultLinearDrag(linearDrag)
|
||||
, defaultAngularDrag(angularDrag)
|
||||
, defaultSubmergedThreshold(submergedThreshold)
|
||||
, waterDensity(density)
|
||||
, gravity(grav)
|
||||
, enabled(enable)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
#endif // WATER_PHYSICS_HPP
|
||||
22
src/features/editScene/components/WaterPhysicsModule.cpp
Normal file
22
src/features/editScene/components/WaterPhysicsModule.cpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#include "WaterPhysics.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/WaterPhysicsEditor.hpp"
|
||||
|
||||
// Register WaterPhysics component
|
||||
REGISTER_COMPONENT("Water Physics", WaterPhysics, WaterPhysicsEditor)
|
||||
{
|
||||
registry.registerComponent<WaterPhysics>(
|
||||
"Water Physics", std::make_unique<WaterPhysicsEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<WaterPhysics>()) {
|
||||
e.set<WaterPhysics>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<WaterPhysics>()) {
|
||||
e.remove<WaterPhysics>();
|
||||
}
|
||||
});
|
||||
}
|
||||
73
src/features/editScene/components/WaterPlane.hpp
Normal file
73
src/features/editScene/components/WaterPlane.hpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#ifndef EDITSCENE_WATERPLANE_HPP
|
||||
#define EDITSCENE_WATERPLANE_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
|
||||
/**
|
||||
* WaterPlane component
|
||||
* Visual water surface with reflection/refraction
|
||||
* for the editScene editor. Lightweight and OpenGL ES 2.0 compatible.
|
||||
*/
|
||||
struct WaterPlane {
|
||||
// Enable/disable water
|
||||
bool enabled = true;
|
||||
|
||||
// Water surface Y level (world space)
|
||||
float waterSurfaceY = -0.1f;
|
||||
|
||||
// Plane size (width and depth)
|
||||
float planeSize = 500.0f;
|
||||
|
||||
// Whether to update waterSurfaceY from WaterPhysics component
|
||||
bool autoUpdateFromWaterPhysics = true;
|
||||
|
||||
// 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;
|
||||
|
||||
// Render texture size (power of two recommended)
|
||||
int renderTextureSize = 512;
|
||||
|
||||
// Runtime objects (managed by EditorWaterPlaneSystem)
|
||||
Ogre::SceneNode *sceneNode = nullptr;
|
||||
Ogre::ManualObject *manualObject = nullptr;
|
||||
|
||||
// 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;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_WATERPLANE_HPP
|
||||
25
src/features/editScene/components/WaterPlaneModule.cpp
Normal file
25
src/features/editScene/components/WaterPlaneModule.cpp
Normal file
@@ -0,0 +1,25 @@
|
||||
#include "WaterPlane.hpp"
|
||||
#include "Transform.hpp"
|
||||
#include "EditorMarker.hpp"
|
||||
#include "EntityName.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/WaterPlaneEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Water Plane", "Water", WaterPlane, WaterPlaneEditor)
|
||||
{
|
||||
registry.registerComponent<WaterPlane>(
|
||||
"Water Plane", "Water",
|
||||
std::make_unique<WaterPlaneEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<WaterPlane>()) {
|
||||
e.set<WaterPlane>({});
|
||||
}
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<WaterPlane>()) {
|
||||
e.remove<WaterPlane>();
|
||||
}
|
||||
});
|
||||
}
|
||||
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
|
||||
@@ -9,11 +9,14 @@ int main(int argc, char *argv[])
|
||||
|
||||
// Parse command line arguments
|
||||
bool gameMode = false;
|
||||
bool debugBuoyancy = false;
|
||||
Ogre::String sceneFile;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
Ogre::String arg = argv[i];
|
||||
if (arg == "--game") {
|
||||
gameMode = true;
|
||||
} else if (arg == "--debug-buoyancy") {
|
||||
debugBuoyancy = true;
|
||||
} else if (arg.length() > 0 && arg[0] != '-') {
|
||||
sceneFile = arg;
|
||||
}
|
||||
@@ -23,14 +26,23 @@ int main(int argc, char *argv[])
|
||||
app.setGameMode(EditorApp::GameMode::Game);
|
||||
}
|
||||
|
||||
if (debugBuoyancy) {
|
||||
app.setDebugBuoyancy(true);
|
||||
}
|
||||
|
||||
app.initApp();
|
||||
|
||||
// Auto-load scene if provided as argument (editor mode only)
|
||||
if (!sceneFile.empty() && app.getGameMode() == EditorApp::GameMode::Editor) {
|
||||
std::cout << "Auto-loading scene: " << sceneFile << std::endl;
|
||||
SceneSerializer serializer(*app.getWorld(), app.getSceneManager());
|
||||
if (!sceneFile.empty() &&
|
||||
app.getGameMode() == EditorApp::GameMode::Editor) {
|
||||
std::cout << "Auto-loading scene: " << sceneFile
|
||||
<< std::endl;
|
||||
SceneSerializer serializer(*app.getWorld(),
|
||||
app.getSceneManager());
|
||||
if (!serializer.loadFromFile(sceneFile, nullptr)) {
|
||||
std::cerr << "Failed to load scene: " << serializer.getLastError() << std::endl;
|
||||
std::cerr << "Failed to load scene: "
|
||||
<< serializer.getLastError()
|
||||
<< std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -90,10 +90,10 @@ public:
|
||||
Layers::MOVING; // Non moving only collides with moving
|
||||
case Layers::MOVING:
|
||||
return true; // Moving collides with everything
|
||||
case Layers::SENSORS:
|
||||
return inObject2 ==
|
||||
Layers::MOVING; // Non moving only collides with moving
|
||||
default:
|
||||
case Layers::SENSORS:
|
||||
return inObject2 ==
|
||||
Layers::MOVING; // Non moving only collides with moving
|
||||
default:
|
||||
JPH_ASSERT(false);
|
||||
return false;
|
||||
}
|
||||
@@ -110,8 +110,8 @@ public:
|
||||
mObjectToBroadPhase[Layers::NON_MOVING] =
|
||||
BroadPhaseLayers::NON_MOVING;
|
||||
mObjectToBroadPhase[Layers::MOVING] = BroadPhaseLayers::MOVING;
|
||||
mObjectToBroadPhase[Layers::SENSORS] = BroadPhaseLayers::MOVING;
|
||||
}
|
||||
mObjectToBroadPhase[Layers::SENSORS] = BroadPhaseLayers::MOVING;
|
||||
}
|
||||
|
||||
virtual uint GetNumBroadPhaseLayers() const override
|
||||
{
|
||||
@@ -269,13 +269,13 @@ public:
|
||||
void DrawLine(JPH::RVec3Arg inFrom, JPH::RVec3Arg inTo,
|
||||
JPH::ColorArg inColor) override
|
||||
{
|
||||
JPH::Vec4 color = inColor.ToVec4();
|
||||
mLines.push_back(
|
||||
{ { (float)inFrom[0], (float)inFrom[1],
|
||||
(float)inFrom[2] },
|
||||
{ (float)inTo[0], (float)inTo[1], (float)inTo[2] },
|
||||
Ogre::ColourValue(color[0], color[1], color[2],
|
||||
color[3]) });
|
||||
JPH::Vec4 color = inColor.ToVec4();
|
||||
mLines.push_back(
|
||||
{ { (float)inFrom[0], (float)inFrom[1],
|
||||
(float)inFrom[2] },
|
||||
{ (float)inTo[0], (float)inTo[1], (float)inTo[2] },
|
||||
Ogre::ColourValue(color[0], color[1], color[2],
|
||||
color[3]) });
|
||||
}
|
||||
void DrawTriangle(JPH::RVec3Arg inV1, JPH::RVec3Arg inV2,
|
||||
JPH::RVec3Arg inV3, JPH::ColorArg inColor,
|
||||
@@ -422,14 +422,14 @@ DebugRenderer::DebugRenderer(Ogre::SceneManager *scnMgr,
|
||||
pass->setCullingMode(Ogre::CullingMode::CULL_NONE);
|
||||
pass->setVertexColourTracking(Ogre::TVC_AMBIENT);
|
||||
pass->setLightingEnabled(false);
|
||||
pass->setDepthWriteEnabled(false);
|
||||
pass->setDepthCheckEnabled(false);
|
||||
pass->setDepthWriteEnabled(false);
|
||||
pass->setDepthCheckEnabled(false);
|
||||
DebugRenderer::Initialize();
|
||||
scnMgr->getRootSceneNode()->attachObject(mObject);
|
||||
mLines.reserve(6000);
|
||||
mObject->estimateVertexCount(64000);
|
||||
mObject->estimateIndexCount(8000);
|
||||
mObject->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
|
||||
mObject->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
|
||||
}
|
||||
DebugRenderer::~DebugRenderer()
|
||||
{
|
||||
@@ -466,7 +466,7 @@ void CompoundShapeBuilder::addShape(JPH::ShapeRefC shape,
|
||||
const Ogre::Vector3 &position,
|
||||
const Ogre::Quaternion &rotation)
|
||||
{
|
||||
shapeSettings.AddShape(JoltPhysics::convert<JPH::Vec3>(position),
|
||||
shapeSettings.AddShape(JoltPhysics::convert<JPH::Vec3>(position),
|
||||
JoltPhysics::convert(rotation), shape.GetPtr());
|
||||
}
|
||||
JPH::ShapeRefC CompoundShapeBuilder::build()
|
||||
@@ -577,13 +577,13 @@ public:
|
||||
, job_system(JPH::cMaxPhysicsJobs, JPH::cMaxPhysicsBarriers,
|
||||
std::thread::hardware_concurrency() - 1)
|
||||
, mDebugRenderer(new DebugRenderer(scnMgr, cameraNode))
|
||||
, object_vs_broadphase_layer_filter{}
|
||||
, object_vs_object_layer_filter{}
|
||||
, object_vs_broadphase_layer_filter{}
|
||||
, object_vs_object_layer_filter{}
|
||||
, debugDraw(false)
|
||||
{
|
||||
static int instanceCount = 0;
|
||||
OgreAssert(instanceCount == 0, "Bad initialisation");
|
||||
instanceCount++;
|
||||
static int instanceCount = 0;
|
||||
OgreAssert(instanceCount == 0, "Bad initialisation");
|
||||
instanceCount++;
|
||||
|
||||
// This is the max amount of rigid bodies that you can add to the physics system. If you try to add more you'll get an error.
|
||||
// Note: This value is low because this is a simple test. For a real project use something in the order of 65536.
|
||||
@@ -773,7 +773,7 @@ public:
|
||||
timeAccumulator -= fixedDeltaTime;
|
||||
}
|
||||
for (JPH::BodyID bID : bodies) {
|
||||
JPH::RVec3 p;
|
||||
JPH::RVec3 p;
|
||||
JPH::Quat q;
|
||||
if (id2node.find(bID) == id2node.end())
|
||||
continue;
|
||||
@@ -790,12 +790,14 @@ 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(ch->GetPosition()));
|
||||
JoltPhysics::convert(
|
||||
ch->GetPosition()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -828,7 +830,7 @@ public:
|
||||
}
|
||||
static JPH::ShapeRefC createBoxShape(float x, float y, float z)
|
||||
{
|
||||
return new JPH::BoxShape(JPH::Vec3(x, y, z));
|
||||
return new JPH::BoxShape(JPH::Vec3(x, y, z));
|
||||
}
|
||||
static JPH::ShapeRefC createCylinderShape(float halfHeight,
|
||||
float radius)
|
||||
@@ -836,7 +838,7 @@ public:
|
||||
return new JPH::CylinderShape(halfHeight, radius);
|
||||
}
|
||||
JPH::BodyCreationSettings createBodyCreationSettings(
|
||||
JPH::Shape *shape, JPH::RVec3 &position, JPH::Quat &rotation,
|
||||
JPH::Shape *shape, JPH::RVec3 &position, JPH::Quat &rotation,
|
||||
JPH::EMotionType motionType, JPH::ObjectLayer layer)
|
||||
{
|
||||
JPH::BodyCreationSettings body_settings(
|
||||
@@ -845,7 +847,7 @@ public:
|
||||
}
|
||||
JPH::BodyCreationSettings
|
||||
createBodyCreationSettings(JPH::ShapeSettings *shapeSettings,
|
||||
JPH::RVec3 &position, JPH::Quat &rotation,
|
||||
JPH::RVec3 &position, JPH::Quat &rotation,
|
||||
JPH::EMotionType motionType,
|
||||
JPH::ObjectLayer layer)
|
||||
{
|
||||
@@ -870,8 +872,8 @@ public:
|
||||
{
|
||||
JPH::BodyInterface &body_interface =
|
||||
physics_system.GetBodyInterface();
|
||||
body_interface.AddAngularImpulse(
|
||||
id, JoltPhysics::convert<JPH::Vec3>(impulse));
|
||||
body_interface.AddAngularImpulse(
|
||||
id, JoltPhysics::convert<JPH::Vec3>(impulse));
|
||||
}
|
||||
JPH::BodyID createBody(const JPH::BodyCreationSettings &settings,
|
||||
ActivationListener *listener = nullptr)
|
||||
@@ -917,14 +919,14 @@ public:
|
||||
msp.ScaleToMass(mass);
|
||||
bodySettings.mMassPropertiesOverride = msp;
|
||||
}
|
||||
JPH::BodyID id = createBody(bodySettings, listener);
|
||||
if (shape->GetType() == JPH::EShapeType::HeightField) {
|
||||
JPH::BodyInterface &body_interface =
|
||||
physics_system.GetBodyInterface();
|
||||
body_interface.SetFriction(id, 1.0f);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
JPH::BodyID id = createBody(bodySettings, listener);
|
||||
if (shape->GetType() == JPH::EShapeType::HeightField) {
|
||||
JPH::BodyInterface &body_interface =
|
||||
physics_system.GetBodyInterface();
|
||||
body_interface.SetFriction(id, 1.0f);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
JPH::BodyID createBody(const JPH::Shape *shape, float mass,
|
||||
Ogre::SceneNode *node, JPH::EMotionType motion,
|
||||
JPH::ObjectLayer layer,
|
||||
@@ -934,8 +936,8 @@ public:
|
||||
const Ogre::Quaternion &rotation =
|
||||
node->_getDerivedOrientation();
|
||||
std::cout << "body position: " << position << std::endl;
|
||||
JPH::BodyID id = createBody(shape, mass, position, rotation,
|
||||
motion, layer, listener);
|
||||
JPH::BodyID id = createBody(shape, mass, position, rotation,
|
||||
motion, layer, listener);
|
||||
id2node[id] = node;
|
||||
node2id[node] = id;
|
||||
return id;
|
||||
@@ -982,7 +984,7 @@ public:
|
||||
JPH::MutableCompoundShape *master =
|
||||
static_cast<JPH::MutableCompoundShape *>(
|
||||
compoundShape.GetPtr());
|
||||
master->AddShape(JoltPhysics::convert<JPH::Vec3>(position),
|
||||
master->AddShape(JoltPhysics::convert<JPH::Vec3>(position),
|
||||
JoltPhysics::convert(rotation),
|
||||
childShape.GetPtr());
|
||||
}
|
||||
@@ -1156,46 +1158,54 @@ public:
|
||||
for (j = 0; j < indices.size() / 3; j++)
|
||||
triangles[j] = { indices[j * 3 + 0], indices[j * 3 + 1],
|
||||
indices[j * 3 + 2] };
|
||||
|
||||
|
||||
// Validate we have enough data
|
||||
if (vertices.size() < 3) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ERROR: Mesh has too few vertices: " + mesh->getName() +
|
||||
" (" + Ogre::StringConverter::toString(vertices.size()) + ")");
|
||||
"ERROR: Mesh has too few vertices: " +
|
||||
mesh->getName() + " (" +
|
||||
Ogre::StringConverter::toString(
|
||||
vertices.size()) +
|
||||
")");
|
||||
return JPH::ShapeRefC();
|
||||
}
|
||||
if (triangles.size() < 1) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ERROR: Mesh has no triangles: " + mesh->getName());
|
||||
"ERROR: Mesh has no triangles: " +
|
||||
mesh->getName());
|
||||
return JPH::ShapeRefC();
|
||||
}
|
||||
|
||||
|
||||
JPH::MeshShapeSettings mesh_shape_settings(vertices, triangles);
|
||||
|
||||
|
||||
// Configure settings to be more permissive
|
||||
mesh_shape_settings.mPerTriangleUserData = false;
|
||||
|
||||
|
||||
// Log mesh stats for debugging
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Creating mesh shape: " + mesh->getName() +
|
||||
" - Vertices: " + Ogre::StringConverter::toString(vertices.size()) +
|
||||
" - Triangles: " + Ogre::StringConverter::toString(triangles.size()));
|
||||
|
||||
"Creating mesh shape: " + mesh->getName() +
|
||||
" - Vertices: " +
|
||||
Ogre::StringConverter::toString(vertices.size()) +
|
||||
" - Triangles: " +
|
||||
Ogre::StringConverter::toString(triangles.size()));
|
||||
|
||||
// Sanitize the mesh data to help with validation
|
||||
mesh_shape_settings.Sanitize();
|
||||
|
||||
|
||||
JPH::ShapeSettings::ShapeResult result =
|
||||
mesh_shape_settings.Create();
|
||||
|
||||
|
||||
if (!result.Get()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ERROR: Failed to create mesh shape: " + mesh->getName() +
|
||||
"ERROR: Failed to create mesh shape: " +
|
||||
mesh->getName() +
|
||||
" - Error: " + result.GetError().c_str());
|
||||
return JPH::ShapeRefC();
|
||||
}
|
||||
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Successfully created mesh shape for: " + mesh->getName());
|
||||
"Successfully created mesh shape for: " +
|
||||
mesh->getName());
|
||||
return result.Get();
|
||||
}
|
||||
JPH::ShapeRefC createMeshShape(Ogre::String meshName)
|
||||
@@ -1206,7 +1216,7 @@ public:
|
||||
new Ogre::DefaultHardwareBufferManager;
|
||||
p = Ogre::DefaultHardwareBufferManager::getSingletonPtr();
|
||||
}
|
||||
|
||||
|
||||
Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().load(
|
||||
meshName, "General");
|
||||
if (mesh.get()) {
|
||||
@@ -1216,7 +1226,7 @@ public:
|
||||
}
|
||||
return createMeshShape(mesh);
|
||||
}
|
||||
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"ERROR: Could not load mesh for collider: " + meshName);
|
||||
return JPH::ShapeRefC();
|
||||
@@ -1361,9 +1371,9 @@ public:
|
||||
{
|
||||
int i;
|
||||
JPH::HeightFieldShapeSettings heightfieldSettings(
|
||||
samples, JoltPhysics::convert<JPH::Vec3>(offset),
|
||||
JoltPhysics::convert<JPH::Vec3>(scale),
|
||||
(uint32_t)sampleCount);
|
||||
samples, JoltPhysics::convert<JPH::Vec3>(offset),
|
||||
JoltPhysics::convert<JPH::Vec3>(scale),
|
||||
(uint32_t)sampleCount);
|
||||
for (i = 0; i < sampleCount; i++) {
|
||||
memcpy(heightfieldSettings.mHeightSamples.data() +
|
||||
sampleCount * i,
|
||||
@@ -1386,10 +1396,10 @@ public:
|
||||
"bad parameters");
|
||||
JPH::MutableCompoundShapeSettings settings;
|
||||
for (i = 0; i < shapes.size(); i++)
|
||||
settings.AddShape(
|
||||
JoltPhysics::convert<JPH::Vec3>(positions[i]),
|
||||
JoltPhysics::convert(rotations[i]),
|
||||
shapes[i].GetPtr());
|
||||
settings.AddShape(
|
||||
JoltPhysics::convert<JPH::Vec3>(positions[i]),
|
||||
JoltPhysics::convert(rotations[i]),
|
||||
shapes[i].GetPtr());
|
||||
JPH::ShapeSettings::ShapeResult result = settings.Create();
|
||||
OgreAssert(result.Get(), "Can not create compound shape");
|
||||
return result.Get();
|
||||
@@ -1405,10 +1415,10 @@ public:
|
||||
"bad parameters");
|
||||
JPH::StaticCompoundShapeSettings settings;
|
||||
for (i = 0; i < shapes.size(); i++)
|
||||
settings.AddShape(
|
||||
JoltPhysics::convert<JPH::Vec3>(positions[i]),
|
||||
JoltPhysics::convert(rotations[i]),
|
||||
shapes[i].GetPtr());
|
||||
settings.AddShape(
|
||||
JoltPhysics::convert<JPH::Vec3>(positions[i]),
|
||||
JoltPhysics::convert(rotations[i]),
|
||||
shapes[i].GetPtr());
|
||||
JPH::ShapeSettings::ShapeResult result = settings.Create();
|
||||
OgreAssert(result.Get(), "Can not create compound shape");
|
||||
return result.Get();
|
||||
@@ -1418,24 +1428,24 @@ public:
|
||||
JPH::ShapeRefC shape)
|
||||
{
|
||||
JPH::OffsetCenterOfMassShapeSettings settings(
|
||||
JoltPhysics::convert<JPH::Vec3>(offset),
|
||||
shape.GetPtr());
|
||||
JoltPhysics::convert<JPH::Vec3>(offset),
|
||||
shape.GetPtr());
|
||||
JPH::ShapeSettings::ShapeResult result = settings.Create();
|
||||
OgreAssert(result.Get(), "Can not create com offset shape");
|
||||
return result.Get();
|
||||
}
|
||||
JPH::ShapeRefC
|
||||
createRotatedTranslatedShape(const Ogre::Vector3 &offset,
|
||||
const Ogre::Quaternion rotation,
|
||||
JPH::ShapeRefC shape)
|
||||
{
|
||||
return JPH::RotatedTranslatedShapeSettings(
|
||||
JoltPhysics::convert<JPH::Vec3>(offset),
|
||||
JoltPhysics::convert(rotation), shape)
|
||||
.Create()
|
||||
.Get();
|
||||
}
|
||||
void applyBuoyancyImpulse(JPH::BodyID id,
|
||||
JPH::ShapeRefC
|
||||
createRotatedTranslatedShape(const Ogre::Vector3 &offset,
|
||||
const Ogre::Quaternion rotation,
|
||||
JPH::ShapeRefC shape)
|
||||
{
|
||||
return JPH::RotatedTranslatedShapeSettings(
|
||||
JoltPhysics::convert<JPH::Vec3>(offset),
|
||||
JoltPhysics::convert(rotation), shape)
|
||||
.Create()
|
||||
.Get();
|
||||
}
|
||||
void applyBuoyancyImpulse(JPH::BodyID id,
|
||||
const Ogre::Vector3 &surfacePosition,
|
||||
const Ogre::Vector3 &surfaceNormal,
|
||||
float buoyancy, float linearDrag,
|
||||
@@ -1446,12 +1456,12 @@ public:
|
||||
JPH::BodyLockWrite lock(physics_system.GetBodyLockInterface(),
|
||||
id);
|
||||
JPH::Body &body = lock.GetBody();
|
||||
body.ApplyBuoyancyImpulse(
|
||||
JoltPhysics::convert(surfacePosition),
|
||||
JoltPhysics::convert<JPH::Vec3>(surfaceNormal),
|
||||
buoyancy, linearDrag, angularDrag,
|
||||
JoltPhysics::convert<JPH::Vec3>(fluidVelocity),
|
||||
JoltPhysics::convert<JPH::Vec3>(gravity), dt);
|
||||
body.ApplyBuoyancyImpulse(
|
||||
JoltPhysics::convert(surfacePosition),
|
||||
JoltPhysics::convert<JPH::Vec3>(surfaceNormal),
|
||||
buoyancy, linearDrag, angularDrag,
|
||||
JoltPhysics::convert<JPH::Vec3>(fluidVelocity),
|
||||
JoltPhysics::convert<JPH::Vec3>(gravity), dt);
|
||||
}
|
||||
void applyBuoyancyImpulse(JPH::BodyID id,
|
||||
const Ogre::Vector3 &surfacePosition,
|
||||
@@ -1463,12 +1473,12 @@ public:
|
||||
JPH::BodyLockWrite lock(physics_system.GetBodyLockInterface(),
|
||||
id);
|
||||
JPH::Body &body = lock.GetBody();
|
||||
body.ApplyBuoyancyImpulse(
|
||||
JoltPhysics::convert(surfacePosition),
|
||||
JoltPhysics::convert<JPH::Vec3>(surfaceNormal),
|
||||
buoyancy, linearDrag, angularDrag,
|
||||
JoltPhysics::convert<JPH::Vec3>(fluidVelocity),
|
||||
physics_system.GetGravity(), dt);
|
||||
body.ApplyBuoyancyImpulse(
|
||||
JoltPhysics::convert(surfacePosition),
|
||||
JoltPhysics::convert<JPH::Vec3>(surfaceNormal),
|
||||
buoyancy, linearDrag, angularDrag,
|
||||
JoltPhysics::convert<JPH::Vec3>(fluidVelocity),
|
||||
physics_system.GetGravity(), dt);
|
||||
}
|
||||
bool isActive(JPH::BodyID id)
|
||||
{
|
||||
@@ -1491,7 +1501,7 @@ public:
|
||||
void getPositionAndRotation(JPH::BodyID id, Ogre::Vector3 &position,
|
||||
Ogre::Quaternion &rotation)
|
||||
{
|
||||
JPH::RVec3 _position;
|
||||
JPH::RVec3 _position;
|
||||
JPH::Quat _rotation;
|
||||
physics_system.GetBodyInterface().GetPositionAndRotation(
|
||||
id, _position, _rotation);
|
||||
@@ -1552,18 +1562,20 @@ public:
|
||||
}
|
||||
void setGravityFactor(JPH::BodyID id, float factor)
|
||||
{
|
||||
return physics_system.GetBodyInterface().SetGravityFactor(id,
|
||||
factor);
|
||||
return physics_system.GetBodyInterface().SetGravityFactor(
|
||||
id, factor);
|
||||
}
|
||||
void broadphaseQuery(float dt, const Ogre::Vector3 &position,
|
||||
std::set<JPH::BodyID> &inWater)
|
||||
{
|
||||
JPH::RVec3 surface_point = JoltPhysics::convert(
|
||||
JPH::RVec3 surface_point = JoltPhysics::convert(
|
||||
position + Ogre::Vector3(0, -0.1f, 0));
|
||||
|
||||
MyCollector collector(&physics_system, surface_point,
|
||||
JPH::Vec3::sAxisY(), dt);
|
||||
JPH::Vec3::sAxisY(), dt);
|
||||
// Apply buoyancy to all bodies that intersect with the water
|
||||
// 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));
|
||||
@@ -1584,36 +1596,50 @@ public:
|
||||
}
|
||||
}
|
||||
bool raycastQuery(Ogre::Vector3 startPoint, Ogre::Vector3 endPoint,
|
||||
Ogre::Vector3 &position, JPH::BodyID &id)
|
||||
Ogre::Vector3 &position, JPH::BodyID &id)
|
||||
{
|
||||
int i;
|
||||
Ogre::Vector3 direction = endPoint - startPoint;
|
||||
JPH::RRayCast ray{ JoltPhysics::convert(startPoint),
|
||||
JoltPhysics::convert<JPH::Vec3>(direction) };
|
||||
JoltPhysics::convert<JPH::Vec3>(direction) };
|
||||
JPH::RayCastResult hit;
|
||||
bool hadHit = physics_system.GetNarrowPhaseQuery().CastRay(
|
||||
ray, hit, {},
|
||||
JPH::SpecifiedObjectLayerFilter(Layers::NON_MOVING));
|
||||
if (hadHit) {
|
||||
if (hadHit) {
|
||||
position = JoltPhysics::convert(
|
||||
ray.GetPointOnRay(hit.mFraction));
|
||||
id = hit.mBodyID;
|
||||
}
|
||||
id = hit.mBodyID;
|
||||
}
|
||||
return hadHit;
|
||||
}
|
||||
bool bodyIsCharacter(JPH::BodyID id) const
|
||||
{
|
||||
return characterBodies.find(id) != characterBodies.end();
|
||||
}
|
||||
void destroyCharacter(std::shared_ptr<JPH::Character> ch)
|
||||
{
|
||||
characterBodies.erase(characterBodies.find(ch->GetBodyID()));
|
||||
characters.erase(ch.get());
|
||||
Ogre::SceneNode *node = id2node[ch->GetBodyID()];
|
||||
id2node.erase(ch->GetBodyID());
|
||||
node2id.erase(node);
|
||||
ch = nullptr;
|
||||
}
|
||||
bool bodyIsCharacter(JPH::BodyID id) const
|
||||
{
|
||||
return characterBodies.find(id) != characterBodies.end();
|
||||
}
|
||||
void destroyCharacter(std::shared_ptr<JPH::Character> ch)
|
||||
{
|
||||
characterBodies.erase(characterBodies.find(ch->GetBodyID()));
|
||||
characters.erase(ch.get());
|
||||
Ogre::SceneNode *node = id2node[ch->GetBodyID()];
|
||||
id2node.erase(ch->GetBodyID());
|
||||
node2id.erase(node);
|
||||
ch = nullptr;
|
||||
}
|
||||
Ogre::SceneNode *getSceneNodeFromBodyID(JPH::BodyID id) const
|
||||
{
|
||||
auto it = id2node.find(id);
|
||||
if (it != id2node.end())
|
||||
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()
|
||||
@@ -1631,20 +1657,20 @@ JoltPhysicsWrapper::JoltPhysicsWrapper(Ogre::SceneManager *scnMgr,
|
||||
// This needs to be done before any other Jolt function is called.
|
||||
JPH::RegisterDefaultAllocator();
|
||||
|
||||
// Install trace and assert callbacks
|
||||
// Install trace and assert callbacks
|
||||
JPH::Trace = TraceImpl;
|
||||
JPH_IF_ENABLE_ASSERTS(JPH::AssertFailed = AssertFailedImpl;)
|
||||
|
||||
// Create a factory, this class is responsible for creating instances of classes based on their name or hash and is mainly used for deserialization of saved data.
|
||||
// It is not directly used in this example but still required.
|
||||
// Create a factory, this class is responsible for creating instances of classes based on their name or hash and is mainly used for deserialization of saved data.
|
||||
// It is not directly used in this example but still required.
|
||||
JPH::Factory::sInstance = new JPH::Factory();
|
||||
// Register all physics types with the factory and install their collision handlers with the CollisionDispatch class.
|
||||
// If you have your own custom shape types you probably need to register their handlers with the CollisionDispatch before calling this function.
|
||||
// If you implement your own default material (PhysicsMaterial::sDefault) make sure to initialize it before this function or else this function will create one for you.
|
||||
// Register all physics types with the factory and install their collision handlers with the CollisionDispatch class.
|
||||
// If you have your own custom shape types you probably need to register their handlers with the CollisionDispatch before calling this function.
|
||||
// If you implement your own default material (PhysicsMaterial::sDefault) make sure to initialize it before this function or else this function will create one for you.
|
||||
JPH::RegisterTypes();
|
||||
|
||||
phys = std::make_unique<Physics>(scnMgr, cameraNode, nullptr,
|
||||
&contacts);
|
||||
phys = std::make_unique<Physics>(scnMgr, cameraNode, nullptr,
|
||||
&contacts);
|
||||
}
|
||||
|
||||
JoltPhysicsWrapper::~JoltPhysicsWrapper()
|
||||
@@ -1679,7 +1705,7 @@ JPH::ShapeRefC JoltPhysicsWrapper::createSphereShape(float radius)
|
||||
}
|
||||
|
||||
JPH::ShapeRefC JoltPhysicsWrapper::createCapsuleShape(float halfHeight,
|
||||
float radius)
|
||||
float radius)
|
||||
{
|
||||
return phys->createCapsuleShape(halfHeight, radius);
|
||||
}
|
||||
@@ -1739,14 +1765,14 @@ JPH::ShapeRefC
|
||||
JoltPhysicsWrapper::createOffsetCenterOfMassShape(const Ogre::Vector3 &offset,
|
||||
JPH::ShapeRefC shape)
|
||||
{
|
||||
return phys->createOffsetCenterOfMassShape(offset, shape);
|
||||
return phys->createOffsetCenterOfMassShape(offset, shape);
|
||||
}
|
||||
|
||||
JPH::ShapeRefC JoltPhysicsWrapper::createRotatedTranslatedShape(
|
||||
const Ogre::Vector3 &offset, const Ogre::Quaternion rotation,
|
||||
JPH::ShapeRefC shape)
|
||||
const Ogre::Vector3 &offset, const Ogre::Quaternion rotation,
|
||||
JPH::ShapeRefC shape)
|
||||
{
|
||||
return phys->createRotatedTranslatedShape(offset, rotation, shape);
|
||||
return phys->createRotatedTranslatedShape(offset, rotation, shape);
|
||||
}
|
||||
|
||||
JPH::BodyID
|
||||
@@ -1936,19 +1962,31 @@ void JoltPhysicsWrapper::removeContactListener(const JPH::BodyID &id)
|
||||
}
|
||||
bool JoltPhysicsWrapper::raycastQuery(Ogre::Vector3 startPoint,
|
||||
Ogre::Vector3 endPoint,
|
||||
Ogre::Vector3 &position, JPH::BodyID &id)
|
||||
Ogre::Vector3 &position, JPH::BodyID &id)
|
||||
{
|
||||
return phys->raycastQuery(startPoint, endPoint, position, id);
|
||||
return phys->raycastQuery(startPoint, endPoint, position, id);
|
||||
}
|
||||
|
||||
bool JoltPhysicsWrapper::bodyIsCharacter(JPH::BodyID id) const
|
||||
{
|
||||
return phys->bodyIsCharacter(id);
|
||||
return phys->bodyIsCharacter(id);
|
||||
}
|
||||
|
||||
void JoltPhysicsWrapper::destroyCharacter(std::shared_ptr<JPH::Character> ch)
|
||||
{
|
||||
phys->destroyCharacter(ch);
|
||||
phys->destroyCharacter(ch);
|
||||
}
|
||||
|
||||
Ogre::SceneNode *
|
||||
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;
|
||||
|
||||
@@ -45,14 +45,13 @@ static constexpr uint NUM_LAYERS(2);
|
||||
|
||||
namespace JoltPhysics
|
||||
{
|
||||
template<class T>
|
||||
Ogre::Vector3 convert(const T &vec)
|
||||
template <class T> Ogre::Vector3 convert(const T &vec)
|
||||
{
|
||||
return {vec[0], vec[1], vec[2]};
|
||||
return { vec[0], vec[1], vec[2] };
|
||||
}
|
||||
template<class T> T convert(const Ogre::Vector3 &vec)
|
||||
template <class T> T convert(const Ogre::Vector3 &vec)
|
||||
{
|
||||
return { vec.x, vec.y, vec.z };
|
||||
return { vec.x, vec.y, vec.z };
|
||||
}
|
||||
Ogre::Vector3 convert(const JPH::RVec3Arg &vec);
|
||||
JPH::RVec3 convert(const Ogre::Vector3 &vec);
|
||||
@@ -146,10 +145,11 @@ public:
|
||||
JPH::ShapeRefC
|
||||
createOffsetCenterOfMassShape(const Ogre::Vector3 &offset,
|
||||
JPH::ShapeRefC shape);
|
||||
JPH::ShapeRefC
|
||||
createRotatedTranslatedShape(const Ogre::Vector3 &offset, const Ogre::Quaternion rotation,
|
||||
JPH::ShapeRefC shape);
|
||||
JPH::BodyID createBody(const JPH::BodyCreationSettings &settings);
|
||||
JPH::ShapeRefC
|
||||
createRotatedTranslatedShape(const Ogre::Vector3 &offset,
|
||||
const Ogre::Quaternion rotation,
|
||||
JPH::ShapeRefC shape);
|
||||
JPH::BodyID createBody(const JPH::BodyCreationSettings &settings);
|
||||
JPH::BodyID createBody(const JPH::Shape *shape, float mass,
|
||||
const Ogre::Vector3 &position,
|
||||
const Ogre::Quaternion &rotation,
|
||||
@@ -226,8 +226,16 @@ public:
|
||||
listener);
|
||||
void removeContactListener(const JPH::BodyID &id);
|
||||
bool raycastQuery(Ogre::Vector3 startPoint, Ogre::Vector3 endPoint,
|
||||
Ogre::Vector3 &position, JPH::BodyID &id);
|
||||
bool bodyIsCharacter(JPH::BodyID id) const;
|
||||
void destroyCharacter(std::shared_ptr<JPH::Character> ch);
|
||||
Ogre::Vector3 &position, JPH::BodyID &id);
|
||||
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: '.*'
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user