Compare commits

...

128 Commits

Author SHA1 Message Date
slapin a553621c7f Fixed normal seams 2026-05-25 16:00:24 +03:00
slapin 86310e96f2 Seams are fixed 2026-05-25 05:07:23 +03:00
slapin 1a0fb87b93 some docs 2026-05-25 04:19:03 +03:00
slapin b71b599d9c Fixed inflation 2026-05-25 04:10:44 +03:00
slapin eea50adfcb Gap filling, improvements for character pipeline 2026-05-25 01:42:28 +03:00
slapin c7ef9283cd Fixed shape keys 2026-05-22 22:03:31 +03:00
slapin 3a3edf785c Character pipeline fixes 2026-05-22 18:09:26 +03:00
slapin fc486eea82 Pipeline update clothes 2026-05-21 20:04:06 +03:00
slapin b19033b557 outfitLevel moved, character ID safeguard 2026-05-21 15:36:27 +03:00
slapin 9968cb8c75 Lua scripts package 2026-05-21 12:36:08 +03:00
slapin aae7620512 Documentation update 2026-05-19 10:15:00 +03:00
slapin 970d0f9034 Save/Load system works well 2026-05-19 09:55:25 +03:00
slapin bea438bd50 No more animation jitter 2026-05-18 16:01:53 +03:00
slapin 3f40d84847 PackageTool and package library for assets 2026-05-16 20:50:26 +03:00
slapin 8630bfcf18 Updated APIs and tests 2026-05-14 13:32:32 +03:00
slapin 5bb20d416d Item registry 2026-05-14 02:28:33 +03:00
slapin eb0d05a577 Pause menu 2026-05-14 00:23:53 +03:00
slapin ef49506515 Pregnancy and birth 2026-05-13 23:31:59 +03:00
slapin 089d13520e Name generator 2026-05-11 16:37:32 +03:00
slapin 472af01e94 Now RPG data are in character registry 2026-05-11 15:03:31 +03:00
slapin f9e61dcb05 Update to use character registry; Character Slots fixes 2026-05-11 13:12:18 +03:00
slapin 42f6a218fb Now nude meshes work as intended 2026-05-11 00:30:47 +03:00
slapin ce888bc5bb Pipeline update 2026-05-10 14:43:57 +03:00
slapin 333a0b9938 Better tag support 2026-05-10 14:12:09 +03:00
slapin 11530dd7fc Dialogue uses arrays 2026-05-03 01:25:25 +03:00
slapin 3fd167ebff More events added 2026-05-03 01:11:14 +03:00
slapin 5952a96ee6 Fixed fonts sizes 2026-05-03 00:02:24 +03:00
slapin 76c3ead4a8 game_start event works 2026-05-02 23:43:48 +03:00
slapin 39a053d4ee Game mode API 2026-05-02 20:25:16 +03:00
slapin c5da977857 Dualogue event API doc/samples 2026-05-02 18:25:30 +03:00
slapin 3e7b0169d5 Lua behavior tree 2026-05-01 13:54:44 +03:00
slapin f918c5cefb Better handling of lua tasks 2026-05-01 04:02:47 +03:00
slapin 976ced3731 Lua-based behavior tree node 2026-05-01 00:31:06 +03:00
slapin 0fd8deaf53 direct save/load action database 2026-04-30 20:21:18 +03:00
slapin 4d843c18c7 Lua API 2026-04-30 19:07:35 +03:00
slapin 0ed83966da Lua action APIs 2026-04-30 10:03:56 +03:00
slapin 998984f75a Root motion fixed now 2026-04-29 18:45:37 +03:00
slapin 02fa78764a Lua API implemented 2026-04-29 14:13:50 +03:00
slapin abe6eef6b3 Modules update 2026-04-29 12:53:13 +03:00
slapin cca732b41b Fixes for event system 2026-04-27 20:53:04 +03:00
slapin 8507a3a501 Event system 2026-04-27 18:45:01 +03:00
slapin b9cce0248a Labels and actuators work perfectly! 2026-04-27 09:03:47 +03:00
slapin fa49bb5005 Actuators 2026-04-27 06:55:05 +03:00
slapin 37441aa8fd Motion fixed 2026-04-27 06:04:14 +03:00
slapin a1b74aa2d5 Now Path Following component works 2026-04-27 05:37:41 +03:00
slapin c80d9c96e6 AI motion refactoring 2026-04-27 05:24:45 +03:00
slapin a75db85027 Can disable physics stepping 2026-04-26 22:15:15 +03:00
slapin 7563937ab8 Prefab placement at cursor 2026-04-26 20:51:20 +03:00
slapin 425bb8411d Fixed crash with entity destruction 2026-04-26 17:56:19 +03:00
slapin 9b29b68b33 Prefab editing and window hiding 2026-04-26 17:45:57 +03:00
slapin 7557c710fb Added prefabs 2026-04-26 16:43:37 +03:00
slapin ce2f6c1306 Navmesh generation works with cell grids 2026-04-26 14:28:54 +03:00
slapin e0e8e316d4 Fixed navmesh 2026-04-26 00:50:55 +03:00
slapin abd2dc22d3 Now can test smart object action on player character again 2026-04-26 00:00:36 +03:00
slapin a5df60769f Now repeated smart object action works perfectly 2026-04-25 23:08:00 +03:00
slapin 75ba39895f Teleport node works 2026-04-25 21:55:21 +03:00
slapin 2cff982473 Delays and animation conflicts fixed 2026-04-25 20:59:50 +03:00
slapin 3bd2801d1d Smart objects work! 2026-04-25 09:04:12 +03:00
slapin 2e358275f0 Path following works great 2026-04-25 01:17:31 +03:00
slapin 5ed7552164 Path following 2026-04-25 00:11:21 +03:00
slapin 2b3482da88 Normal display tool implemented 2026-04-24 21:12:02 +03:00
slapin 1d2c330481 Material fixes, playing with navmesh 2026-04-24 20:29:54 +03:00
slapin a0d2561587 navmesh 2026-04-24 04:37:38 +03:00
slapin e95b904f4e Underwater effect 2026-04-23 01:55:09 +03:00
slapin 9d4fad1d10 Water plane 2026-04-23 01:29:53 +03:00
slapin 4335a8cb05 Skybox and sun 2026-04-23 00:38:20 +03:00
slapin d55bf970e0 Swim animations 2026-04-22 21:38:33 +03:00
slapin 30814ea35a Added animations for swimming 2026-04-22 19:50:14 +03:00
slapin 35f50f7f51 Fixed buoyancy 2026-04-22 18:57:33 +03:00
slapin ca5b5b3052 Buoyancy 2026-04-22 17:27:40 +03:00
slapin 7e4e8f6638 Color atlas serialization fixes 2026-04-21 11:42:46 +03:00
slapin c6fb3bb463 Camera works now 2026-04-21 03:59:45 +03:00
slapin 1411990def Forward/backwards fixed 2026-04-21 03:27:53 +03:00
slapin 1488d7d918 Camera change... 2026-04-21 03:04:47 +03:00
slapin ef708fa14a Fixed game mode menu 2026-04-20 22:10:17 +03:00
slapin 6d7fcb1157 Not so well working game mode 2026-04-20 20:21:27 +03:00
slapin 4313d190f9 Characters are fully functional now 2026-04-20 12:23:31 +03:00
slapin a2173114b9 Some questionable changes 2026-04-19 23:50:00 +03:00
slapin fb6881998c Added character physics but it does not work yet 2026-04-19 23:45:00 +03:00
slapin 529476d8cd Animation Tree was implemented 2026-04-19 22:06:46 +03:00
slapin 43e9fb330f Character Animation 2026-04-19 19:24:55 +03:00
slapin a392eb0bf9 Character display 2026-04-19 18:19:48 +03:00
slapin e2960d67e4 Added character models and lua scripts to groups 2026-04-18 11:35:38 +03:00
slapin 79b6af1fff Furniture vertical offset 2026-04-16 17:11:12 +03:00
slapin 863c401230 De-clutter the scene 2026-04-16 05:39:56 +03:00
slapin eec0d8f6f7 Physics works 2026-04-15 23:49:24 +03:00
slapin c2a1db5a65 The furniture is batched now 2026-04-15 04:28:30 +03:00
slapin 77f93659d5 Furiture placement works 2026-04-14 21:44:06 +03:00
slapin febeb8ff8d Furniture enabled 2026-04-14 18:54:28 +03:00
slapin 611dcd0d46 FPS/batch counter works 2026-04-14 13:55:39 +03:00
slapin e6494936d6 Atlas margin setting support 2026-04-14 13:09:14 +03:00
slapin e3b90e8bba Fixed room connectivity code 2026-04-14 12:35:20 +03:00
slapin 7846082220 valid connection doors positions 2026-04-13 11:51:26 +03:00
slapin a955f0b218 Almost good generation of room layout 2026-04-13 04:03:48 +03:00
slapin da4a1a6722 Now camera switching works! 2026-04-12 19:22:18 +03:00
slapin 21879c2784 Groups and components 2026-04-12 04:04:40 +03:00
slapin 5377d1a75a Material editing fixes 2026-04-12 01:36:58 +03:00
slapin 03f72bdd77 roof color editing 2026-04-11 14:59:43 +03:00
slapin 3c47a87768 Roof now works 2026-04-08 20:06:40 +03:00
slapin 4ba28fe512 Town components indicators 2026-04-07 11:37:43 +03:00
slapin 9f2f0be4a3 Adjusted frame geometry 2026-04-07 11:21:01 +03:00
slapin 7d64ba30cb CellGrid+Frames done 2026-04-07 01:39:32 +03:00
slapin 82c0e8c6ce Fixed doors and windows geometry 2026-04-07 01:18:48 +03:00
slapin 3798f227a7 Fixed external door geometry 2026-04-06 23:00:20 +03:00
slapin 19e4d80741 Proper walls geometry with frames 2026-04-06 19:31:10 +03:00
slapin d8122e3275 Fixed floor offset 2026-04-06 02:16:45 +03:00
slapin 0ebba40867 Corners finally match 2026-04-06 01:45:26 +03:00
slapin 64b03abb48 Fixed material for Lot geometry and CellGrid is re-created now 2026-04-05 19:51:22 +03:00
slapin cfd9dde5da Town plaza fixed 2026-04-05 17:23:28 +03:00
slapin 07101fcc64 Fixed crash on texture change 2026-04-04 05:36:08 +03:00
slapin b8c61da1f7 TriangleBuffer 2026-04-04 05:13:15 +03:00
slapin b1413d6d00 Procedural texture implementation 2026-04-04 03:42:50 +03:00
slapin f785339852 Added OgreProcedural support 2026-04-04 02:21:18 +03:00
slapin c2cbd0974d Added StaticGeometry support 2026-04-04 02:12:38 +03:00
slapin 9e72a48457 Fixed LOD support 2026-04-04 00:42:27 +03:00
slapin ebd875feac Static body check 2026-04-03 21:12:45 +03:00
slapin 2a2fd53c4f Camera and Light components 2026-04-03 20:08:47 +03:00
slapin d4061386ec Fixed crashes 2026-04-03 18:43:54 +03:00
slapin ec2695bc6c Now Mesh collider works 2026-04-02 05:54:58 +03:00
slapin d68da8fc04 physics save/load added 2026-04-02 02:40:40 +03:00
slapin 2371ba3b19 added grid and angle buttons 2026-04-02 00:18:50 +03:00
slapin bcf9291c03 Load meshes immediately 2026-04-01 20:11:43 +03:00
slapin c31892ac05 Load/Save dialogues implemented 2026-04-01 00:04:06 +03:00
slapin abd961ea0f Camera using CameraMan with working keys 2026-03-31 23:45:59 +03:00
slapin e5f4bbfb90 Keyboard camera navigation works now 2026-03-31 22:47:03 +03:00
slapin 3ebb41647e scene save implemented 2026-03-31 12:57:59 +03:00
slapin db15c6a48a Proper scene editor implementation 2026-03-31 02:48:01 +03:00
slapin 9c2adbb698 Added demo project to research root motion 2026-03-26 12:19:51 +03:00
668 changed files with 269202 additions and 5650 deletions
+1
View File
@@ -78,6 +78,7 @@ add_subdirectory(assets/blender/buildings/parts)
add_subdirectory(assets/blender/characters)
add_subdirectory(resources)
add_subdirectory(src/text_editor)
add_subdirectory(src/features)
add_executable(Game Game.cpp ${WATER_SRC})
target_include_directories(Game PRIVATE src/gamedata)
+65 -13
View File
@@ -47,15 +47,12 @@ set(VRM_IMPORTED_BLENDS
# COMMAND ${CMAKE_COMMAND} -E touch_nocreate ${CHARACTER_GLBS}
# DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models.py ${VRM_IMPORTED_BLENDS} ${EDITED_BLEND_TARGETS}
# WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
set(FEMALE_OBJECTS "BodyTopRobe;BodyTop;BodyBottom;BodyFeet;Hair;Face;BackHair;Accessoty")
set(MALE_OBJECTS "BodyTopRobe;BodyTop;BodyBottomPants;BodyBottom_Panties001;BodyBottom;BodyFeetPants;BodyFeetPantsShoes;BodyFeet;Hair;Face;BackHair;Accessory")
add_custom_command(
OUTPUT ${CMAKE_BINARY_DIR}/characters/male/normal-male.glb
COMMAND ${CMAKE_COMMAND} -E make_directory ${CREATE_DIRECTORIES}
COMMAND ${BLENDER} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py --
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated.blend
${CMAKE_BINARY_DIR}/characters/male/normal-male.glb
"${MALE_OBJECTS}"
"male"
tmp-edited-male.blend
COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_BINARY_DIR}/characters/male/normal-male.glb
@@ -76,7 +73,6 @@ add_custom_command(
COMMAND ${BLENDER} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py --
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated.blend
${CMAKE_BINARY_DIR}/characters/female/normal-female.glb
"${FEMALE_OBJECTS}"
"female"
tmp-edited-female.blend
COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_BINARY_DIR}/characters/female/normal-female.glb
@@ -164,9 +160,9 @@ function(blender_import_vrm BLEND VRM EDITABLE RIG)
COMMAND ${BLENDER} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/import_vrm2.py -- ${VRM_NAME}.vrm ${BLEND} ${EDITABLE} ${RIG}
COMMAND ${CMAKE_COMMAND} -D FILE=${BLEND} -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
COMMAND ${CMAKE_COMMAND} -E touch_nocreate ${BLEND}
DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/import_vrm2.py
DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/import_vrm2.py
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
${VRM}
${VRM} ${CMAKE_BINARY_DIR}/assets/blender/mixamo
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
endfunction()
@@ -328,8 +324,8 @@ function(add_blend_consolidation INPUT_BLEND COMBINED_BLEND OUTPUT_BLEND)
# Get the base name from the combined blend file for stamp derivation
get_filename_component(COMBINED_NAME "${COMBINED_BLEND}" NAME_WE)
# Remove "_combined" suffix if present
string(REGEX REPLACE "_combined$" "" TARGET_BASE "${COMBINED_NAME}")
# Remove "_combined" or "_shaped" suffix if present
string(REGEX REPLACE "_(combined|shaped)$" "" TARGET_BASE "${COMBINED_NAME}")
# Derive stamp dependency
get_filename_component(COMBINED_DIR "${COMBINED_BLEND}" DIRECTORY)
@@ -408,10 +404,10 @@ function(add_clothes_pipeline INPUT_BLEND WEIGHTED_BLEND FINAL_OUTPUT_BLEND)
${COMBINED_BLEND}
${SHAPED_BLEND}
)
# Step 2: Consolidate
# Step 2: Consolidate (use shaped blend which has shape keys transferred)
add_blend_consolidation(
"${INPUT_BLEND}"
"${COMBINED_BLEND}"
"${SHAPED_BLEND}"
"${FINAL_OUTPUT_BLEND}"
)
@@ -422,18 +418,74 @@ function(add_clothes_pipeline INPUT_BLEND WEIGHTED_BLEND FINAL_OUTPUT_BLEND)
)
endfunction()
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-male-top.blend)
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-male-bottom.blend)
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-male-feet.blend)
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-top.blend)
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-bottom.blend)
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-feet.blend)
# Propagate missing shape keys (like "fat") from Body_shapes to base body parts.
# This ensures all body part meshes have consistent shape keys, preventing seams.
function(add_shape_key_propagation INPUT_BLEND OUTPUT_BLEND)
add_custom_command(
OUTPUT ${OUTPUT_BLEND}
DEPENDS ${INPUT_BLEND}
${CMAKE_CURRENT_SOURCE_DIR}/add_missing_shape_keys.py
${CMAKE_CURRENT_SOURCE_DIR}/transfer_shape_keys.py
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
COMMAND ${CMAKE_COMMAND} -E copy ${INPUT_BLEND} ${OUTPUT_BLEND}
COMMAND ${BLENDER} -b -Y ${OUTPUT_BLEND}
-P ${CMAKE_CURRENT_SOURCE_DIR}/add_missing_shape_keys.py --
${OUTPUT_BLEND} ${OUTPUT_BLEND}
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
endfunction()
add_shape_key_propagation(
${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-male.blend
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-shapes.blend
)
add_shape_key_propagation(
${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-female.blend
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-shapes.blend
)
# male
add_clothes_pipeline(
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-shapes.blend" # INPUT_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-male-bottom_weighted.blend" # WEIGHTED_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated-bottom.blend" # BOTTOM_OUTPUT_BLEND
)
add_clothes_pipeline(
"${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-male.blend" # INPUT_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-male-bottom_weighted.blend" # WEIGHTED_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated-bottom.blend" # INPUT_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-male-top_weighted.blend" # WEIGHTED_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated-top.blend" # FINAL_OUTPUT_BLEND
)
add_clothes_pipeline(
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated-top.blend" # INPUT_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-male-feet_weighted.blend" # WEIGHTED_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated.blend" # FINAL_OUTPUT_BLEND
)
# female
add_clothes_pipeline(
"${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-female.blend" # INPUT_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-shapes.blend" # INPUT_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-female-bottom_weighted.blend" # WEIGHTED_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated-bottom.blend" # FINAL_OUTPUT_BLEND
)
add_clothes_pipeline(
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated-bottom.blend" # INPUT_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-female-top_weighted.blend" # WEIGHTED_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated-top.blend" # FINAL_OUTPUT_BLEND
)
add_clothes_pipeline(
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated-top.blend" # INPUT_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-female-feet_weighted.blend" # WEIGHTED_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated.blend" # FINAL_OUTPUT_BLEND
)
@@ -0,0 +1,141 @@
"""
Add missing shape keys from Body_shapes to body part objects.
Preserves existing shape keys.
Usage: blender -b --python add_missing_shape_keys.py -- <blend_file> <output_file>
"""
import bpy
import sys
import os
import mathutils
from mathutils.bvhtree import BVHTree
# Import functions from transfer_shape_keys.py
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from transfer_shape_keys import (
compute_robust_surface_mapping,
compute_side_preserving_position,
smooth_boundary_areas,
smooth_penetration_areas,
create_bvh_for_source,
load_source_data,
fix_seams_across_objects,
fix_normals_across_objects
)
def get_source_object_name(target_obj_info):
if target_obj_info.get('ref_shapes'):
return target_obj_info['ref_shapes']
return None
def add_missing_shape_keys(source_data, target_obj):
"""Add only missing shape keys to target object, preserving existing ones."""
if not target_obj.data.shape_keys:
target_obj.shape_key_add(name='Basis', from_mix=False)
existing_names = {kb.name for kb in target_obj.data.shape_keys.key_blocks}
missing_names = [n for n in source_data['names'] if n != 'Basis' and n not in existing_names]
if not missing_names:
print(f" {target_obj.name}: all shape keys present, skipping")
return
print(f" {target_obj.name}: adding missing keys: {missing_names}")
# Create missing shape keys
for sk_name in missing_names:
target_obj.shape_key_add(name=sk_name, from_mix=False)
target_obj.data.shape_keys.use_relative = True
# Compute mapping
mapping = compute_robust_surface_mapping(target_obj, source_data)
# Set values for missing shape keys
for sk_name in missing_names:
print(f" Setting {sk_name}...")
sk = target_obj.data.shape_keys.key_blocks[sk_name]
target_obj.data.shape_keys.use_relative = False
for i, v in enumerate(sk.data):
if i < len(mapping['target_verts']):
pos = compute_side_preserving_position(i, sk_name, mapping, source_data, 1.0)
v.co = pos
sk.data.update()
target_obj.data.update_tag()
bpy.context.view_layer.update()
target_obj.data.shape_keys.use_relative = True
target_obj.data.update_tag()
bpy.context.view_layer.update()
smooth_boundary_areas(target_obj, sk_name, mapping, source_data)
smooth_penetration_areas(target_obj, sk_name, mapping, source_data)
print(f" ✓ Added {sk_name}")
def process_blend(blend_path, output_path):
print(f"Processing: {blend_path}")
bpy.ops.wm.open_mainfile(filepath=blend_path)
# Find source object
source_obj = None
for name in ['Body_shapes', 'Body']:
if name in bpy.data.objects and bpy.data.objects[name].type == 'MESH':
obj = bpy.data.objects[name]
if obj.data.shape_keys and len(obj.data.shape_keys.key_blocks) > 1:
source_obj = obj
print(f"Found source: {name}")
break
if not source_obj:
print("ERROR: No source object with shape keys found")
return False
# Build source data
source_data = {
'names': [kb.name for kb in source_obj.data.shape_keys.key_blocks],
'vertex_positions': {},
'polygons': [],
'is_relative': source_obj.data.shape_keys.use_relative,
'source_obj_name': source_obj.name
}
for poly in source_obj.data.polygons:
source_data['polygons'].append([v for v in poly.vertices])
for sk in source_obj.data.shape_keys.key_blocks:
source_data['vertex_positions'][sk.name] = [(v.co.x, v.co.y, v.co.z) for v in sk.data]
print(f"Source shape keys: {source_data['names']}")
# Process body part objects
required_props = {'age', 'sex', 'slot'}
modified = False
for obj in bpy.data.objects:
if obj.type == 'MESH' and all(p in obj for p in required_props):
if obj == source_obj:
continue
add_missing_shape_keys(source_data, obj)
modified = True
# Fix seams: synchronize shape key offsets for vertices sharing
# the same basis position across body parts.
fix_seams_across_objects()
fix_normals_across_objects()
bpy.ops.wm.save_as_mainfile(filepath=output_path)
print(f"Saved: {output_path}")
return True
if __name__ == "__main__":
try:
args = sys.argv[sys.argv.index("--") + 1:]
if len(args) >= 2:
process_blend(args[0], args[1])
else:
print("Usage: blender -b -P script.py -- <input.blend> <output.blend>")
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+133 -26
View File
@@ -2,14 +2,21 @@ import bpy
import bmesh
import sys
import os
import json
import mathutils
from mathutils.bvhtree import BVHTree
def load_blend_files(clothes_blend_path, body_blend_path):
"""Load objects from blend files and return all loaded objects"""
"""Load objects from blend files and return all loaded objects
IMPORTANT: Body file must be loaded FIRST so its objects keep their original names.
The clothes file may contain reference meshes with the same names as body parts
(e.g., 'BodyTop' used for weight painting). If clothes are loaded first, the body's
real objects get renamed to 'BodyTop.001' etc., breaking the ref_part lookup.
"""
loaded_objects = []
for path in [clothes_blend_path, body_blend_path]:
for path in [body_blend_path, clothes_blend_path]:
with bpy.data.libraries.load(path) as (data_from, data_to):
data_to.objects = data_from.objects
for obj in data_to.objects:
@@ -36,34 +43,27 @@ def get_transformation_matrices(obj):
return m, m_inv, m_normal
def raycast_and_adjust_vertices(target_body, bvh_cloth, out_ray_length=0.15):
"""Raycast from body to cloth and adjust vertices that intersect"""
m_body, m_body_inv, m_body_normal = get_transformation_matrices(target_body)
"""Raycast from body to cloth and mark vertices that are covered.
Returns a list where 1 = vertex is covered by clothing, 0 = visible.
Does NOT modify vertex positions -- that used to cause visible steps
at clothing boundaries because border vertices kept the inward offset
while exposed neighbours did not."""
m_body, _, m_body_normal = get_transformation_matrices(target_body)
num_verts = len(target_body.data.vertices)
hit_values = [0] * num_verts
has_shape_keys = target_body.data.shape_keys is not None
# Forward raycast (into cloth)
for i, v in enumerate(target_body.data.vertices):
v_world = m_body @ v.co
n_world = (m_body_normal @ v.normal).normalized()
# Raycast forward (into cloth) and backward (from inside cloth)
# Raycast forward (outward) and backward (inward)
hit_f, _, _, _ = bvh_cloth.ray_cast(v_world, n_world, out_ray_length)
hit_b, _, _, _ = bvh_cloth.ray_cast(v_world, -n_world, 0.005)
if hit_f or hit_b:
hit_values[i] = 1
# Adjust vertex position to be slightly outside cloth
offset = -n_world * (0.005 if hit_f else 0.01)
new_co = m_body_inv @ (v_world + offset)
v.co = new_co
# Update shape keys if they exist
if has_shape_keys:
for kb in target_body.data.shape_keys.key_blocks:
kb.data[i].co = new_co
return hit_values
def protect_and_remove_hidden_geometry(target_body, hit_values, threshold=4.0):
@@ -158,6 +158,13 @@ def process_clothing_pair(clothing_obj, target_obj, whitelist, is_clothing_copy=
clothing_props[prop_name] = clothing_obj[prop_name]
print(f" Stored property '{prop_name}' = {clothing_obj[prop_name]} from clothing")
# Also store metadata tags for export pipeline
clothing_meta = {}
for prop_name in ["ref_layer", "ref_part", "garment_id", "tags", "age", "sex", "slot"]:
if prop_name in clothing_obj:
clothing_meta[prop_name] = clothing_obj[prop_name]
print(f" Stored meta '{prop_name}' = {clothing_obj[prop_name]} from clothing")
# Step A: Raycast & adjust vertices
out_ray_length = 0.015
if "ref_ray_length" in clothing_obj:
@@ -204,6 +211,26 @@ def process_clothing_pair(clothing_obj, target_obj, whitelist, is_clothing_copy=
new_target[prop_name] = prop_value
print(f" Copied property '{prop_name}' = {prop_value} to combined object")
# NEW CODE: Aggregate clothing metadata onto combined object
# Collect from all clothing objects processed for this target
# Use JSON string since Blender ID properties don't support Python lists/sets
if "_combined_meta" in new_target:
meta = json.loads(new_target["_combined_meta"])
else:
meta = {"garments": [], "tags": [], "layers": []}
if "garment_id" in clothing_meta:
meta["garments"].append(clothing_meta["garment_id"])
else:
meta["garments"].append(clothing_name_for_final)
if "tags" in clothing_meta and clothing_meta["tags"]:
for t in str(clothing_meta["tags"]).split(";"):
t = t.strip()
if t and t not in meta["tags"]:
meta["tags"].append(t)
if "ref_layer" in clothing_meta:
meta["layers"].append(int(clothing_meta["ref_layer"]))
new_target["_combined_meta"] = json.dumps(meta)
# Rename the combined object using the appropriate clothing name
new_target.name = f"{target_name}_{clothing_name_for_final}"
whitelist.add(new_target)
@@ -237,11 +264,66 @@ def run_batch_combine():
all_objs = bpy.data.objects
# Separate body objects (no ref_layer property)
body_objects = [o for o in all_objs if o.type == 'MESH' and "ref_layer" not in o]
# Filter out rig control shapes (cs_*), helper objects, and objects with no vertices
body_objects = []
skipped_objects = []
for o in all_objs:
if o.type != 'MESH' or "ref_layer" in o:
continue
# Skip rig control shapes and helper objects
if o.name.startswith("cs_") or o.name.startswith("WGT-") or o.name.startswith("MCH-") or o.name.startswith("ORG-"):
skipped_objects.append(o.name)
continue
# Skip objects with no vertices (empty meshes)
if len(o.data.vertices) == 0:
skipped_objects.append(o.name)
continue
body_objects.append(o)
# Separate clothing by layer
clothing_layer1 = [o for o in all_objs if o.type == 'MESH' and "ref_layer" in o and o["ref_layer"] == 1]
clothing_layer2 = [o for o in all_objs if o.type == 'MESH' and "ref_layer" in o and o["ref_layer"] == 2]
if skipped_objects:
print(f"Skipped {len(skipped_objects)} non-body mesh objects: {', '.join(skipped_objects[:10])}{'...' if len(skipped_objects) > 10 else ''}")
# Filter body objects: those missing age/sex/slot are treated as helpers/references
required_body_props = {"age", "sex", "slot"}
valid_body_objects = []
skipped_body_objects = []
for body_obj in body_objects:
missing = [p for p in required_body_props if p not in body_obj]
if missing:
print(f"WARNING: Mesh object '{body_obj.name}' is missing required body properties {missing}.")
print(f" Treating as helper/reference object (not a body part).")
print(f" Available properties: {[k for k in body_obj.keys() if not k.startswith('_')]}")
skipped_body_objects.append(body_obj.name)
else:
valid_body_objects.append(body_obj)
if skipped_body_objects:
print(f"Skipped {len(skipped_body_objects)} mesh objects treated as helpers: {', '.join(skipped_body_objects)}")
body_objects = valid_body_objects
# Separate clothing by layer, filtering out objects missing required clothing properties
required_clothing_props = {"ref_layer", "ref_part", "garment_id"}
clothing_layer1 = []
clothing_layer2 = []
skipped_clothing_objects = []
for o in all_objs:
if o.type != 'MESH' or "ref_layer" not in o:
continue
# Check if this is a valid clothing object (has required clothing properties)
missing = [p for p in required_clothing_props if p not in o]
if missing:
print(f"WARNING: Mesh object '{o.name}' has ref_layer but is missing required clothing properties {missing}.")
print(f" Treating as helper/reference object (not clothing).")
skipped_clothing_objects.append(o.name)
continue
if o["ref_layer"] == 1:
clothing_layer1.append(o)
elif o["ref_layer"] == 2:
clothing_layer2.append(o)
if skipped_clothing_objects:
print(f"Skipped {len(skipped_clothing_objects)} mesh objects treated as helpers: {', '.join(skipped_clothing_objects)}")
print(f"Found {len(body_objects)} body objects")
print(f"Found {len(clothing_layer1)} layer 1 clothing objects")
@@ -349,9 +431,34 @@ def run_batch_combine():
print(f"Layer 2 direct to body results: {len(layer2_results_direct)}")
print(f"Total combined objects: {len(all_results)}")
# Final cleanup - keep all combined objects and their armatures
# Finalize aggregated metadata on all combined objects
for obj in all_results:
whitelist.add(obj)
if "_combined_meta" in obj:
meta = json.loads(obj["_combined_meta"])
layers = meta.get("layers", [])
garments = meta.get("garments", [])
tags = meta.get("tags", [])
if layers:
obj["layer"] = max(layers)
else:
obj["layer"] = 0
if garments:
obj["garments"] = ";".join(garments)
else:
obj["garments"] = ""
if tags:
obj["clothing_tags"] = ";".join(sorted(set(tags)))
else:
obj["clothing_tags"] = ""
print(f"Finalized metadata for {obj.name}: layer={obj['layer']}, garments={obj['garments']}, tags={obj['clothing_tags']}")
# Remove temporary meta property
del obj["_combined_meta"]
cleanup_unused_objects(whitelist)
+22
View File
@@ -2,11 +2,18 @@ import bpy
import sys
import os
# Import seam/normal fix functions from transfer_shape_keys.py
_script_dir = os.path.dirname(os.path.abspath(__file__))
if _script_dir not in sys.path:
sys.path.insert(0, _script_dir)
from transfer_shape_keys import fix_seams_across_objects, fix_normals_across_objects
def process_append(source_files, output_path):
required_props = {"age", "sex", "slot"}
for file_path in source_files:
if not os.path.exists(file_path):
print(f"Warning: Source file not found: {file_path}")
continue
with bpy.data.libraries.load(file_path) as (data_from, data_to):
@@ -50,6 +57,13 @@ def process_append(source_files, output_path):
print(f"Processed {obj.name}: Parented and Modset to {arm_name}")
else:
print(f"Warning: Armature '{arm_name}' not found for {obj.name}")
elif obj.type == 'MESH':
# Object is a mesh but missing required properties - treat as helper/reference
missing = [p for p in required_props if p not in obj.keys()]
print(f"WARNING: Mesh object '{obj.name}' is missing required properties {missing}.")
print(f" Treating as helper/reference object (not a body part).")
print(f" Available properties: {[k for k in obj.keys() if not k.startswith('_')]}")
# Don't remove - keep helper/reference objects in the scene
else:
# Clean up data not meeting criteria
bpy.data.objects.remove(obj, do_unlink=True)
@@ -57,6 +71,14 @@ def process_append(source_files, output_path):
# 4. Recursive Purge of all unlinked data (Materials, Textures, Meshes)
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
# 5. Fix seams across ALL body parts in the consolidated scene.
# This synchronizes shape key offsets and normals for matching vertices
# across base body parts and combined clothing variants.
print("\nFixing seams across all consolidated body parts...")
fix_seams_across_objects()
fix_normals_across_objects()
print("Consolidated seam/normal fix complete.\n")
# Save
bpy.ops.wm.save_as_mainfile(filepath=output_path)
@@ -0,0 +1,501 @@
#!/usr/bin/env python3
"""
Garment System UI Panel for Blender
Run this script in Blender's Scripting editor (or via blender -b -P garment_system_ui.py)
to add a side-panel for editing garment-system custom properties on mesh objects.
Properties managed:
Body Part -> age, sex, slot, layer, garments, clothing_tags
Clothing -> ref_layer, ref_part, garment_id, ref_sex, ref_age, ref_clothing, tags
"""
import bpy
# ---------------------------------------------------------------------------
# Property helpers
# ---------------------------------------------------------------------------
SLOT_ITEMS = [
("face", "Face", ""),
("bottom", "Bottom", ""),
("top", "Top", ""),
("feet", "Feet", ""),
("hair", "Hair", ""),
]
SEX_ITEMS = [
("male", "Male", ""),
("female", "Female", ""),
]
AGE_ITEMS = [
("adult", "Adult", ""),
("child", "Child", ""),
("teen", "Teen", ""),
("baby", "Baby", ""),
]
def _ensure_prop(obj, key, default):
if key not in obj:
obj[key] = default
return obj[key]
def _get_prop_str(obj, key, default=""):
if key not in obj:
return default
return str(obj[key])
def _get_prop_int(obj, key, default=0):
if key not in obj:
return default
try:
return int(obj[key])
except (ValueError, TypeError):
return default
def _set_prop_str(obj, key, value):
if value:
obj[key] = value
elif key in obj:
del obj[key]
def _set_prop_int(obj, key, value):
obj[key] = int(value)
def _del_prop(obj, key):
if key in obj:
del obj[key]
# ---------------------------------------------------------------------------
# Operators
# ---------------------------------------------------------------------------
class GARMENT_OT_apply_body_part(bpy.types.Operator):
bl_idname = "garment.apply_body_part"
bl_label = "Apply Body Part"
bl_description = "Write body-part custom properties to the active object"
bl_options = {"REGISTER", "UNDO"}
age: bpy.props.EnumProperty(name="Age", items=AGE_ITEMS)
sex: bpy.props.EnumProperty(name="Sex", items=SEX_ITEMS)
slot: bpy.props.EnumProperty(name="Slot", items=SLOT_ITEMS)
layer: bpy.props.IntProperty(name="Layer", min=0, max=2, default=0)
garments: bpy.props.StringProperty(name="Garments", description="Semicolon-separated garment names")
clothing_tags: bpy.props.StringProperty(name="Clothing Tags", description="Semicolon-separated tags")
def execute(self, context):
obj = context.active_object
if not obj or obj.type != "MESH":
self.report({"WARNING"}, "Active object is not a mesh")
return {"CANCELLED"}
_set_prop_str(obj, "age", self.age)
_set_prop_str(obj, "sex", self.sex)
_set_prop_str(obj, "slot", self.slot)
_set_prop_int(obj, "layer", self.layer)
_set_prop_str(obj, "garments", self.garments)
_set_prop_str(obj, "clothing_tags", self.clothing_tags)
# Remove clothing-only props so they don't confuse the pipeline
for k in ("ref_layer", "ref_part", "garment_id", "ref_sex",
"ref_age", "ref_clothing", "tags"):
_del_prop(obj, k)
self.report({"INFO"}, f"Body-part props applied to {obj.name}")
return {"FINISHED"}
def invoke(self, context, event):
obj = context.active_object
if obj:
self.age = _get_prop_str(obj, "age", "adult")
self.sex = _get_prop_str(obj, "sex", "male")
self.slot = _get_prop_str(obj, "slot", "bottom")
self.layer = _get_prop_int(obj, "layer", 0)
self.garments = _get_prop_str(obj, "garments", "")
self.clothing_tags = _get_prop_str(obj, "clothing_tags", "")
return context.window_manager.invoke_props_dialog(self)
class GARMENT_OT_apply_clothing(bpy.types.Operator):
bl_idname = "garment.apply_clothing"
bl_label = "Apply Clothing"
bl_description = "Write clothing custom properties to the active object"
bl_options = {"REGISTER", "UNDO"}
ref_layer: bpy.props.IntProperty(name="Ref Layer", min=1, max=2, default=1,
description="1 = lingerie, 2 = clothing")
ref_part: bpy.props.StringProperty(name="Ref Part",
description="Target body-part object name, e.g. BodyBottom")
garment_id: bpy.props.StringProperty(name="Garment ID",
description="Garment name used for labels, e.g. panties7")
ref_sex: bpy.props.EnumProperty(name="Ref Sex", items=SEX_ITEMS)
ref_age: bpy.props.EnumProperty(name="Ref Age", items=AGE_ITEMS)
ref_clothing: bpy.props.StringProperty(name="Ref Clothing",
description="Mesh name used for weight transfer, e.g. BodyBottom")
tags: bpy.props.StringProperty(name="Tags", description="Semicolon-separated tags")
def execute(self, context):
obj = context.active_object
if not obj or obj.type != "MESH":
self.report({"WARNING"}, "Active object is not a mesh")
return {"CANCELLED"}
_set_prop_int(obj, "ref_layer", self.ref_layer)
_set_prop_str(obj, "ref_part", self.ref_part)
_set_prop_str(obj, "garment_id", self.garment_id)
_set_prop_str(obj, "ref_sex", self.ref_sex)
_set_prop_str(obj, "ref_age", self.ref_age)
_set_prop_str(obj, "ref_clothing", self.ref_clothing)
_set_prop_str(obj, "tags", self.tags)
# Remove body-part-only props so they don't confuse the pipeline
for k in ("age", "sex", "slot", "layer", "garments", "clothing_tags"):
_del_prop(obj, k)
self.report({"INFO"}, f"Clothing props applied to {obj.name}")
return {"FINISHED"}
def invoke(self, context, event):
obj = context.active_object
if obj:
self.ref_layer = _get_prop_int(obj, "ref_layer", 1)
self.ref_part = _get_prop_str(obj, "ref_part", "")
self.garment_id = _get_prop_str(obj, "garment_id", "")
self.ref_sex = _get_prop_str(obj, "ref_sex", "male")
self.ref_age = _get_prop_str(obj, "ref_age", "adult")
self.ref_clothing = _get_prop_str(obj, "ref_clothing", "")
self.tags = _get_prop_str(obj, "tags", "")
return context.window_manager.invoke_props_dialog(self)
class GARMENT_OT_auto_detect(bpy.types.Operator):
bl_idname = "garment.auto_detect"
bl_label = "Auto-detect from Name"
bl_description = "Guess sex/slot from object name"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
if not obj or obj.type != "MESH":
self.report({"WARNING"}, "Active object is not a mesh")
return {"CANCELLED"}
name = obj.name.lower()
changed = []
# Guess sex
if "female" in name:
obj["sex"] = "female"
changed.append("sex=female")
elif "male" in name:
obj["sex"] = "male"
changed.append("sex=male")
# Guess slot
slot_map = {
"face": "face",
"bottom": "bottom",
"top": "top",
"feet": "feet",
"hair": "hair",
}
for key, slot in slot_map.items():
if key in name:
obj["slot"] = slot
changed.append(f"slot={slot}")
break
# Guess layer from name hints
if any(x in name for x in ("panties", "lingerie", "underwear")):
obj["layer"] = 1
changed.append("layer=1")
elif any(x in name for x in ("pants", "skirt", "robe", "shirt", "dress", "cloth")):
obj["layer"] = 2
changed.append("layer=2")
else:
obj["layer"] = 0
changed.append("layer=0")
# Set garment_id from object name for clothing
if "ref_layer" in obj or "ref_part" in obj:
if "garment_id" not in obj or not obj["garment_id"]:
obj["garment_id"] = obj.name
changed.append(f"garment_id={obj.name}")
self.report({"INFO"}, "Auto-detect: " + ", ".join(changed) if changed else "nothing changed")
return {"FINISHED"}
class GARMENT_OT_clear_props(bpy.types.Operator):
bl_idname = "garment.clear_props"
bl_label = "Clear All Garment Props"
bl_description = "Remove all garment-system custom properties from the active object"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
if not obj:
return {"CANCELLED"}
keys = list(obj.keys())
removed = []
for k in keys:
if k in ("age", "sex", "slot", "layer", "garments", "clothing_tags",
"ref_layer", "ref_part", "garment_id", "ref_sex", "ref_age",
"ref_clothing", "tags"):
del obj[k]
removed.append(k)
self.report({"INFO"}, f"Removed {len(removed)} props" if removed else "No garment props found")
return {"FINISHED"}
class GARMENT_OT_sync_tags(bpy.types.Operator):
bl_idname = "garment.sync_tags"
bl_label = "Sync Tags from Garments"
bl_description = "Copy clothing_tags from garments field"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
if not obj:
return {"CANCELLED"}
garments = _get_prop_str(obj, "garments", "")
obj["clothing_tags"] = garments
self.report({"INFO"}, f"Tags set to: {garments}")
return {"FINISHED"}
class GARMENT_OT_check_scene(bpy.types.Operator):
bl_idname = "garment.check_scene"
bl_label = "Check Scene"
bl_description = "Validate all garment-system objects for pipeline readiness"
bl_options = {"REGISTER"}
def execute(self, context):
errors = []
warnings = []
body_count = 0
cloth_count = 0
ok_count = 0
required_body_props = {"age", "sex", "slot"}
required_clothing_props = {"ref_layer", "ref_part", "garment_id"}
for obj in bpy.data.objects:
if obj.type != "MESH":
continue
has_body_props = all(p in obj for p in required_body_props)
has_clothing_props = all(p in obj for p in required_clothing_props)
if has_body_props and has_clothing_props:
errors.append(f"'{obj.name}': has BOTH body-part and clothing properties (mixed)")
continue
if has_body_props:
body_count += 1
# Check body part values are valid
age = str(obj.get("age", ""))
sex = str(obj.get("sex", ""))
slot = str(obj.get("slot", ""))
valid_ages = {"adult", "child", "teen", "baby"}
valid_sexes = {"male", "female"}
valid_slots = {"face", "bottom", "top", "feet", "hair"}
if age not in valid_ages:
errors.append(f"'{obj.name}': invalid age='{age}' (valid: {', '.join(sorted(valid_ages))})")
if sex not in valid_sexes:
errors.append(f"'{obj.name}': invalid sex='{sex}' (valid: {', '.join(sorted(valid_sexes))})")
if slot not in valid_slots:
errors.append(f"'{obj.name}': invalid slot='{slot}' (valid: {', '.join(sorted(valid_slots))})")
ok_count += 1
elif has_clothing_props:
cloth_count += 1
# Check clothing values are valid
ref_sex = str(obj.get("ref_sex", ""))
ref_age = str(obj.get("ref_age", ""))
valid_ages = {"adult", "child", "teen", "baby"}
valid_sexes = {"male", "female"}
if ref_sex and ref_sex not in valid_sexes:
errors.append(f"'{obj.name}': invalid ref_sex='{ref_sex}' (valid: {', '.join(sorted(valid_sexes))})")
if ref_age and ref_age not in valid_ages:
errors.append(f"'{obj.name}': invalid ref_age='{ref_age}' (valid: {', '.join(sorted(valid_ages))})")
# Check ref_part points to an existing body object
ref_part = str(obj.get("ref_part", ""))
if ref_part and ref_part not in bpy.data.objects:
warnings.append(f"'{obj.name}': ref_part='{ref_part}' not found in scene")
ok_count += 1
else:
# Mesh object with no garment props - check if it should have them
if obj.name.startswith("Body") or any(x in obj.name.lower() for x in ("shoes", "pants", "shirt", "skirt", "dress", "hat", "hair", "top", "bottom")):
warnings.append(f"'{obj.name}': mesh object with no garment properties (may need age/sex/slot or ref_layer/ref_part/garment_id)")
# Report results
self.report({"INFO"}, f"Check complete: {body_count} body parts, {cloth_count} clothing, {len(errors)} errors, {len(warnings)} warnings")
# Print detailed results to console
print("\n" + "=" * 60)
print("GARMENT SYSTEM SCENE CHECK")
print("=" * 60)
print(f"Body parts: {body_count}")
print(f"Clothing: {cloth_count}")
print(f"OK objects: {ok_count}")
print(f"Errors: {len(errors)}")
print(f"Warnings: {len(warnings)}")
print("-" * 60)
if errors:
print("\nERRORS (must fix before pipeline run):")
for e in errors:
print(f" [ERROR] {e}")
if warnings:
print("\nWARNINGS (review recommended):")
for w in warnings:
print(f" [WARN] {w}")
if not errors and not warnings:
print("\n ✓ Scene looks good! Pipeline should run successfully.")
print("=" * 60)
return {"FINISHED"}
# ---------------------------------------------------------------------------
# Panel
# ---------------------------------------------------------------------------
class GARMENT_PT_panel(bpy.types.Panel):
bl_label = "Garment System"
bl_idname = "GARMENT_PT_panel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Garment"
def draw(self, context):
layout = self.layout
obj = context.active_object
if not obj:
layout.label(text="No active object")
return
layout.label(text=f"Object: {obj.name}", icon="OBJECT_DATA")
# Detect current type
is_body = any(k in obj for k in ("age", "sex", "slot"))
is_cloth = any(k in obj for k in ("ref_layer", "ref_part", "garment_id"))
if is_body and is_cloth:
layout.alert = True
layout.label(text="WARNING: mixed props", icon="ERROR")
layout.alert = False
elif is_body:
layout.label(text="Type: Body Part", icon="MESH_DATA")
elif is_cloth:
layout.label(text="Type: Clothing", icon="MOD_CLOTH")
else:
layout.label(text="Type: (unset)", icon="QUESTION")
layout.separator()
# Quick action buttons
row = layout.row(align=True)
row.operator("garment.apply_body_part", icon="MESH_DATA")
row = layout.row(align=True)
row.operator("garment.apply_clothing", icon="MOD_CLOTH")
layout.separator()
row = layout.row(align=True)
row.operator("garment.auto_detect", icon="VIEWZOOM")
row.operator("garment.clear_props", icon="X")
layout.separator()
# Show current body-part props
if is_body or not is_cloth:
box = layout.box()
box.label(text="Body Part Props", icon="MESH_DATA")
col = box.column(align=True)
col.label(text=f"age: {_get_prop_str(obj, 'age', '')}")
col.label(text=f"sex: {_get_prop_str(obj, 'sex', '')}")
col.label(text=f"slot: {_get_prop_str(obj, 'slot', '')}")
col.label(text=f"layer: {_get_prop_int(obj, 'layer', 0)}")
col.label(text=f"garments: {_get_prop_str(obj, 'garments', '')}")
col.label(text=f"clothing_tags: {_get_prop_str(obj, 'clothing_tags', '')}")
if _get_prop_str(obj, "garments", ""):
col.operator("garment.sync_tags", icon="FILE_REFRESH")
# Show current clothing props
if is_cloth:
box = layout.box()
box.label(text="Clothing Props", icon="MOD_CLOTH")
col = box.column(align=True)
col.label(text=f"ref_layer: {_get_prop_int(obj, 'ref_layer', 1)}")
col.label(text=f"ref_part: {_get_prop_str(obj, 'ref_part', '')}")
col.label(text=f"garment_id: {_get_prop_str(obj, 'garment_id', '')}")
col.label(text=f"ref_sex: {_get_prop_str(obj, 'ref_sex', '')}")
col.label(text=f"ref_age: {_get_prop_str(obj, 'ref_age', '')}")
col.label(text=f"ref_clothing: {_get_prop_str(obj, 'ref_clothing', '')}")
col.label(text=f"tags: {_get_prop_str(obj, 'tags', '')}")
layout.separator()
# Check scene button
layout.separator()
row = layout.row(align=True)
row.scale_y = 1.5
row.operator("garment.check_scene", icon="CHECKBOX_HLT")
# Scene overview
box = layout.box()
box.label(text="Scene Overview", icon="SCENE_DATA")
body_count = 0
cloth_count = 0
for o in bpy.data.objects:
if o.type != "MESH":
continue
if any(k in o for k in ("age", "sex", "slot")):
body_count += 1
elif any(k in o for k in ("ref_layer", "ref_part", "garment_id")):
cloth_count += 1
col = box.column(align=True)
col.label(text=f"Body parts: {body_count}")
col.label(text=f"Clothing: {cloth_count}")
# ---------------------------------------------------------------------------
# Registration
# ---------------------------------------------------------------------------
classes = [
GARMENT_OT_apply_body_part,
GARMENT_OT_apply_clothing,
GARMENT_OT_auto_detect,
GARMENT_OT_clear_props,
GARMENT_OT_sync_tags,
GARMENT_OT_check_scene,
GARMENT_PT_panel,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
if __name__ == "__main__":
register()
@@ -122,6 +122,10 @@ def process_batch():
remove_empty_vertex_groups(clothing, threshold=0.001)
# Set garment_id from object name if not already set
if "garment_id" not in clothing:
clothing["garment_id"] = clothing.name
# 7. Final Parenting
clothing.parent = rig
arm_mod = clothing.modifiers.new(name="Armature", type='ARMATURE')
@@ -0,0 +1,91 @@
import bpy
import sys
import os
def propagate_shape_keys(blend_path, output_path):
"""Copy missing shape keys from Body_shapes to all body part objects."""
print(f"Propagating shape keys from {blend_path} to {output_path}")
bpy.ops.wm.open_mainfile(filepath=blend_path)
# Find source object with all shape keys
source_obj = None
for name in ['Body_shapes', 'Body']:
if name in bpy.data.objects and bpy.data.objects[name].type == 'MESH':
obj = bpy.data.objects[name]
if obj.data.shape_keys and len(obj.data.shape_keys.key_blocks) > 1:
source_obj = obj
print(f"Found source object: {name}")
break
if not source_obj:
print("ERROR: No source object with shape keys found")
return False
source_keys = source_obj.data.shape_keys.key_blocks
source_basis = source_keys['Basis'].data
modified = False
for obj in bpy.data.objects:
if obj.type != 'MESH':
continue
# Only process body part objects
if not all(p in obj for p in ['age', 'sex', 'slot']):
continue
# Skip the source object itself
if obj == source_obj:
continue
if not obj.data.shape_keys:
# Create basis if missing
obj.shape_key_add(name='Basis', from_mix=False)
existing_names = {kb.name for kb in obj.data.shape_keys.key_blocks}
for sk in source_keys:
if sk.name == 'Basis':
continue
if sk.name in existing_names:
print(f" {obj.name}: already has '{sk.name}', skipping")
continue
# Copy shape key
print(f" {obj.name}: adding '{sk.name}' from {source_obj.name}")
new_sk = obj.shape_key_add(name=sk.name, from_mix=False)
# Transfer vertex positions relative to basis
# We need to map source vertices to target vertices
# For now, assume same topology (body parts were separated from same mesh)
num_verts = min(len(new_sk.data), len(sk.data), len(source_basis))
for i in range(num_verts):
# The shape key position in source
src_pos = sk.data[i].co
src_basis_pos = source_basis[i].co
# Offset from basis
offset = src_pos - src_basis_pos
# Apply to target using target's basis position
new_sk.data[i].co = obj.data.shape_keys.key_blocks['Basis'].data[i].co + offset
new_sk.data.update()
modified = True
if modified:
obj.data.update_tag()
bpy.context.view_layer.update()
bpy.ops.wm.save_as_mainfile(filepath=output_path)
print(f"Saved: {output_path}")
return True
if __name__ == "__main__":
try:
args = sys.argv[sys.argv.index("--") + 1:]
if len(args) >= 2:
propagate_shape_keys(args[0], args[1])
else:
print("Usage: blender -b -P script.py -- <input.blend> <output.blend>")
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
+283 -23
View File
@@ -137,9 +137,10 @@ def load_target_file(target_path):
# Store information about target objects without keeping references
target_objects_info = []
required_props = {'age', 'sex', 'slot'}
for obj in bpy.data.objects:
if obj.type == 'MESH' and obj.data:
if all(prop in obj for prop in ['age', 'sex', 'slot']):
if all(prop in obj for prop in required_props):
obj_info = {
'name': obj.name,
'age': obj['age'],
@@ -157,6 +158,24 @@ def load_target_file(target_path):
if obj_info['ref_shapes']:
print(f" - ref_shapes: '{obj_info['ref_shapes']}'")
if not target_objects_info:
print("\n" + "=" * 70)
print("WARNING: No target objects found with required properties (age, sex, slot)")
print("=" * 70)
print("\nThis likely means the combine_clothes.py step did not properly")
print("propagate age/sex/slot properties from body objects to combined objects,")
print("or the source blend file only contains helper/reference objects.")
print("\nAvailable mesh objects and their properties:")
for obj in bpy.data.objects:
if obj.type == 'MESH' and obj.data:
props = [k for k in obj.keys() if not k.startswith('_')]
has_req = all(p in obj for p in required_props)
marker = " [OK]" if has_req else " [MISSING age/sex/slot]"
print(f" - {obj.name}: {props}{marker}")
print("\nReturning empty target list. The pipeline will skip shape key transfer.")
print("=" * 70)
return target_objects_info, temp_target
return target_objects_info, temp_target
def get_target_object_by_name(obj_name):
@@ -166,22 +185,45 @@ def get_target_object_by_name(obj_name):
return None
def delete_existing_shape_keys(target_obj):
"""Delete all existing shape keys from target object"""
"""Delete all existing shape keys from target object.
CRITICAL: Blender's shape_key_remove() leaves mesh vertices in their
*deformed* state rather than restoring basis positions. We must save
the Basis positions before deletion and restore them afterwards."""
if not target_obj.data.shape_keys:
return False
# Save Basis positions before deletion
basis_positions = None
for kb in target_obj.data.shape_keys.key_blocks:
if kb.name == 'Basis':
basis_positions = [v.co.copy() for v in kb.data]
break
# Fallback: if no Basis exists, use current vertex positions
if basis_positions is None:
basis_positions = [v.co.copy() for v in target_obj.data.vertices]
num_keys = len(target_obj.data.shape_keys.key_blocks)
print(f" Deleting {num_keys} existing shape keys...")
bpy.context.view_layer.objects.active = target_obj
target_obj.select_set(True)
bpy.ops.object.mode_set(mode='OBJECT')
while target_obj.data.shape_keys:
target_obj.active_shape_key_index = 0
bpy.ops.object.shape_key_remove()
target_obj.select_set(False)
# RESTORE BASIS POSITIONS - Blender leaves mesh deformed after removal
for i, v in enumerate(target_obj.data.vertices):
if i < len(basis_positions):
v.co = basis_positions[i]
target_obj.data.update_tag()
bpy.context.view_layer.update()
print(f" Restored basis positions for {len(basis_positions)} vertices")
return True
def ensure_shape_keys_structure(shape_key_names, target_obj):
@@ -224,12 +266,16 @@ def compute_signed_distance_and_direction(bvh, point, reference_normal=None):
if location is None:
return None, None, None, None
# Determine sign using reference normal
# Determine sign using reference normal.
# For points outside (in the direction of the normal), signed_distance
# must be positive. For points inside, negative.
if reference_normal is not None:
to_surface = location - point
if to_surface.length > 0:
dot = to_surface.normalized().dot(reference_normal)
signed_distance = distance if dot > 0 else -distance
# Inverted relative to the no-reference-normal branch because
# to_surface here points *toward* the surface, not away.
signed_distance = -distance if dot > 0 else distance
else:
signed_distance = 0
else:
@@ -503,9 +549,9 @@ def enforce_side_constraint(pos, surface_point, reference_normal, target_side, c
# Very small offset to stay near surface
offset = abs((surface_point - pos).length) * 0.2
if target_side > 0:
return proj_point - reference_normal * offset
else:
return proj_point + reference_normal * offset
else:
return proj_point - reference_normal * offset
return pos
# Regular vertices - stronger enforcement
@@ -515,9 +561,9 @@ def enforce_side_constraint(pos, surface_point, reference_normal, target_side, c
offset = abs((surface_point - pos).length) * 0.95
if target_side > 0:
corrected_pos = proj_point - reference_normal * offset
else:
corrected_pos = proj_point + reference_normal * offset
else:
corrected_pos = proj_point - reference_normal * offset
return corrected_pos
@@ -724,15 +770,21 @@ def smooth_penetration_areas(target_obj, sk_name, mapping, source_data, threshol
problem_vertices = set()
bvh_deformed, _ = create_bvh_for_source(source_data, sk_name)
# Shape keys are in relative mode here, so current_positions are offsets.
# The BVH is built from absolute source positions, so we must convert
# relative offsets to absolute positions before querying it.
basis_positions = mapping['target_verts']
for i, pos in enumerate(current_positions):
if i < len(mapping['surface_points']) and i < len(mapping['side_flags']):
surface_point = mapping['surface_points'][i]
target_side = mapping['side_flags'][i]
if target_side != 0:
location, _, _, _ = bvh_deformed.find_nearest(pos)
if target_side != 0 and i < len(basis_positions):
abs_pos = basis_positions[i] + pos
location, _, _, _ = bvh_deformed.find_nearest(abs_pos)
if location and i < len(mapping['direction_vectors']):
to_surface = pos - location
to_surface = abs_pos - location
current_side = 1 if to_surface.dot(mapping['direction_vectors'][i]) > 0 else -1
if current_side != target_side:
@@ -860,7 +912,7 @@ def set_shape_key_with_side_preservation(target_obj, sk_name, mapping, source_da
smooth_penetration_areas(target_obj, sk_name, mapping, source_data)
def test_shape_key_quality(target_obj, sk_name, mapping, source_data):
"""Test shape key quality with side tracking"""
"""Test shape key quality with side tracking (READ-ONLY - does not modify shape key data)"""
sk = target_obj.data.shape_keys.key_blocks[sk_name]
@@ -870,6 +922,9 @@ def test_shape_key_quality(target_obj, sk_name, mapping, source_data):
prev_positions = None
boundary_vertices = mapping.get('boundary_vertices', set())
# Save original shape key data so we can restore it after testing
original_positions = [v.co.copy() for v in sk.data]
for val in test_values:
if val == 0.0:
sk.value = 0.0
@@ -913,6 +968,13 @@ def test_shape_key_quality(target_obj, sk_name, mapping, source_data):
prev_positions = [v.co.copy() for v in target_obj.data.vertices]
# Restore original shape key data (the test should not modify the shape key)
target_obj.data.shape_keys.use_relative = False
for i, v in enumerate(sk.data):
if i < len(original_positions):
v.co = original_positions[i]
target_obj.data.shape_keys.use_relative = True
sk.value = 0.0
target_obj.data.update_tag()
bpy.context.view_layer.update()
@@ -980,16 +1042,197 @@ def process_target_object(target_obj_info, source_data, current_file_path):
mapping = transfer_shape_keys(source_data, target_obj)
verify_transfer(source_data['names'], target_obj)
# Test quality of 'fat' shape key if it exists
for sk_name in source_data['names']:
if sk_name == 'fat':
test_shape_key_quality(target_obj, sk_name, mapping, source_data)
break
print(f" {'=' * 40}")
print(f" ✓ Completed")
return True
def get_body_part_objects():
"""Find all mesh objects that are body parts (have age/sex/slot)"""
result = []
required_props = {'age', 'sex', 'slot'}
for obj in bpy.data.objects:
if obj.type == 'MESH' and obj.data and all(p in obj for p in required_props):
result.append(obj)
return result
def round_pos(co, tolerance=0.0001):
"""Round a coordinate to tolerance-based buckets for spatial hashing"""
return (round(co.x / tolerance),
round(co.y / tolerance),
round(co.z / tolerance))
def fix_seams_across_objects(tolerance=0.0001):
"""
Post-process shape keys to ensure vertices sharing the same basis
position (within tolerance) get identical offsets. This fixes seams
between body parts and UV seams within the same part.
"""
body_parts = get_body_part_objects()
if not body_parts:
print("No body part objects found for seam fixing")
return
print(f"\n{'=' * 60}")
print("SEAM FIX: Synchronizing shape key offsets across body parts")
print(f"{'=' * 60}")
print(f"Found {len(body_parts)} body part objects")
# Collect shape key names from first object that has them
shape_key_names = []
for obj in body_parts:
if obj.data.shape_keys and obj.data.shape_keys.key_blocks:
for kb in obj.data.shape_keys.key_blocks:
if kb.name != 'Basis':
shape_key_names.append(kb.name)
break
if not shape_key_names:
print("No shape keys found, skipping seam fix")
return
print(f"Synchronizing {len(shape_key_names)} shape key(s): {', '.join(shape_key_names)}")
fixed_vertices = 0
fixed_groups = 0
for sk_name in shape_key_names:
# Build spatial hash: position_key -> list of (obj, vert_idx, offset)
pos_map = {}
for obj in body_parts:
if not obj.data.shape_keys or sk_name not in obj.data.shape_keys.key_blocks:
continue
sk = obj.data.shape_keys.key_blocks[sk_name]
for i, v in enumerate(obj.data.vertices):
if i >= len(sk.data):
continue
pos_key = round_pos(v.co, tolerance)
offset = sk.data[i].co.copy()
if pos_key not in pos_map:
pos_map[pos_key] = []
pos_map[pos_key].append((obj, i, offset))
# Average offsets for each position group with multiple vertices
for pos_key, entries in pos_map.items():
if len(entries) < 2:
continue
# Compute average offset
avg_offset = mathutils.Vector((0.0, 0.0, 0.0))
for obj, idx, offset in entries:
avg_offset += offset
avg_offset /= len(entries)
# Apply averaged offset to all vertices in this group
for obj, idx, offset in entries:
sk = obj.data.shape_keys.key_blocks[sk_name]
sk.data[idx].co = avg_offset.copy()
fixed_vertices += 1
fixed_groups += 1
# Update all meshes
for obj in body_parts:
if obj.data.shape_keys:
for kb in obj.data.shape_keys.key_blocks:
kb.data.update()
obj.data.update_tag()
bpy.context.view_layer.update()
print(f"Fixed {fixed_vertices} vertices across {fixed_groups} position groups")
print(f"Seam fix complete")
def fix_normals_across_objects(tolerance=0.0001):
"""
Post-process normals to ensure vertices sharing the same basis
position (within tolerance) get identical normals. This fixes
lighting seams between body parts.
"""
body_parts = get_body_part_objects()
if not body_parts:
print("No body part objects found for normal fixing")
return
print(f"\n{'=' * 60}")
print("NORMAL FIX: Synchronizing normals across body parts")
print(f"{'=' * 60}")
print(f"Found {len(body_parts)} body part objects")
# Build spatial hash: position_key -> list of normals
pos_map = {}
for obj in body_parts:
obj.data.calc_normals()
for v in obj.data.vertices:
pos_key = round_pos(v.co, tolerance)
if pos_key not in pos_map:
pos_map[pos_key] = []
pos_map[pos_key].append(v.normal.copy())
# Compute averaged normals for positions with multiple vertices
avg_normals = {}
for pos_key, normals in pos_map.items():
if len(normals) < 2:
continue
avg = mathutils.Vector((0.0, 0.0, 0.0))
for n in normals:
avg += n
if avg.length > 0.001:
avg.normalize()
avg_normals[pos_key] = avg
if not avg_normals:
print("No matching vertices found, skipping normal fix")
return
print(f"Found {len(avg_normals)} position groups with matching vertices")
# Apply averaged normals to each object
fixed_loops = 0
fixed_verts = 0
for obj in body_parts:
obj.data.use_auto_smooth = True
if not obj.data.has_custom_normals:
obj.data.create_normals_split()
obj.data.calc_normals_split()
vert_normal_map = {}
for i, v in enumerate(obj.data.vertices):
pos_key = round_pos(v.co, tolerance)
if pos_key in avg_normals:
vert_normal_map[i] = avg_normals[pos_key]
if not vert_normal_map:
continue
custom_normals = []
for loop in obj.data.loops:
if loop.vertex_index in vert_normal_map:
n = vert_normal_map[loop.vertex_index]
custom_normals.append((n.x, n.y, n.z))
fixed_loops += 1
else:
if hasattr(obj.data, 'corner_normals'):
cn = obj.data.corner_normals[loop.index]
custom_normals.append((cn.vector.x, cn.vector.y, cn.vector.z))
else:
custom_normals.append((loop.normal.x, loop.normal.y, loop.normal.z))
obj.data.normals_split_custom_set(custom_normals)
fixed_verts += len(vert_normal_map)
obj.data.update_tag()
bpy.context.view_layer.update()
print(f"Fixed {fixed_verts} vertices ({fixed_loops} loops) across {len(body_parts)} objects")
print(f"Normal fix complete")
def main():
print("=" * 60)
print("Blender Shape Key Transfer Script - Boundary Velocity Limiting")
@@ -1006,7 +1249,14 @@ def main():
if not target_objects_info:
print("\nNo target objects found with required properties")
sys.exit(1)
print("Skipping shape key transfer. Saving target file as-is.")
# Save the target file as-is (no shape keys transferred)
bpy.ops.wm.save_as_mainfile(filepath=output_file)
print(f"\nSaved output (unchanged): {output_file}")
print("=" * 60)
print("Script completed (no shape keys transferred)")
print("=" * 60)
return
print(f"\nFound {len(target_objects_info)} target objects to process")
@@ -1041,6 +1291,16 @@ def main():
print(f"\nProgress saved to: {temp_progress_file}")
# Post-process: fix seams by synchronizing offsets across body parts
print(f"\nLoading final working file for seam fix...")
bpy.ops.wm.open_mainfile(filepath=working_file)
fix_seams_across_objects()
fix_normals_across_objects()
# Save after seam fix
print(f"\nSaving after seam fix...")
bpy.ops.wm.save_as_mainfile(filepath=working_file)
# Copy the final working file to the output location
print(f"\nCopying final result to: {output_file}")
shutil.copy2(working_file, output_file)
@@ -354,7 +354,11 @@ class OgreMaterialGenerator(object):
else:
image_filepath = bpy.path.abspath(image.filepath, library=image.library)
image_filepath = os.path.normpath(image_filepath)
if not os.path.isfile(image_filepath):
logger.warning("Skipping texture copy: source path is not a file (%s)", image_filepath)
return
# Should we update the file
update = False
if os.path.isfile(target_filepath):
+161 -16
View File
@@ -186,26 +186,32 @@ def extra_linear(angle, offset):
mapping_blend_path = argv[0]
mapping_gltf_path = argv[1]
mapping_objects = argv[2]
mapping_armature_name = argv[3]
mapping_outfile = argv[4]
# Backward compat: support both 4-arg (blend, gltf, armature, outfile) and 5-arg (blend, gltf, objects, armature, outfile)
if len(argv) >= 5:
mapping_objects = argv[2]
mapping_armature_name = argv[3]
mapping_outfile = argv[4]
else:
mapping_objects = "AUTO"
mapping_armature_name = argv[2]
mapping_outfile = argv[3]
#for mapping in [ExportMappingFemale(), ExportMappingMale(), ExportMappingMaleBabyShape(), ExportMappingMaleEdited(), ExportMappingFemaleEdited(), ExportMappingMaleTestShapeEdited(), ExportMappingMaleBaseShapeEdited()]:
class CommandLineMapping:
blend_path = mapping_blend_path
gltf_path = mapping_gltf_path
# ogre_scene = "characters/female/vroid-normal-female.scene"
inner_path = "Object"
# objs = ["male", "Body", "Hair", "Face", "BackHair", "Tops", "Bottoms", "Shoes", "Accessory"]
# objs = ["female", "Body", "Hair", "Face", "BackHair", "Tops", "Bottoms", "Shoes", "Accessory"]
objs = []
armature_name = mapping_armature_name
outfile = mapping_outfile
default_action = 'default'
auto_discover = False
def __init__(self):
self.objs = [mapping_armature_name]
if len(mapping_objects) > 0:
if len(mapping_objects) > 0 and mapping_objects != "AUTO":
self.objs += [o.strip() for o in mapping_objects.split(";")]
else:
self.auto_discover = True
self.files = []
for fobj in self.objs:
self.files.append({"name": fobj})
@@ -224,11 +230,30 @@ for mapping in[CommandLineMapping()]:
bpy.app.driver_namespace["angle_to_linear_x"] = angle_to_linear_x
print("Driver setup done...")
bpy.ops.wm.append(
filepath=os.path.join(mapping.blend_path, mapping.inner_path),
directory=os.path.join(mapping.blend_path, mapping.inner_path),
files=mapping.files)
print("Append done...")
if mapping.auto_discover:
# Load all objects from the blend file so we can discover meshes by custom props
with bpy.data.libraries.load(mapping.blend_path) as (data_from, data_to):
data_to.objects = data_from.objects
for obj in data_to.objects:
if obj is not None:
try:
bpy.context.collection.objects.link(obj)
except RuntimeError:
pass # Already linked
print("Library load done...")
discovered = []
for ob in bpy.data.objects:
if ob.type == 'MESH' and all(p in ob.keys() for p in ["age", "sex", "slot"]):
discovered.append(ob.name)
mapping.objs += discovered
print(f"Auto-discovered {len(discovered)} body part objects: {discovered}")
else:
bpy.ops.wm.append(
filepath=os.path.join(mapping.blend_path, mapping.inner_path),
directory=os.path.join(mapping.blend_path, mapping.inner_path),
files=mapping.files)
print("Append done...")
prepare_armature(mapping)
print("Armature done...")
@@ -239,6 +264,21 @@ for mapping in[CommandLineMapping()]:
bpy.data.objects.remove(ob)
elif ob.name.startswith("Face") and ob.name != "Face":
bpy.data.objects.remove(ob)
# Remove auto-discovered objects that weren't meant for export
elif mapping.auto_discover and ob.type == 'MESH' and ob.name not in mapping.objs:
if not ob.name.endswith("-noimp"):
ob.name = ob.name + "-noimp"
# Remove .001 suffixed duplicates that may have leaked from consolidate step
# These are objects that have the same base name as an object in mapping.objs
# but with a .001 suffix (e.g., BodyBottom.001 when BodyBottom is in mapping.objs)
if mapping.auto_discover:
for ob in list(bpy.data.objects):
if ob.type == 'MESH' and ob.name.endswith('.001'):
base_name = ob.name[:-4]
if base_name in mapping.objs:
print(f"Removing duplicate '{ob.name}' (base '{base_name}' already in export list)")
bpy.data.objects.remove(ob, do_unlink=True)
print("Removing original armature and actions...")
orig_arm = bpy.data.objects[mapping.armature_name + '_orig']
@@ -298,9 +338,18 @@ for mapping in[CommandLineMapping()]:
obj = bpy.data.objects.get(name)
if obj and obj.type == 'MESH':
# 1. Rename Mesh Data
if not obj.data.name.startswith(prefix):
obj.data.name = prefix + obj.data.name
# 1. Rename Mesh Data to match object name (not mesh data name)
# This prevents .001 suffixed mesh data names (e.g., BodyBottom.001)
# from becoming male_BodyBottom.001 and clashing with male_BodyBottom.
new_mesh_name = prefix + name
if obj.data.name != new_mesh_name:
# Check if another mesh data already has this name
if new_mesh_name in bpy.data.meshes and bpy.data.meshes[new_mesh_name] != obj.data:
old = bpy.data.meshes[new_mesh_name]
old.name = new_mesh_name + "_old"
print(f"Renamed conflicting mesh data '{old.name}' for object '{name}'")
obj.data.name = new_mesh_name
print(f"Renamed mesh data from '{obj.data.name}' to '{new_mesh_name}' for object '{name}'")
# 2. Iterate through all Material Slots on the object
for slot in obj.material_slots:
@@ -317,17 +366,113 @@ for mapping in[CommandLineMapping()]:
save_data[key] = obj[key]
if key.startswith("body_"):
save_data[key.replace("body_", "", 1)] = obj[key]
# Export aggregated clothing metadata
if "layer" in obj:
save_data["layer"] = int(obj["layer"])
else:
save_data["layer"] = 0
tags = []
if "garments" in obj and obj["garments"]:
garments = [g.strip() for g in str(obj["garments"]).split(";") if g.strip()]
save_data["garments"] = garments
tags.extend(garments)
else:
save_data["garments"] = []
if "clothing_tags" in obj and obj["clothing_tags"]:
clothing_tags = [t.strip() for t in str(obj["clothing_tags"]).split(";") if t.strip()]
tags.extend(clothing_tags)
# Deduplicate and sort
seen = set()
unique_tags = []
for t in tags:
t_lower = t.lower()
if t_lower not in seen:
seen.add(t_lower)
unique_tags.append(t)
save_data["tags"] = unique_tags
# Export shape keys if present
# IMPORTANT: Skip "Basis" to match OGRE's pose indexing.
# OGRE's blender2ogre exporter skips the Basis shape key (index 0),
# so pose index 0 in OGRE = first non-Basis shape key.
shape_keys = []
if obj.data.shape_keys and obj.data.shape_keys.key_blocks:
for sk in obj.data.shape_keys.key_blocks:
if sk.name != 'Basis':
shape_keys.append(sk.name)
save_data["shape_keys"] = shape_keys
save_data["mesh"] = obj.data.name + ".mesh"
json_dir = os.path.dirname(mapping.gltf_path)
save_file = json_dir + "/body_part_" + obj.data.name + ".json"
json_filepath = os.path.join(json_dir, save_file)
with open(json_filepath, 'w') as f:
json.dump(save_data, f)
json.dump(save_data, f, indent=2)
# Triangulate body part meshes before OGRE export to prevent the exporter's
# triangulation from producing mismatched split normals at seams.
# BodyTop and BodyBottom have different face topologies, so the exporter's
# bmesh triangulation creates slightly different corner normals for matching
# vertices. Pre-triangulating ensures the exporter sees already-triangulated
# meshes and preserves our custom normals.
import bmesh
body_part_objs = []
for ob in bpy.data.objects:
if ob.type == 'MESH' and all(p in ob for p in ["age", "sex", "slot"]):
body_part_objs.append(ob)
print(f"\nTriangulating {len(body_part_objs)} body part meshes for OGRE export...")
for obj in body_part_objs:
mesh = obj.data
bm = bmesh.new()
bm.from_mesh(mesh)
bmesh.ops.triangulate(bm, faces=bm.faces)
bm.to_mesh(mesh)
bm.free()
mesh.calc_normals()
print(f" Triangulated '{obj.name}': {len(mesh.polygons)} polygons")
# Re-run normal fix on triangulated meshes to ensure matching vertices
# have identical custom split normals after triangulation.
chars_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "characters")
if chars_dir not in sys.path:
sys.path.insert(0, chars_dir)
from transfer_shape_keys import fix_normals_across_objects
fix_normals_across_objects()
armobj = bpy.data.objects.get(mapping.armature_name)
armobj.data.name = armobj.name
bpy.ops.ogre.export(filepath=mapping.gltf_path.replace(".glb", ".scene"), EX_SELECTED_ONLY=False, EX_SHARED_ARMATURE=True, EX_LOD_GENERATION='0', EX_LOD_DISTANCE=20, EX_LOD_LEVELS=4, EX_GENERATE_TANGENTS='4')
# Post-process exported OGRE meshes to fix normal/tangent seams between
# body parts. Even with identical custom normals in Blender, the OGRE
# exporter's calc_tangents() produces slightly different tangents for
# matching vertices because BodyTop and BodyBottom have different face
# topologies. This causes a visible lighting seam with normal mapping,
# especially when shape keys (like "fat") are applied.
ogre_export_dir = os.path.dirname(mapping.gltf_path.replace(".glb", ".scene"))
fix_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fix_ogre_mesh_seams.py")
if os.path.exists(fix_script):
print(f"\nPost-processing OGRE meshes in {ogre_export_dir}...")
import subprocess
import shutil
# Use system python3 if available, since sys.executable in Blender
# may point to the Blender binary itself, not a Python interpreter.
python_exe = shutil.which('python3') or shutil.which('python') or sys.executable
result = subprocess.run(
[python_exe, fix_script, ogre_export_dir],
capture_output=True, text=True
)
print(result.stdout)
if result.returncode != 0:
print(f"WARNING: Mesh seam fix failed: {result.stderr}")
else:
print(f"WARNING: fix_ogre_mesh_seams.py not found at {fix_script}")
bpy.ops.wm.read_homefile(use_empty=True)
time.sleep(2)
bpy.ops.wm.quit_blender()
@@ -0,0 +1,445 @@
#!/usr/bin/env python3
"""
Post-process exported OGRE mesh files to fix normal/tangent seams
between body part meshes at matching vertex positions.
Also fixes pose normal offsets (shape key normals) so that animated
shape keys don't reintroduce seams at runtime.
Usage: python3 fix_ogre_mesh_seams.py <mesh_directory> [ogre_xml_converter_path]
"""
import os
import sys
import subprocess
import xml.etree.ElementTree as ET
import math
# Default path to OgreXMLConverter (can be overridden via argument or env var)
DEFAULT_OGRE_XML_CONVERTER = os.environ.get(
'OGRETOOLS_XML_CONVERTER',
'/media/slapin/library/ogre3/ogre-sdk/bin/OgreXMLConverter'
)
# Body part prefixes to process
BODY_PREFIXES = ['BodyTop', 'BodyBottom', 'BodyFeet']
def find_converter():
"""Find OgreXMLConverter executable."""
if len(sys.argv) >= 3:
return sys.argv[2]
if os.path.exists(DEFAULT_OGRE_XML_CONVERTER):
return DEFAULT_OGRE_XML_CONVERTER
# Try PATH
for path in os.environ.get('PATH', '').split(os.pathsep):
exe = os.path.join(path, 'OgreXMLConverter')
if os.path.exists(exe):
return exe
return None
def mesh_to_xml(mesh_path, converter):
xml_path = mesh_path + '.xml'
result = subprocess.run(
[converter, mesh_path, xml_path],
capture_output=True, text=True
)
if result.returncode != 0:
print(f"WARNING: Failed to convert {mesh_path} to XML:")
print(result.stdout)
print(result.stderr)
return None
return xml_path
def xml_to_mesh(xml_path, converter):
mesh_path = xml_path[:-4] # remove .xml
result = subprocess.run(
[converter, xml_path, mesh_path],
capture_output=True, text=True
)
if result.returncode != 0:
print(f"WARNING: Failed to convert {xml_path} to binary:")
print(result.stdout)
print(result.stderr)
return None
return mesh_path
def parse_mesh_xml(xml_path):
"""Parse mesh XML and return (tree, vertices list, poses dict)."""
try:
tree = ET.parse(xml_path)
except ET.ParseError as e:
print(f"ERROR: Failed to parse {xml_path}: {e}")
return None, None, None
root = tree.getroot()
sharedgeom = root.find('.//sharedgeometry')
if sharedgeom is None:
return None, None, None
vbs = sharedgeom.findall('vertexbuffer')
if len(vbs) < 1:
return None, None, None
# The first vertexbuffer has positions and normals
pos_norm_vb = vbs[0]
# The second vertexbuffer (if any) has texcoords and tangents
tex_tan_vb = vbs[1] if len(vbs) > 1 else None
pn_verts = pos_norm_vb.findall('vertex')
tt_verts = tex_tan_vb.findall('vertex') if tex_tan_vb is not None else []
vertices = []
for i, pnv in enumerate(pn_verts):
pos = pnv.find('position')
normal = pnv.find('normal')
if pos is None or normal is None:
continue
p = (float(pos.get('x')), float(pos.get('y')), float(pos.get('z')))
n = (float(normal.get('x')), float(normal.get('y')), float(normal.get('z')))
t = None
if i < len(tt_verts):
tangent = tt_verts[i].find('tangent')
if tangent is not None:
t = (float(tangent.get('x')), float(tangent.get('y')), float(tangent.get('z')))
vertices.append({
'index': i,
'pos': p,
'normal': n,
'tangent': t,
})
# Parse poses: pose_name -> {vertex_index -> (nx, ny, nz)}
poses = {}
for pose in root.findall('.//poses/pose'):
name = pose.get('name')
pose_offsets = {}
for po in pose.findall('poseoffset'):
idx = int(po.get('index'))
nx = float(po.get('nx', 0.0))
ny = float(po.get('ny', 0.0))
nz = float(po.get('nz', 0.0))
pose_offsets[idx] = (nx, ny, nz)
poses[name] = pose_offsets
return tree, vertices, poses
def round_pos(p, tolerance=0.0001):
return (
round(p[0] / tolerance) * tolerance,
round(p[1] / tolerance) * tolerance,
round(p[2] / tolerance) * tolerance,
)
def vector_normalize(v):
length = math.sqrt(v[0]**2 + v[1]**2 + v[2]**2)
if length > 0.0001:
return (v[0] / length, v[1] / length, v[2] / length)
return v
def fix_seams(mesh_dir, converter):
"""Fix normal/tangent/pose-normal seams across all body part meshes in directory."""
# Find all body part mesh files
mesh_files = []
for f in os.listdir(mesh_dir):
if not f.endswith('.mesh') or f.endswith('.mesh.xml'):
continue
for prefix in BODY_PREFIXES:
if prefix in f:
mesh_files.append(os.path.join(mesh_dir, f))
break
if not mesh_files:
print(f"No body part meshes found in {mesh_dir}")
return 0
print(f"\n[OGRE Mesh Seam Fix] Found {len(mesh_files)} body part meshes")
for mf in mesh_files:
print(f" - {os.path.basename(mf)}")
# Convert all to XML
xml_paths = []
for mesh_path in mesh_files:
print(f"\nConverting {os.path.basename(mesh_path)} to XML...")
xml_path = mesh_to_xml(mesh_path, converter)
if xml_path:
xml_paths.append(xml_path)
else:
print(f" SKIPPED (conversion failed)")
if not xml_paths:
print("No meshes could be converted to XML, aborting.")
return 0
# Parse all XMLs
mesh_data = {}
all_pose_names = set()
for xml_path in xml_paths:
name = os.path.basename(xml_path)[:-9] # remove .mesh.xml
tree, vertices, poses = parse_mesh_xml(xml_path)
if vertices:
mesh_data[name] = {
'tree': tree,
'vertices': vertices,
'poses': poses,
'xml_path': xml_path,
}
all_pose_names.update(poses.keys())
else:
print(f"WARNING: No vertices found in {name}")
if not mesh_data:
print("No mesh data could be parsed, aborting.")
return 0
# Build global position -> list of (normal, tangent) map for BASIS
pos_map = {}
for name, data in mesh_data.items():
for v in data['vertices']:
key = round_pos(v['pos'], 0.0001)
if key not in pos_map:
pos_map[key] = []
pos_map[key].append((v['normal'], v['tangent']))
# Compute averaged normals and tangents for positions appearing in 2+ meshes
avg_data = {}
multi_mesh_positions = 0
for pos_key, entries in pos_map.items():
if len(entries) < 2:
continue
multi_mesh_positions += 1
# Average normals
avg_n = [0.0, 0.0, 0.0]
for n, t in entries:
avg_n[0] += n[0]
avg_n[1] += n[1]
avg_n[2] += n[2]
avg_n = vector_normalize(avg_n)
# Average tangents (only if at least one entry has a tangent)
avg_t = [0.0, 0.0, 0.0]
tangent_count = 0
for n, t in entries:
if t is not None:
avg_t[0] += t[0]
avg_t[1] += t[1]
avg_t[2] += t[2]
tangent_count += 1
if tangent_count > 0:
avg_t = vector_normalize(avg_t)
else:
avg_t = None
avg_data[pos_key] = {
'normal': avg_n,
'tangent': avg_t,
}
print(f"\nFound {multi_mesh_positions} positions shared across 2+ meshes")
print(f"Averaging normals/tangents for {len(avg_data)} positions...")
# Build global position -> list of pose normal offsets for EACH pose
# pose_name -> {pos_key -> list of (nx, ny, nz)}
pose_pos_maps = {}
for pose_name in all_pose_names:
pose_pos_map = {}
for name, data in mesh_data.items():
if pose_name not in data['poses']:
continue
pose_offsets = data['poses'][pose_name]
for v in data['vertices']:
if v['index'] not in pose_offsets:
continue
key = round_pos(v['pos'], 0.0001)
if key not in pose_pos_map:
pose_pos_map[key] = []
pose_pos_map[key].append(pose_offsets[v['index']])
pose_pos_maps[pose_name] = pose_pos_map
# Compute averaged pose normal offsets for positions appearing in 2+ meshes
avg_pose_data = {}
for pose_name, pose_pos_map in pose_pos_maps.items():
avg_offsets = {}
for pos_key, entries in pose_pos_map.items():
if len(entries) < 2:
continue
avg_n = [0.0, 0.0, 0.0]
for nx, ny, nz in entries:
avg_n[0] += nx
avg_n[1] += ny
avg_n[2] += nz
avg_n = vector_normalize(avg_n)
avg_offsets[pos_key] = avg_n
if avg_offsets:
avg_pose_data[pose_name] = avg_offsets
if avg_pose_data:
print(f"Averaging pose normal offsets for {len(avg_pose_data)} pose(s):")
for pose_name in sorted(avg_pose_data.keys()):
print(f" - {pose_name}: {len(avg_pose_data[pose_name])} positions")
else:
print("No pose normal offsets to fix.")
# Update all meshes with averaged values
modified_count = 0
for name, data in mesh_data.items():
tree = data['tree']
vertices = data['vertices']
modified = False
normal_fixes = 0
tangent_fixes = 0
pose_normal_fixes = 0
root = tree.getroot()
sharedgeom = root.find('.//sharedgeometry')
if sharedgeom is None:
continue
vbs = sharedgeom.findall('vertexbuffer')
if len(vbs) < 1:
continue
pos_norm_vb = vbs[0]
tt_verts = vbs[1].findall('vertex') if len(vbs) > 1 else []
pn_verts = pos_norm_vb.findall('vertex')
# Fix basis normals and tangents
for v in vertices:
key = round_pos(v['pos'], 0.0001)
if key not in avg_data:
continue
avg = avg_data[key]
# Update normal
pnv = pn_verts[v['index']]
normal = pnv.find('normal')
if normal is not None:
old_n = (float(normal.get('x')), float(normal.get('y')), float(normal.get('z')))
new_n = avg['normal']
diff = math.sqrt(
(old_n[0]-new_n[0])**2 +
(old_n[1]-new_n[1])**2 +
(old_n[2]-new_n[2])**2
)
if diff > 0.000001:
normal.set('x', f"{new_n[0]:.6f}")
normal.set('y', f"{new_n[1]:.6f}")
normal.set('z', f"{new_n[2]:.6f}")
normal_fixes += 1
# Update tangent
if avg['tangent'] is not None and v['tangent'] is not None:
if v['index'] < len(tt_verts):
ttv = tt_verts[v['index']]
tangent = ttv.find('tangent')
if tangent is not None:
old_t = (float(tangent.get('x')), float(tangent.get('y')), float(tangent.get('z')))
new_t = avg['tangent']
diff = math.sqrt(
(old_t[0]-new_t[0])**2 +
(old_t[1]-new_t[1])**2 +
(old_t[2]-new_t[2])**2
)
if diff > 0.000001:
tangent.set('x', f"{new_t[0]:.6f}")
tangent.set('y', f"{new_t[1]:.6f}")
tangent.set('z', f"{new_t[2]:.6f}")
tangent_fixes += 1
modified = True
# Fix pose normal offsets
for pose_name, avg_offsets in avg_pose_data.items():
if pose_name not in data['poses']:
continue
# Find the <pose> element
pose_elem = None
for p in root.findall('.//poses/pose'):
if p.get('name') == pose_name:
pose_elem = p
break
if pose_elem is None:
continue
for v in vertices:
key = round_pos(v['pos'], 0.0001)
if key not in avg_offsets:
continue
if v['index'] not in data['poses'][pose_name]:
continue
# Find the poseoffset element for this vertex
for po in pose_elem.findall('poseoffset'):
if int(po.get('index')) == v['index']:
old_nx = float(po.get('nx', 0.0))
old_ny = float(po.get('ny', 0.0))
old_nz = float(po.get('nz', 0.0))
new_n = avg_offsets[key]
diff = math.sqrt(
(old_nx-new_n[0])**2 +
(old_ny-new_n[1])**2 +
(old_nz-new_n[2])**2
)
if diff > 0.000001:
po.set('nx', f"{new_n[0]:.6f}")
po.set('ny', f"{new_n[1]:.6f}")
po.set('nz', f"{new_n[2]:.6f}")
pose_normal_fixes += 1
break
if pose_normal_fixes > 0:
modified = True
if modified:
xml_path = data['xml_path']
tree.write(xml_path, encoding='utf-8', xml_declaration=True)
print(f" {name}: fixed {normal_fixes} normals, {tangent_fixes} tangents, {pose_normal_fixes} pose normals")
modified_count += 1
else:
print(f" {name}: no changes needed")
# Convert back to binary
print(f"\nConverting {modified_count} modified meshes back to binary...")
converted = 0
for xml_path in xml_paths:
mesh_path = xml_to_mesh(xml_path, converter)
if mesh_path:
converted += 1
# Clean up XML file
try:
os.remove(xml_path)
except OSError:
pass
print(f"[OGRE Mesh Seam Fix] Done. {converted}/{len(xml_paths)} meshes converted.")
return converted
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: fix_ogre_mesh_seams.py <mesh_directory> [ogre_xml_converter_path]")
sys.exit(1)
mesh_dir = sys.argv[1]
converter = find_converter()
if not converter:
print("ERROR: OgreXMLConverter not found. Set OGRETOOLS_XML_CONVERTER env var or pass path as 2nd argument.")
sys.exit(1)
if not os.path.isdir(mesh_dir):
print(f"ERROR: {mesh_dir} is not a directory")
sys.exit(1)
fix_seams(mesh_dir, converter)
+217
View File
@@ -0,0 +1,217 @@
# Buoyancy System Analysis
## Problem: Characters are not affected by buoyancy
After analyzing the code in `src/features/editScene`, I've identified several potential issues:
## 1. Character Gravity Factor Issue
**Root Cause**: Characters have gravity factor set to 0.0f by default.
In `CharacterSystem.cpp` line 163:
```cpp
m_physics->setGravityFactor(ch->GetBodyID(), 0.0f);
```
This means characters won't sink into water naturally. The `BuoyancySystem` tries to handle this by setting gravity factor to 1.0f when characters are in water (line 127 in `BuoyancySystem.cpp`), but there may be timing or detection issues.
## 2. Broadphase Query Area Settings
The `broadphaseQuery` function in `physics.cpp` uses these settings:
```cpp
JPH::AABox water_box(-JPH::Vec3(1000, 1000, 1000),
JPH::Vec3(1000, 0.1f, 1000));
water_box.Translate(JPH::Vec3(surface_point));
```
Where `surface_point = position + Ogre::Vector3(0, -0.1f, 0)` (position is the water surface Y level).
**Dimensions**:
- X: -1000 to 1000 (2000 units wide, centered at surface_point.x)
- Y: -1000 to 0.1f (1000.1 units tall, but centered 0.1 units BELOW water surface)
- Z: -1000 to 1000 (2000 units deep, centered at surface_point.z)
**Issue**: The water box extends 1000 units BELOW the surface point, but only 0.1 units ABOVE it. Since `surface_point` is 0.1 units below the actual water surface, the box effectively covers:
- From 1000.1 units below water surface
- To 0.0 units at water surface (not above it)
This means bodies need to be at or below the water surface to be detected.
## 3. Character Detection in Broadphase
Characters are `JPH::Character` objects, not regular dynamic bodies. The broadphase query filters for:
- `BroadPhaseLayers::MOVING` layer
- `Layers::MOVING` object layer
Characters should be in these layers, but there may be issues with how character bodies are registered in the broadphase.
## 4. Debugging Approach
### 4.1 Enable Debug Logging
Modify `BuoyancySystem.cpp` to add debug logging:
```cpp
// In update() method, after broadphaseQuery call:
Ogre::LogManager::getSingleton().logMessage(
"BuoyancySystem: Found " + Ogre::StringConverter::toString(m_bodiesInWater.size()) +
" bodies in water");
// In the loop applying buoyancy:
for (JPH::BodyID bodyID : m_bodiesInWater) {
Ogre::SceneNode *node = m_physics->getSceneNodeFromBodyID(bodyID);
if (node) {
Ogre::LogManager::getSingleton().logMessage(
"Body " + Ogre::StringConverter::toString(bodyID.GetIndex()) +
" at position: " + Ogre::StringConverter::toString(m_physics->getPosition(bodyID)));
}
// Check if it's a character
if (m_physics->bodyIsCharacter(bodyID)) {
Ogre::LogManager::getSingleton().logMessage(
"Body " + Ogre::StringConverter::toString(bodyID.GetIndex()) + " is a character");
}
}
```
### 4.2 Visual Debugging - Draw Water Box
Add debug rendering to visualize the water detection area:
```cpp
// In BuoyancySystem::update(), after broadphaseQuery:
void drawDebugWaterBox(const Ogre::Vector3& waterSurfacePos) {
// Create a manual object to visualize the water box
static Ogre::ManualObject* waterBoxDebug = nullptr;
if (!waterBoxDebug) {
waterBoxDebug = m_sceneManager->createManualObject("WaterBoxDebug");
Ogre::SceneNode* debugNode = m_sceneManager->getRootSceneNode()->createChildSceneNode();
debugNode->attachObject(waterBoxDebug);
}
waterBoxDebug->clear();
waterBoxDebug->begin("BaseWhiteNoLighting", Ogre::RenderOperation::OT_LINE_LIST);
// Water box dimensions (matching broadphaseQuery)
float halfSize = 1000.0f;
float top = waterSurfacePos.y - 0.1f + 0.1f; // surface_point.y + 0.1f
float bottom = waterSurfacePos.y - 0.1f - 1000.0f; // surface_point.y - 1000.0f
// Draw box edges
Ogre::Vector3 corners[8] = {
{waterSurfacePos.x - halfSize, bottom, waterSurfacePos.z - halfSize},
{waterSurfacePos.x + halfSize, bottom, waterSurfacePos.z - halfSize},
{waterSurfacePos.x + halfSize, bottom, waterSurfacePos.z + halfSize},
{waterSurfacePos.x - halfSize, bottom, waterSurfacePos.z + halfSize},
{waterSurfacePos.x - halfSize, top, waterSurfacePos.z - halfSize},
{waterSurfacePos.x + halfSize, top, waterSurfacePos.z - halfSize},
{waterSurfacePos.x + halfSize, top, waterSurfacePos.z + halfSize},
{waterSurfacePos.x - halfSize, top, waterSurfacePos.z + halfSize}
};
// Bottom square
for (int i = 0; i < 4; i++) {
waterBoxDebug->position(corners[i]);
waterBoxDebug->position(corners[(i+1)%4]);
}
// Top square
for (int i = 4; i < 8; i++) {
waterBoxDebug->position(corners[i]);
waterBoxDebug->position(corners[4 + (i-3)%4]);
}
// Vertical edges
for (int i = 0; i < 4; i++) {
waterBoxDebug->position(corners[i]);
waterBoxDebug->position(corners[i+4]);
}
waterBoxDebug->end();
}
```
### 4.3 Check Character Position Relative to Water
Add a debug function to check character positions:
```cpp
void debugCharacterPositions() {
m_world.query<CharacterComponent, TransformComponent>().each(
[&](flecs::entity entity, CharacterComponent &cc, TransformComponent &transform) {
if (transform.node) {
Ogre::Vector3 worldPos = transform.node->_getDerivedPosition();
Ogre::LogManager::getSingleton().logMessage(
"Character entity " + Ogre::StringConverter::toString(entity.id()) +
" at Y: " + Ogre::StringConverter::toString(worldPos.y));
// Check if character has physics body
auto it = m_states.find(entity.id());
if (it != m_states.end() && it->second.character) {
JPH::BodyID bodyID = it->second.character->GetBodyID();
Ogre::Vector3 bodyPos = m_physics->getPosition(bodyID);
Ogre::LogManager::getSingleton().logMessage(
"Character body at Y: " + Ogre::StringConverter::toString(bodyPos.y) +
", gravity factor: " + Ogre::StringConverter::toString(m_physics->getGravityFactor(bodyID)));
}
}
});
}
```
## 5. Recommended Fixes
### 5.1 Adjust Water Box Parameters
The current water box may be too shallow (only 0.1 units at the top). Consider adjusting:
```cpp
// In broadphaseQuery function:
JPH::AABox water_box(-JPH::Vec3(1000, 1.0f, 1000), // Increased from 0.1f to 1.0f
JPH::Vec3(1000, 1000, 1000)); // Symmetrical above/below
```
This creates a 2-unit tall detection area centered on the surface point.
### 5.2 Fix Character Gravity Handling
Modify `BuoyancySystem::update()` to better handle character gravity:
```cpp
// Current issue: characters with gravity factor 0 won't sink into water
// Even if buoyancy is applied, they need gravity to sink first
// Potential fix: Always give characters some minimal gravity when near water
// or modify CharacterSystem to not set gravity factor to 0
```
### 5.3 Verify Character Body Registration
Ensure character bodies are properly registered in the physics system and included in broadphase queries. Check that:
1. Characters are added to the physics system (`ch->AddToPhysicsSystem()`)
2. They are in the `MOVING` broadphase layer
3. Their body IDs are valid for queries
## 6. Testing Procedure
1. **Enable debug logging** as shown above
2. **Place a character in water** (Y position below water surface)
3. **Check console output** for:
- Number of bodies detected in water
- Character body positions
- Gravity factor changes
4. **Use visual debug** to see water box
5. **Adjust water surface Y** in WaterPhysics component to ensure it's above character position
## 7. Water Physics Settings
Default `WaterPhysics` component has:
- `waterSurfaceY = -0.1f` (slightly below origin)
- `defaultBuoyancy = 1.0f` (neutral buoyancy)
- `enabled = true`
Make sure:
1. WaterPhysics entity exists (BuoyancySystem creates one if missing)
2. `waterSurfaceY` is above character positions for testing
3. Water physics is enabled (`enabled = true`)
+172
View File
@@ -0,0 +1,172 @@
# Buoyancy System Analysis and Debugging Guide
## Problem Analysis
After analyzing the buoyancy system in `src/features/editScene`, I identified several key issues why characters are not affected by buoyancy:
### 1. **Broadphase Query Area Not Following Camera**
The original buoyancy system used a fixed position `(0, waterSurfaceY, 0)` for the broadphase query AABox. This meant the water detection area was static at world origin, not following the camera or characters.
**Fix Applied**: Modified `BuoyancySystem::update()` to use camera XZ position with water Y position:
```cpp
Ogre::Vector3 waterSurfacePos(m_cameraPosition.x, waterPhysics->waterSurfaceY, m_cameraPosition.z);
```
### 2. **Character Physics Layer Issue**
Characters are created with `Layers::MOVING` but the broadphase query in `physics.cpp` was checking for bodies in specific layers. The query needs to include the MOVING layer.
**Fix Applied**: Updated `broadphaseQuery` in `physics.cpp` to include `Layers::MOVING`:
```cpp
if (body->GetMotionType() == JPH::EMotionType::Dynamic &&
(body->GetObjectLayer() == Layers::MOVING ||
body->GetObjectLayer() == Layers::NON_MOVING)) {
```
### 3. **Character Gravity Factor Management**
Characters have gravity factor 0 by default (to prevent sinking into terrain). When they enter water, we need to:
1. Save original gravity factor
2. Set gravity factor to 1.0 to allow sinking
3. Restore original gravity when leaving water
**Fix Applied**: Added gravity factor caching in `BuoyancySystem`:
```cpp
// Save original gravity factor if not already saved
if (m_characterOriginalGravity.find(bodyID) == m_characterOriginalGravity.end()) {
m_characterOriginalGravity[bodyID] = m_physics->getGravityFactor(bodyID);
}
// Enable gravity for characters in water so they sink
m_physics->setGravityFactor(bodyID, 1.0f);
```
### 4. **Water AABox Size Configuration**
The broadphase query uses an AABox centered at water surface position with size `(100, 10, 100)`. This may need adjustment based on your scene scale.
## Debugging Approach
### 1. **Enable Debug Logging**
Run the editor with the `--debug-buoyancy` command line option:
```bash
./build/Editor --debug-buoyancy
```
This enables verbose logging every 60 frames (about 1 second at 60 FPS) showing:
- Water physics state (surface Y, enabled, buoyancy)
- Camera position
- Water detection center position
- All characters and their positions
- Bodies detected in water by broadphase query
- Character gravity factor cache
### 2. **Broadphase Query Settings**
The water detection area is configured in `src/features/editScene/physics/physics.cpp`:
```cpp
// AABox for water detection (centered at water surface position)
// Box extends from (-1000, 1.0, -1000) to (1000, 1000, 1000) relative to surface
// Total size: 2000x999x2000 units
JPH::AABox water_box(-JPH::Vec3(1000, 1.0f, 1000),
JPH::Vec3(1000, 1000, 1000));
water_box.Translate(JPH::Vec3(surface_point));
```
**Current Filter Settings**:
- Only checks `Layers::MOVING` bodies (line 1587)
- Uses `BroadPhaseLayers::MOVING` filter (line 1586)
**Adjustment Recommendations**:
1. **Check character layer**: Ensure characters are in `Layers::MOVING`
2. **Adjust box size**: The current 2000x999x2000 box is very large
- Reduce 1000 values for smaller detection area
- Adjust Y values (1.0f and 1000) for vertical detection range
3. **Add NON_MOVING layer**: If characters are in NON_MOVING layer, update filter:
```cpp
JPH::SpecifiedObjectLayerFilter(Layers::MOVING | Layers::NON_MOVING)
```
4. **Box follows camera**: The box is translated to `surface_point` which now uses camera XZ position
### 3. **Water Physics Configuration**
Default water settings (in `EditorApp::createDefaultEntities()`):
- `waterSurfaceY = -0.1f` (just below ground level)
- `defaultBuoyancy = 1.0f` (neutral buoyancy)
- `defaultLinearDrag = 0.1f`
- `defaultAngularDrag = 0.05f`
- `gravity = 9.81f`
**To adjust**: Use the Water Physics editor UI or modify the WaterPhysics component.
### 4. **Character Configuration**
Ensure characters:
1. Have `CharacterComponent` attached
2. Are in the `MOVING` physics layer
3. Have proper collision shapes
4. Are spawned at Y position below water surface for testing
## Testing Procedure
1. **Build the project**:
```bash
cmake --build build --target Editor
```
2. **Run with debug mode**:
```bash
./build/Editor --debug-buoyancy
```
3. **Create test scene**:
- Add water (WaterPhysics entity exists by default)
- Spawn characters (use Character spawner in UI)
- Position characters below water surface (Y < -0.1)
4. **Monitor console output** for debug messages showing:
- "Bodies in water (broadphase): X"
- Character positions and "inWater" status
- Gravity factor changes
5. **Adjust settings as needed**:
- Increase water surface Y if characters are above water
- Adjust AABox size in physics.cpp
- Modify buoyancy/drag coefficients
## Key Code Changes Made
1. **BuoyancySystem.cpp/hpp**:
- Added camera position tracking
- Added gravity factor caching for characters
- Added debug logging system
- Fixed broadphase query position
2. **physics.cpp**:
- Fixed broadphase query to include MOVING layer
- Ensured character bodies are detected
3. **EditorApp.cpp/hpp**:
- Added `--debug-buoyancy` command line option
- Added `setDebugBuoyancy()` method
- Updated camera position to buoyancy system each frame
4. **EditorCamera.hpp**:
- Added `getPosition()` method
5. **main.cpp**:
- Added command line argument parsing for `--debug-buoyancy`
## Expected Behavior After Fixes
1. Characters should sink into water (gravity enabled)
2. Buoyancy forces should push characters upward
3. Debug logs should show bodies detected in water
4. Character gravity should be restored when leaving water
5. Water detection area should follow camera movement
## Troubleshooting
If characters still aren't affected:
1. **Check debug logs**: Ensure bodies are being detected
2. **Verify water surface Y**: Characters must be below this value
3. **Check physics layers**: Characters should be in MOVING layer
4. **Test with simple objects**: Create a simple box to verify buoyancy works
5. **Adjust AABox size**: Increase detection area if characters are far from camera
The system is now properly configured to detect characters in water and apply buoyancy forces with comprehensive debugging capabilities.
-2
View File
@@ -73,8 +73,6 @@ FileSystem=resources/fonts
[LuaScripts]
FileSystem=lua-scripts
#[Characters]
#FileSystem=./characters
[Audio]
FileSystem=./audio/gui
+12 -1
View File
@@ -13,6 +13,17 @@ foreach(DIR_NAME ${DIRECTORY_LIST})
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${DIR_NAME})
list(APPEND TARGET_PATHS ${CMAKE_CURRENT_BINARY_DIR}/${DIR_NAME})
endforeach()
set(COPY_FILES main/flare.png)
set(TARGET_FILES)
foreach(PFILE ${COPY_FILES})
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${PFILE}
COMMAND ${CMAKE_COMMAND} -E copy
${CMAKE_CURRENT_SOURCE_DIR}/${PFILE}
${CMAKE_CURRENT_BINARY_DIR}/${PFILE}
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${PFILE}
)
list(APPEND TARGET_FILES ${CMAKE_CURRENT_BINARY_DIR}/${PFILE})
endforeach()
#add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/terrain/world_map.png
# COMMAND unzip -o ${CMAKE_CURRENT_SOURCE_DIR}/world_map.kra mergedimage.png -d ${CMAKE_CURRENT_BINARY_DIR}/world_map
@@ -31,4 +42,4 @@ add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/terrain/brushes.png
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/brushes.kra)
list(APPEND TARGET_PATHS ${CMAKE_CURRENT_BINARY_DIR}/terrain/brushes.png)
add_custom_target(stage_resources ALL DEPENDS ${TARGET_PATHS})
add_custom_target(stage_resources ALL DEPENDS ${TARGET_PATHS} ${TARGET_FILES})
+24
View File
@@ -69,3 +69,27 @@ material Debug/Red2
}
}
/**
* Debug material for normal visualization overlay.
* Renders on top of everything (depth_check off, depth_write off)
* Uses vertex colours so each normal line can have its own colour.
* Rendered in overlay queue to appear on top of all geometry.
*/
material Debug/NormalOverlay
{
technique
{
pass
{
lighting off
depth_check off
depth_write off
ambient 1.0 1.0 1.0 1.0
diffuse vertexcolour
specular 0.0 0.0 0.0 1.0
cull_software none
cull_hardware none
scene_blend alpha_blend
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

+19
View File
@@ -0,0 +1,19 @@
material Examples/Flare
{
technique
{
pass
{
lighting off
scene_blend add
depth_write off
diffuse vertexcolour
texture_unit
{
texture flare.png
}
}
}
}
+4
View File
@@ -0,0 +1,4 @@
project(features)
add_subdirectory(characters)
add_subdirectory(sceneEditor)
add_subdirectory(editScene)
+33
View File
@@ -0,0 +1,33 @@
project(characters)
find_package(OGRE REQUIRED COMPONENTS Bites Paging Terrain CONFIG)
find_package(ZLIB)
find_package(SDL2)
find_package(assimp REQUIRED CONFIG)
find_package(OgreProcedural REQUIRED CONFIG)
find_package(pugixml REQUIRED CONFIG)
find_package(flecs REQUIRED CONFIG)
find_package(Tracy REQUIRED CONFIG)
add_executable(demo main.cpp TimeEvents.cpp)
target_link_libraries(demo OgreMain OgreBites)
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/resources.cfg"
DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
add_custom_command(TARGET demo POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_BINARY_DIR}/resources"
"${CMAKE_CURRENT_BINARY_DIR}/resources"
COMMAND ${CMAKE_COMMAND} -E copy
"${CMAKE_CURRENT_SOURCE_DIR}/resources/main/jaiqua.mesh"
"${CMAKE_CURRENT_BINARY_DIR}/resources/main/jaiqua.mesh"
COMMAND ${CMAKE_COMMAND} -E copy
"${CMAKE_CURRENT_SOURCE_DIR}/resources/main/jaiqua.skeleton"
"${CMAKE_CURRENT_BINARY_DIR}/resources/main/jaiqua.skeleton"
COMMAND ${CMAKE_COMMAND} -E copy
"${CMAKE_CURRENT_SOURCE_DIR}/resources/main/jaiqua.material"
"${CMAKE_CURRENT_BINARY_DIR}/resources/main/jaiqua.material"
COMMAND ${CMAKE_COMMAND} -E copy
"${CMAKE_CURRENT_SOURCE_DIR}/resources/main/blue_jaiqua.jpg"
"${CMAKE_CURRENT_BINARY_DIR}/resources/main/blue_jaiqua.jpg"
DEPENDS "${CMAKE_BINARY_DIR}/resources"
COMMENT "Copying generated resources from root build dir to local build dir"
)
+207
View File
@@ -0,0 +1,207 @@
//
// TimeEvents.cpp
// OGRE
//
// Created by Chilly Willy on 11/29/25.
//
#include "TimeEvents.h"
#include <algorithm>
void TimeEventDispatcher::addEventList(const TimeEventList * list)
{
if (std::find(mEventLists.begin(), mEventLists.end(), list) == mEventLists.end())
{
mEventLists.push_back(list);
}
}
void TimeEventDispatcher::removeEventList(const TimeEventList * list)
{
auto i = std::find(mEventLists.begin(), mEventLists.end(), list);
if (i != mEventLists.end())
{
mEventLists.erase(i);
}
}
void TimeEventDispatcher::addListener(TimeEventListener * listener)
{
if (std::find(mListeners.begin(), mListeners.end(), listener) == mListeners.end())
{
mListeners.push_back(listener);
}
}
void TimeEventDispatcher::removeListener(TimeEventListener * listener)
{
auto i = std::find(mListeners.begin(), mListeners.end(), listener);
if (i != mListeners.end())
{
mListeners.erase(i);
}
}
/*
lastTime, thisTime, n=loops
lastTime is the thisTime from the last frame and thisTime will be the lastTime in the next frame.
Events at those times should only be dispatched once so we should include one and exclude the other.
If we include thisTime and exclude lastTime, then we need to dispatch events before we start playing,
eg when we call setTimePosition(), and we would need to somehow know whether it should be dispatched
as Forward or Backward.
If we include lastTime and exclude thisTime, then we don't need to do anything before playing but
we need to include the length/0 when we loop or finish, which requires special handling anyway.
Looping Forward:
next lastTime
thisTime < length thisTime lastTime <= t < thisTime
thisTime = length 0 lastTime <= t <= length
thisTime > length thisTime - n * length lastTime <= t <= length ... 0 <= t < thisTime
... 0 <= t <= length
Looping Backward:
next lastTime
thisTime > 0 thisTime lastTime >= t > thisTime
thisTime = 0 0 !!! lastTime >= t >= 0
thisTime < 0 thisTime + n * length lastTime >= t >= 0 ... length >= t > thisTime
... length >= t >= 0
Not-Looping Forward:
next lastTime
thisTime < length thisTime lastTime <= t < thisTime
thisTime = length length lastTime <= t <= length
thisTime > length length lastTime <= t <= length
Not-Looping Backward:
next lastTime
thisTime > 0 thisTime lastTime >= t > thisTime
thisTime = 0 0 lastTime >= t >= 0
thisTime < 0 0 lastTime >= t >= 0
*/
void TimeEventDispatcher::dispatch(float lastTime, float thisTime, int loops, float length)
{
if ((loops > 0) || (thisTime > lastTime))
{
while (loops--)
{
dispatchForwardInclusive(lastTime, length);
lastTime = 0.0f;
}
if (thisTime >= length)
{
dispatchForwardInclusive(lastTime, length);
}
else
{
dispatchForwardExclusive(lastTime, thisTime);
}
}
else if ((loops < 0) || (thisTime < lastTime))
{
if (lastTime == 0.0f)
{
/* length mod length = 0 mod length
Either we've already been looping backward and the last frame just happened
to stop on 0, in which case we already triggered its events, or we are just
starting to to loop backward, in which case we should not trigger events at
0 until we do a full backward run through the animation. If we haven't been
looping then we would not get in here because thisTime == lastTime == 0.0f.
For the same reason we also know loops < 0.
*/
lastTime = length;
++loops;
}
while (loops++)
{
dispatchBackwardInclusive(lastTime, 0.0f);
lastTime = length;
}
if (thisTime <= 0.0f)
{
dispatchBackwardInclusive(lastTime, 0.0f);
}
else
{
dispatchBackwardExclusive(lastTime, thisTime);
}
}
else
{
// Nothing doing
}
}
void TimeEventDispatcher::dispatchForwardInclusive(float lastTime, float thisTime)
{
for (const TimeEventList * events : mEventLists)
{
for (auto ie = events->begin(); ie != events->end(); ++ie)
{
if (ie->first >= lastTime && ie->first <= thisTime)
{
dispatchEvent(ie->second, TED_FORWARD);
}
}
}
}
void TimeEventDispatcher::dispatchForwardExclusive(float lastTime, float thisTime)
{
for (const TimeEventList * events : mEventLists)
{
for (auto ie = events->begin(); ie != events->end(); ++ie)
{
if (ie->first >= lastTime && ie->first < thisTime)
{
dispatchEvent(ie->second, TED_FORWARD);
}
}
}
}
void TimeEventDispatcher::dispatchBackwardInclusive(float lastTime, float thisTime)
{
for (const TimeEventList * events : mEventLists)
{
for (auto ie = events->rbegin(); ie != events->rend(); ++ie)
{
if (ie->first <= lastTime && ie->first >= thisTime)
{
dispatchEvent(ie->second, TED_BACKWARD);
}
}
}
}
void TimeEventDispatcher::dispatchBackwardExclusive(float lastTime, float thisTime)
{
for (const TimeEventList * events : mEventLists)
{
for (auto ie = events->rbegin(); ie != events->rend(); ++ie)
{
if (ie->first <= lastTime && ie->first > thisTime)
{
dispatchEvent(ie->second, TED_BACKWARD);
}
}
}
}
void TimeEventDispatcher::dispatchEvent(const std::string & name, TimeEventDirection direction)
{
for (TimeEventListener * listener : mListeners)
{
listener->eventOccurred(name, direction);
}
}
+58
View File
@@ -0,0 +1,58 @@
//
// TimeEvents.h
// OGRE
//
// Created by Chilly Willy on 11/29/25.
//
#ifndef INCLUDE_OGRE_TIME_EVENTS_H
#define INCLUDE_OGRE_TIME_EVENTS_H
#include <map>
#include <vector>
#include <string>
typedef std::multimap<float, std::string> TimeEventList;
enum TimeEventDirection
{
TED_FORWARD,
TED_BACKWARD,
};
class TimeEventListener
{
public:
virtual void eventOccurred(const std::string & name, TimeEventDirection direction) {}
};
class TimeEventDispatcher
{
public:
void addEventList(const TimeEventList * list);
void removeEventList(const TimeEventList * list);
void addListener(TimeEventListener * listener);
void removeListener(TimeEventListener * listener);
void dispatch(float lastTime, float thisTime, int loops, float length);
private:
void dispatchForwardInclusive(float lastTime, float thisTime);
void dispatchForwardExclusive(float lastTime, float thisTime);
void dispatchBackwardInclusive(float lastTime, float thisTime);
void dispatchBackwardExclusive(float lastTime, float thisTime);
void dispatchEvent(const std::string & name, TimeEventDirection direction);
std::vector<const TimeEventList *> mEventLists;
std::vector<TimeEventListener *> mListeners;
};
#endif /* INCLUDE_OGRE_TIME_EVENTS_H */
File diff suppressed because it is too large Load Diff
+82
View File
@@ -0,0 +1,82 @@
# Ogre Core Resources
[OgreInternal]
#FileSystem=./Media/Main
FileSystem=resources/main
FileSystem=resources/shaderlib
#FileSystem=./Media/RTShaderLib
FileSystem=resources/terrain
# Resources required by OgreBites::Trays
[Essential]
#Zip=./Media/packs/SdkTrays.zip
#Zip=./Media/packs/profiler.zip
## this line will end up in the [Essential] group
#FileSystem=./Media/thumbnails
# Common sample resources needed by many of the samples.
# Rarely used resources should be separately loaded by the
# samples which require them.
[General]
FileSystem=skybox
FileSystem=resources/buildings
FileSystem=resources/buildings/parts/pier
FileSystem=resources/buildings/parts/furniture
FileSystem=resources/vehicles
FileSystem=resources/debug
FileSystem=resources/fonts
# PBR media must come before the scripts that reference it
#FileSystem=./Media/PBR
#FileSystem=./Media/PBR/filament
#FileSystem=./Media/materials/programs/GLSL
#FileSystem=./Media/materials/programs/GLSL120
#FileSystem=./Media/materials/programs/GLSL150
#FileSystem=./Media/materials/programs/GLSL400
#FileSystem=./Media/materials/programs/GLSLES
#FileSystem=./Media/materials/programs/SPIRV
#FileSystem=./Media/materials/programs/Cg
#FileSystem=./Media/materials/programs/HLSL
#FileSystem=./Media/materials/programs/HLSL_Cg
#FileSystem=./Media/materials/scripts
#FileSystem=./Media/materials/textures
#FileSystem=./Media/materials/textures/terrain
#FileSystem=./Media/models
#FileSystem=./Media/particle
#FileSystem=./Media/DeferredShadingMedia
#FileSystem=./Media/DeferredShadingMedia/DeferredShading/post
#FileSystem=./Media/PCZAppMedia
#FileSystem=./Media/materials/scripts/SSAO
#FileSystem=./Media/materials/textures/SSAO
#FileSystem=./Media/volumeTerrain
#FileSystem=./Media/CSMShadows
#Zip=./Media/packs/cubemap.zip
#Zip=./Media/packs/cubemapsJS.zip
#Zip=./Media/packs/dragon.zip
#Zip=./Media/packs/fresneldemo.zip
#Zip=./Media/packs/ogredance.zip
#Zip=./Media/packs/Sinbad.zip
#Zip=./Media/packs/skybox.zip
#Zip=./Media/volumeTerrain/volumeTerrainBig.zip
#Zip=./Media/packs/DamagedHelmet.zip
#Zip=./Media/packs/filament_shaders.zip
#[BSPWorld]
#Zip=./Media/packs/oa_rpg3dm2.pk3
#Zip=./Media/packs/ogretestmap.zip
# Materials for visual tests
#[Tests]
#FileSystem=/media/slapin/library/ogre/ogre-sdk/Tests/Media
[LuaScripts]
FileSystem=lua-scripts
#[Characters]
#FileSystem=./characters
[Audio]
FileSystem=./audio/gui
[Water]
FileSystem=water
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

@@ -0,0 +1,24 @@
material jaiqua
{
technique
{
pass
{
texture_unit
{
texture blue_jaiqua.jpg
tex_address_mode clamp
}
rtshader_system
{
// In case the system uses the RTSS, the following line will ensure
// that hardware animation is used.
// Alternatively, you can derive this information programatically via
// HardwareSkinningFactory::prepareEntityForSkinning
hardware_skinning 38 2 dual_quaternion true false
}
}
}
}
Binary file not shown.
+754
View File
@@ -0,0 +1,754 @@
project(editScene)
set(CMAKE_CXX_STANDARD 17)
find_package(OGRE REQUIRED COMPONENTS Bites Overlay MeshLodGenerator CONFIG)
find_package(flecs REQUIRED CONFIG)
find_package(nlohmann_json REQUIRED)
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
systems/StaticGeometrySystem.cpp
systems/ProceduralTextureSystem.cpp
systems/ProceduralMaterialSystem.cpp
systems/ProceduralMeshSystem.cpp
systems/CellGridSystem.cpp
systems/NormalDebugSystem.cpp
systems/RoomLayoutSystem.cpp
systems/FurnitureLibrary.cpp
systems/StartupMenuSystem.cpp
systems/PauseMenuSystem.cpp
systems/ItemRegistry.cpp
systems/ContainerStateRegistry.cpp
systems/ItemStateRegistry.cpp
systems/SaveLoadSystem.cpp
systems/SaveLoadDialog.cpp
systems/PlayerControllerSystem.cpp
systems/CharacterSlotSystem.cpp
systems/OgreEntityHack.cpp
systems/CharacterRegistry.cpp
systems/MarkovNameGenerator.cpp
systems/PregnancySystem.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
ui/RigidBodyEditor.cpp
ui/LightEditor.cpp
ui/CameraEditor.cpp
ui/LodEditor.cpp
ui/LodSettingsEditor.cpp
ui/StaticGeometryEditor.cpp
ui/StaticGeometryMemberEditor.cpp
ui/ProceduralTextureEditor.cpp
ui/ProceduralMaterialEditor.cpp
ui/PrimitiveEditor.cpp
ui/TriangleBufferEditor.cpp
ui/CharacterSlotsEditor.cpp
ui/CharacterIdentityEditor.cpp
ui/AnimationTreeEditor.cpp
ui/AnimationTreeTemplateEditor.cpp
ui/CharacterEditor.cpp
ui/CellGridEditor.cpp
ui/LotEditor.cpp
ui/DistrictEditor.cpp
ui/TownEditor.cpp
ui/RoofEditor.cpp
ui/RoomEditor.cpp
ui/ClearAreaEditor.cpp
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
components/StaticGeometryModule.cpp
components/ProceduralTextureModule.cpp
components/ProceduralMaterialModule.cpp
components/PrimitiveModule.cpp
components/TriangleBufferModule.cpp
components/CharacterSlotsModule.cpp
components/AnimationTreeModule.cpp
components/AnimationTreeTemplateModule.cpp
components/AnimationTree.cpp
components/CharacterModule.cpp
components/CellGridModule.cpp
components/CellGridEditorsModule.cpp
components/CellGrid.cpp
components/StartupMenuModule.cpp
components/PlayerControllerModule.cpp
systems/DialogueSystem.cpp
lua/LuaDialogueApi.cpp
lua/LuaItemApi.cpp
components/Formula.cpp
components/CharacterClassDatabase.cpp
systems/CharacterClassSystem.cpp
ui/CharacterClassDatabaseEditor.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
lua/LuaCharacterClassApi.cpp
lua/LuaCharacterApi.cpp
lua/LuaSaveLoadApi.cpp
)
set(EDITSCENE_HEADERS
EditorApp.hpp
components/Transform.hpp
components/Renderable.hpp
components/EntityName.hpp
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
components/LodSettings.hpp
components/StaticGeometry.hpp
components/StaticGeometryMember.hpp
components/ProceduralTexture.hpp
components/ProceduralMaterial.hpp
components/Primitive.hpp
components/TriangleBuffer.hpp
components/CharacterSlots.hpp
components/CharacterIdentity.hpp
components/RuntimeMarker.hpp
components/AnimationTree.hpp
components/Character.hpp
components/CellGrid.hpp
components/StartupMenu.hpp
components/PlayerController.hpp
systems/DialogueSystem.hpp
lua/LuaDialogueApi.hpp
lua/LuaItemApi.hpp
components/Formula.hpp
components/CharacterClassDatabase.hpp
ui/CharacterClassDatabaseEditor.hpp
systems/CharacterClassSystem.hpp
systems/StartupMenuSystem.hpp
systems/PauseMenuSystem.hpp
systems/ItemRegistry.hpp
systems/ContainerStateRegistry.hpp
systems/ItemStateRegistry.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/CharacterRegistry.hpp
systems/PregnancySystem.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/SaveLoadSystem.hpp
systems/SaveLoadDialog.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
ui/ComponentEditor.hpp
ui/ComponentRegistry.hpp
ui/ComponentRegistration.hpp
ui/TransformEditor.hpp
ui/RenderableEditor.hpp
ui/PhysicsColliderEditor.hpp
ui/RigidBodyEditor.hpp
ui/LightEditor.hpp
ui/CameraEditor.hpp
ui/LodEditor.hpp
ui/LodSettingsEditor.hpp
ui/StaticGeometryEditor.hpp
ui/StaticGeometryMemberEditor.hpp
ui/ProceduralTextureEditor.hpp
ui/ProceduralMaterialEditor.hpp
ui/PrimitiveEditor.hpp
ui/TriangleBufferEditor.hpp
ui/CharacterSlotsEditor.hpp
ui/CharacterIdentityEditor.hpp
ui/AnimationTreeEditor.hpp
ui/AnimationTreeTemplateEditor.hpp
ui/CharacterEditor.hpp
ui/CellGridEditor.hpp
ui/LotEditor.hpp
ui/DistrictEditor.hpp
ui/TownEditor.hpp
ui/RoofEditor.hpp
ui/RoomEditor.hpp
ui/ClearAreaEditor.hpp
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
lua/LuaCharacterClassApi.hpp
lua/LuaCharacterApi.hpp
lua/LuaSaveLoadApi.hpp
)
add_executable(editSceneEditor ${EDITSCENE_SOURCES} ${EDITSCENE_HEADERS})
add_dependencies(editSceneEditor morph)
# Define JPH_DEBUG_RENDERER for physics debug drawing
target_compile_definitions(editSceneEditor PRIVATE JPH_DEBUG_RENDERER)
target_link_libraries(editSceneEditor
OgreMain
OgreBites
OgreOverlay
OgreMeshLodGenerator
flecs::flecs_static
nlohmann_json::nlohmann_json
Jolt::Jolt
OgreProcedural::OgreProcedural
RecastNavigation::Recast
RecastNavigation::Detour
RecastNavigation::DetourTileCache
RecastNavigation::DetourCrowd
RecastNavigation::DebugUtils
PackageArchive
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
)
# Test: Item / Inventory / Container Lua API
add_executable(item_lua_test
tests/item_lua_test.cpp
tests/lua_test_stubs.cpp
)
target_link_libraries(item_lua_test
lua
)
target_include_directories(item_lua_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tests
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
)
# Test: Character Lua API
add_executable(character_lua_test
tests/character_lua_test.cpp
tests/lua_test_stubs.cpp
)
target_link_libraries(character_lua_test
lua
)
target_include_directories(character_lua_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tests
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
)
# Test: Dialogue Lua API
add_executable(dialogue_lua_test
tests/dialogue_lua_test.cpp
tests/lua_test_stubs.cpp
)
target_link_libraries(dialogue_lua_test
lua
)
target_include_directories(dialogue_lua_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tests
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
)
# Test: Character Class Lua API
add_executable(character_class_lua_test
tests/character_class_lua_test.cpp
tests/lua_test_stubs.cpp
)
target_link_libraries(character_class_lua_test
lua
)
target_include_directories(character_class_lua_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tests
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
)
# Test: Save/Load Lua API
add_executable(save_load_lua_test
tests/save_load_lua_test.cpp
tests/lua_test_stubs.cpp
)
target_link_libraries(save_load_lua_test
lua
)
target_include_directories(save_load_lua_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tests
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
)
# ---------------------------------------------------------------------------
# Package Archive Library
# ---------------------------------------------------------------------------
# This implements an Ogre::Archive for .package files (uncompressed indexed
# file containers, similar to Unity packages). It can be used from resources.cfg
# with "Package=path/to/file.package".
add_library(PackageArchive STATIC
package/OgrePackageArchive.cpp
package/OgrePackageArchive.h
)
target_include_directories(PackageArchive PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(PackageArchive PUBLIC
OgreMain
)
# ---------------------------------------------------------------------------
# PackageTool - command-line tool for creating/managing .package archives
# ---------------------------------------------------------------------------
add_executable(PackageTool
package/PackageTool.cpp
package/OgrePackageArchive.cpp
package/OgrePackageArchive.h
)
target_link_libraries(PackageTool
PackageArchive
OgreMain
)
target_include_directories(PackageTool PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
# ---------------------------------------------------------------------------
# lua-scripts.package - package all Lua scripts into a single archive
# ---------------------------------------------------------------------------
# Creates lua-scripts.package from the staged lua-scripts build directory.
# Files are stored with paths relative to lua-scripts/ (the "lua-scripts"
# directory prefix is stripped from archive paths).
#
# The staged directory is assembled from two sources:
# 1. Main lua-scripts (from ${CMAKE_BINARY_DIR}/lua-scripts, staged by
# the stage_lua_scripts target in the main build)
# 2. EditScene-specific lua-scripts (from
# ${CMAKE_CURRENT_SOURCE_DIR}/lua-scripts, overlaid on top)
# The overlay ensures editScene files take precedence over main files
# with the same name.
set(LUA_SCRIPTS_PACKAGE "${CMAKE_CURRENT_BINARY_DIR}/lua-scripts.package")
set(LUA_SCRIPTS_STAGED_DIR "${CMAKE_CURRENT_BINARY_DIR}/lua-scripts")
set(LUA_SCRIPTS_EDITSCENE_SRC "${CMAKE_CURRENT_SOURCE_DIR}/lua-scripts")
# Collect all source files from the main lua-scripts source directory so
# that the package is rebuilt when any of them change.
# CONFIGURE_DEPENDS makes CMake re-evaluate the glob at build time,
# so newly added files are picked up without re-running cmake.
file(GLOB_RECURSE LUA_SCRIPTS_MAIN_FILES
CONFIGURE_DEPENDS
"${CMAKE_SOURCE_DIR}/lua-scripts/*.lua"
"${CMAKE_SOURCE_DIR}/lua-scripts/*.ink"
)
# Collect all editScene-specific lua-scripts source files.
file(GLOB_RECURSE LUA_SCRIPTS_EDITSCENE_FILES
CONFIGURE_DEPENDS
"${LUA_SCRIPTS_EDITSCENE_SRC}/*.lua"
"${LUA_SCRIPTS_EDITSCENE_SRC}/*.ink"
)
# Custom command to overlay editScene-specific lua-scripts on top of the
# staged main lua-scripts directory. This copies files from the editScene
# lua-scripts source directory into the staged directory, overlaying on
# top of any files already there from the main build.
set(LUA_SCRIPTS_OVERLAY_DONE "${CMAKE_CURRENT_BINARY_DIR}/.lua_scripts_overlay_done")
add_custom_command(
OUTPUT "${LUA_SCRIPTS_OVERLAY_DONE}"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${LUA_SCRIPTS_EDITSCENE_SRC}"
"${LUA_SCRIPTS_STAGED_DIR}"
COMMAND ${CMAKE_COMMAND} -E touch "${LUA_SCRIPTS_OVERLAY_DONE}"
DEPENDS stage_lua_scripts
${LUA_SCRIPTS_EDITSCENE_FILES}
COMMENT "Overlaying editScene lua-scripts on ${LUA_SCRIPTS_STAGED_DIR}"
)
add_custom_command(
OUTPUT "${LUA_SCRIPTS_PACKAGE}"
COMMAND $<TARGET_FILE:PackageTool>
create "${LUA_SCRIPTS_PACKAGE}"
--exclude "CMakeFiles"
--exclude "cmake_install.cmake"
--exclude "Makefile"
--exclude ".lua_scripts_overlay_done"
--exclude "*.lua-"
"${LUA_SCRIPTS_STAGED_DIR}"
DEPENDS PackageTool
"${LUA_SCRIPTS_OVERLAY_DONE}"
${LUA_SCRIPTS_MAIN_FILES}
COMMENT "Creating lua-scripts.package from ${LUA_SCRIPTS_STAGED_DIR}"
)
add_custom_target(lua_scripts_package ALL
DEPENDS "${LUA_SCRIPTS_PACKAGE}"
)
# ---------------------------------------------------------------------------
# Package Archive Test
# ---------------------------------------------------------------------------
add_executable(PackageArchiveTest
tests/package_archive_test.cpp
package/OgrePackageArchive.cpp
package/OgrePackageArchive.h
)
target_link_libraries(PackageArchiveTest
PackageArchive
OgreMain
)
target_include_directories(PackageArchiveTest PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/package
)
# Copy local resources (materials, etc.)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/resources")
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/resources"
DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
endif()
# Copy resources from main build
add_custom_command(TARGET editSceneEditor POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_BINARY_DIR}/resources"
"${CMAKE_CURRENT_BINARY_DIR}/resources"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_BINARY_DIR}/characters"
"${CMAKE_CURRENT_BINARY_DIR}/characters"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_BINARY_DIR}/lua-scripts"
"${CMAKE_CURRENT_BINARY_DIR}/lua-scripts"
# Overlay editScene-specific lua-scripts on top of main lua-scripts
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${LUA_SCRIPTS_EDITSCENE_SRC}"
"${CMAKE_CURRENT_BINARY_DIR}/lua-scripts"
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"
)
File diff suppressed because it is too large Load Diff
+293
View File
@@ -0,0 +1,293 @@
#ifndef EDITSCENE_EDITORAPP_HPP
#define EDITSCENE_EDITORAPP_HPP
#pragma once
#include <Ogre.h>
#include <OgreApplicationContext.h>
#include <OgreInput.h>
#include <OgreOverlaySystem.h>
#include <OgreImGuiOverlay.h>
#include <OgreRenderTargetListener.h>
#include <flecs.h>
#include <memory>
#include "lua/LuaState.hpp"
// Forward declarations
class EditorUISystem;
class EditorCamera;
class EditorPhysicsSystem;
class EditorLightSystem;
class EditorCameraSystem;
class EditorLodSystem;
class StaticGeometrySystem;
class ProceduralTextureSystem;
class ProceduralMaterialSystem;
class ProceduralMeshSystem;
class CharacterSlotSystem;
class AnimationTreeSystem;
class BehaviorTreeSystem;
class NavMeshSystem;
class CharacterSystem;
class CellGridSystem;
class RoomLayoutSystem;
class StartupMenuSystem;
class PauseMenuSystem;
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 CharacterClassSystem;
class PregnancySystem;
class EditorApp;
/**
* Shared input state for game mode
*/
struct GameInputState {
bool w = false;
bool a = false;
bool s = false;
bool d = false;
bool shift = false;
bool e = false;
bool f = false;
bool i = false;
bool ePressed = false;
bool fPressed = false;
bool iPressed = false;
float mouseDeltaX = 0.0f;
float mouseDeltaY = 0.0f;
bool mouseMoved = false;
void resetPerFrame()
{
mouseMoved = false;
mouseDeltaX = 0.0f;
mouseDeltaY = 0.0f;
ePressed = false;
fPressed = false;
iPressed = false;
}
};
/**
* RenderTargetListener for ImGui frame management
* Handles NewFrame() in preViewportUpdate and EndFrame() in postViewportUpdate
*/
class ImGuiRenderListener : public Ogre::RenderTargetListener {
public:
ImGuiRenderListener(Ogre::ImGuiOverlay *imguiOverlay,
EditorUISystem *uiSystem,
Ogre::RenderWindow *renderWindow,
EditorApp *editorApp);
void
preViewportUpdate(const Ogre::RenderTargetViewportEvent &evt) override;
void
postViewportUpdate(const Ogre::RenderTargetViewportEvent &evt) override;
private:
Ogre::ImGuiOverlay *m_imguiOverlay;
EditorUISystem *m_uiSystem;
Ogre::RenderWindow *m_renderWindow;
EditorApp *m_editorApp;
// Timer for delta time calculation
Ogre::Timer m_timer;
unsigned long m_lastTime = 0;
float m_deltaTime = 0.0f;
// Frame stats (updated in postViewportUpdate)
int m_lastBatchCount = 0;
};
/**
* Main application class for the scene editor / game
*/
class EditorApp : public OgreBites::ApplicationContext,
public OgreBites::InputListener {
public:
enum class GameMode { Editor, Game };
enum class GamePlayState { Menu, Playing, Paused };
EditorApp();
virtual ~EditorApp();
// OgreBites::ApplicationContext overrides
void setup() override;
bool frameRenderingQueued(const Ogre::FrameEvent &evt) override;
void locateResources() override;
// OgreBites::InputListener overrides
bool mouseMoved(const OgreBites::MouseMotionEvent &evt) override;
bool mousePressed(const OgreBites::MouseButtonEvent &evt) override;
bool mouseReleased(const OgreBites::MouseButtonEvent &evt) override;
bool keyPressed(const OgreBites::KeyboardEvent &evt) override;
bool keyReleased(const OgreBites::KeyboardEvent &evt) override;
// Scene setup
void setupLights();
void createGrid();
void createAxes();
// ECS setup
void setupECS();
void createDefaultEntities();
// Game mode management
void setGameMode(GameMode mode);
GameMode getGameMode() const
{
return m_gameMode;
}
// Debug buoyancy
void setDebugBuoyancy(bool enabled);
bool getDebugBuoyancy() const
{
return m_debugBuoyancy;
}
GamePlayState getGamePlayState() const
{
return m_gamePlayState;
}
void setGamePlayState(GamePlayState state);
void startNewGame(const Ogre::String &scenePath);
void clearScene();
// Save / Load
void saveGame(const std::string &slotPath,
const std::string &slotName);
void loadGame(const std::string &slotPath);
// Input access
GameInputState &getGameInputState()
{
return m_gameInput;
}
// Getters
flecs::entity getSelectedEntity() const;
Ogre::SceneManager *getSceneManager() const
{
return m_sceneMgr;
}
flecs::world *getWorld()
{
return &m_world;
}
EditorCamera *getEditorCamera() const
{
return m_camera.get();
}
AnimationTreeSystem *getAnimationTreeSystem() const
{
return m_animationTreeSystem.get();
}
CharacterSlotSystem *getCharacterSlotSystem() const
{
return m_characterSlotSystem.get();
}
StartupMenuSystem *getStartupMenuSystem() const
{
return m_startupMenuSystem.get();
}
PauseMenuSystem *getPauseMenuSystem() const;
DialogueSystem *getDialogueSystem() const;
ActuatorSystem *getActuatorSystem() const
{
return m_actuatorSystem.get();
}
EventHandlerSystem *getEventHandlerSystem() const
{
return m_eventHandlerSystem.get();
}
CharacterClassSystem *getCharacterClassSystem() const
{
return m_characterClassSystem.get();
}
PregnancySystem *getPregnancySystem() const
{
return m_pregnancySystem.get();
}
Ogre::ImGuiOverlay *getImGuiOverlay() const
{
return m_imguiOverlay;
}
private:
// Ogre objects
Ogre::SceneManager *m_sceneMgr;
Ogre::OverlaySystem *m_overlaySystem;
Ogre::ImGuiOverlay *m_imguiOverlay;
// ECS
flecs::world m_world;
std::vector<flecs::entity> m_defaultEntities;
// Editor systems
std::unique_ptr<EditorUISystem> m_uiSystem;
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;
std::unique_ptr<StaticGeometrySystem> m_staticGeometrySystem;
std::unique_ptr<ProceduralTextureSystem> m_proceduralTextureSystem;
std::unique_ptr<ProceduralMaterialSystem> m_proceduralMaterialSystem;
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;
std::unique_ptr<CharacterClassSystem> m_characterClassSystem;
std::unique_ptr<PregnancySystem> m_pregnancySystem;
// Game systems
std::unique_ptr<StartupMenuSystem> m_startupMenuSystem;
std::unique_ptr<PlayerControllerSystem> m_playerControllerSystem;
// State
uint16_t m_currentModifiers;
GameMode m_gameMode = GameMode::Editor;
GamePlayState m_gamePlayState = GamePlayState::Menu;
GameInputState m_gameInput;
bool m_setupComplete = false;
bool m_debugBuoyancy = false;
float m_playTime = 0.0f;
std::string m_currentBaseScene;
// Lua scripting
editScene::LuaState m_lua;
// Editor visualization nodes
Ogre::SceneNode *m_gridNode = nullptr;
Ogre::SceneNode *m_axisNode = nullptr;
};
#endif // EDITSCENE_EDITORAPP_HPP
+37
View File
@@ -0,0 +1,37 @@
#include "GameMode.hpp"
namespace editScene
{
namespace
{
/// Global game mode state.
GameMode s_gameMode = GameMode::Editor;
/// Global gameplay state (only meaningful in game mode).
GamePlayState s_gamePlayState = GamePlayState::Menu;
} // anonymous namespace
void setEditSceneGameMode(GameMode mode) noexcept
{
s_gameMode = mode;
}
void setEditSceneGamePlayState(GamePlayState state) noexcept
{
s_gamePlayState = state;
}
GameMode getGameMode() noexcept
{
return s_gameMode;
}
GamePlayState getGamePlayState() noexcept
{
return s_gamePlayState;
}
} // namespace editScene
+101
View File
@@ -0,0 +1,101 @@
#ifndef EDITSCENE_GAMEMODE_HPP
#define EDITSCENE_GAMEMODE_HPP
#pragma once
/**
* @file GameMode.hpp
*
* Global game mode query functions for the editScene feature.
*
* These functions allow any code in the editScene feature to query
* whether the application is currently in editor mode or game mode,
* and what the current gameplay state is, without needing a direct
* pointer to EditorApp.
*
* The EditorApp sets the current mode via setEditSceneGameMode()
* during its lifetime. Code outside the editScene feature should
* continue to use EditorApp::getGameMode() / getGamePlayState()
* directly.
*/
namespace editScene
{
/**
* Application mode: editor or game.
*/
enum class GameMode { Editor, Game };
/**
* Play state when in game mode.
*/
enum class GamePlayState { Menu, Playing, Paused };
// ---------------------------------------------------------------------------
// Global state management (called by EditorApp)
// ---------------------------------------------------------------------------
/**
* Set the current game mode. Called by EditorApp on mode changes.
*/
void setEditSceneGameMode(GameMode mode) noexcept;
/**
* Set the current gameplay state. Called by EditorApp on state changes.
*/
void setEditSceneGamePlayState(GamePlayState state) noexcept;
// ---------------------------------------------------------------------------
// Query functions
// ---------------------------------------------------------------------------
/**
* Return the current application mode.
*/
GameMode getGameMode() noexcept;
/**
* Return the current gameplay state (only meaningful in game mode).
*/
GamePlayState getGamePlayState() noexcept;
// ---------------------------------------------------------------------------
// Predicates
// ---------------------------------------------------------------------------
/** True when the application is in editor mode. */
inline bool isEditorMode() noexcept
{
return getGameMode() == GameMode::Editor;
}
/** True when the application is in game mode (any play state). */
inline bool isGameMode() noexcept
{
return getGameMode() == GameMode::Game;
}
/** True when in game mode and the gameplay state is Playing. */
inline bool isGamePlaying() noexcept
{
return getGameMode() == GameMode::Game &&
getGamePlayState() == GamePlayState::Playing;
}
/** True when in game mode and the gameplay state is Menu. */
inline bool isGameMenu() noexcept
{
return getGameMode() == GameMode::Game &&
getGamePlayState() == GamePlayState::Menu;
}
/** True when in game mode and the gameplay state is Paused. */
inline bool isGamePaused() noexcept
{
return getGameMode() == GameMode::Game &&
getGamePlayState() == GamePlayState::Paused;
}
} // namespace editScene
#endif // EDITSCENE_GAMEMODE_HPP
@@ -0,0 +1,237 @@
#include "EditorCamera.hpp"
#include <OgreViewport.h>
EditorCamera::EditorCamera(Ogre::SceneManager *sceneMgr,
Ogre::RenderWindow *window)
: m_sceneMgr(sceneMgr)
, m_camera(nullptr)
, m_cameraNode(nullptr)
, m_targetNode(nullptr)
, m_position(0, 5, 15)
, m_target(0, 0, 0)
, m_distance(15.0f)
, m_yaw(0.0f)
, m_pitch(-20.0f)
, m_rotating(false)
, m_panning(false)
, m_fpsMode(false)
, m_lastMouseX(0)
, m_lastMouseY(0)
, m_keyW(false)
, m_keyS(false)
, m_keyA(false)
, m_keyD(false)
, m_keyQ(false)
, m_keyE(false)
{
// Create camera
m_camera = sceneMgr->createCamera("EditorCamera");
m_camera->setNearClipDistance(0.1f);
m_camera->setFarClipDistance(1000.0f);
m_camera->setAutoAspectRatio(true);
// Create camera node
m_cameraNode = sceneMgr->getRootSceneNode()->createChildSceneNode(
"EditorCameraNode");
m_cameraNode->attachObject(m_camera);
// Create target node
m_targetNode = sceneMgr->getRootSceneNode()->createChildSceneNode(
"EditorCameraTarget");
// Setup viewport
Ogre::Viewport *vp = window->addViewport(m_camera);
vp->setBackgroundColour(Ogre::ColourValue(0.1f, 0.1f, 0.1f));
// Setup CameraMan for proper camera control (no roll)
m_cameraMan = std::make_unique<OgreBites::CameraMan>(m_cameraNode);
m_cameraMan->setStyle(OgreBites::CS_ORBIT);
m_cameraMan->setTarget(m_targetNode);
m_cameraMan->setYawPitchDist(Ogre::Degree(m_yaw), Ogre::Degree(m_pitch),
m_distance);
updateCameraPosition();
}
EditorCamera::~EditorCamera() = default;
void EditorCamera::update(float deltaTime)
{
if (m_fpsMode) {
updateFPSMovement(deltaTime);
}
// CameraMan handles the positioning
m_cameraMan->setYawPitchDist(Ogre::Degree(m_yaw), Ogre::Degree(m_pitch),
m_distance);
m_cameraMan->frameRendered(Ogre::FrameEvent{ deltaTime });
}
void EditorCamera::updateFPSMovement(float deltaTime)
{
// Get camera's forward and right vectors from CameraMan's derived orientation
Ogre::Quaternion orientation = m_camera->getDerivedOrientation();
Ogre::Vector3 forward = orientation * Ogre::Vector3::UNIT_Z;
Ogre::Vector3 right = orientation * Ogre::Vector3::UNIT_X;
Ogre::Vector3 up = Ogre::Vector3::UNIT_Y; // World up for Q/E
// Flatten forward vector to horizontal plane for WSAD movement
Ogre::Vector3 forwardHorizontal =
Ogre::Vector3(forward.x, 0, forward.z);
if (forwardHorizontal.squaredLength() > 0.0001f) {
forwardHorizontal.normalise();
}
Ogre::Vector3 movement = Ogre::Vector3::ZERO;
// WSAD movement (horizontal plane)
if (m_keyW)
movement -= forwardHorizontal;
if (m_keyS)
movement += forwardHorizontal;
if (m_keyA)
movement -= right;
if (m_keyD)
movement += right;
// Q/E for vertical movement
if (m_keyQ)
movement -= up;
if (m_keyE)
movement += up;
// Apply movement
if (movement.squaredLength() > 0.0001f) {
movement.normalise();
m_target += movement * FPS_SPEED * deltaTime;
m_targetNode->setPosition(m_target);
m_cameraMan->setTarget(m_targetNode);
}
}
void EditorCamera::handleMouseMove(const OgreBites::MouseMotionEvent &evt)
{
int dx = evt.x - m_lastMouseX;
int dy = evt.y - m_lastMouseY;
m_lastMouseX = evt.x;
m_lastMouseY = evt.y;
if (m_rotating) {
m_yaw -= dx * ROTATION_SPEED;
m_pitch -= dy * ROTATION_SPEED;
// Clamp pitch
if (m_pitch > 89.0f)
m_pitch = 89.0f;
if (m_pitch < -89.0f)
m_pitch = -89.0f;
}
if (m_panning) {
// Get right and up vectors from camera's orientation
Ogre::Quaternion orientation =
m_camera->getDerivedOrientation();
Ogre::Vector3 right = orientation * Ogre::Vector3::UNIT_X;
Ogre::Vector3 up = orientation * Ogre::Vector3::UNIT_Y;
Ogre::Vector3 pan = right * (-dx * PAN_SPEED * m_distance) +
up * (dy * PAN_SPEED * m_distance);
m_target += pan;
m_targetNode->setPosition(m_target);
m_cameraMan->setTarget(m_targetNode);
}
}
void EditorCamera::handleMousePress(const OgreBites::MouseButtonEvent &evt)
{
m_lastMouseX = evt.x;
m_lastMouseY = evt.y;
if (evt.button == OgreBites::BUTTON_RIGHT) {
m_rotating = true;
m_fpsMode = true; // Enable FPS mode when right mouse is held
} else if (evt.button == OgreBites::BUTTON_MIDDLE) {
m_panning = true;
}
}
void EditorCamera::handleMouseRelease(const OgreBites::MouseButtonEvent &evt)
{
if (evt.button == OgreBites::BUTTON_RIGHT) {
m_rotating = false;
m_fpsMode =
false; // Disable FPS mode when right mouse is released
// Reset key states
m_keyW = m_keyS = m_keyA = m_keyD = m_keyQ = m_keyE = false;
} else if (evt.button == OgreBites::BUTTON_MIDDLE) {
m_panning = false;
}
}
void EditorCamera::handleKeyboard(const OgreBites::KeyboardEvent &evt)
{
// Track key states for FPS movement
bool pressed = (evt.type == OgreBites::KEYDOWN);
switch (evt.keysym.sym) {
case 'w':
case 'W':
m_keyW = pressed;
break;
case 's':
case 'S':
m_keyS = pressed;
break;
case 'a':
case 'A':
m_keyA = pressed;
break;
case 'd':
case 'D':
m_keyD = pressed;
break;
case 'q':
case 'Q':
m_keyQ = pressed;
break;
case 'e':
case 'E':
m_keyE = pressed;
break;
}
}
void EditorCamera::focusOn(const Ogre::Vector3 &point)
{
m_target = point;
m_targetNode->setPosition(m_target);
m_cameraMan->setTarget(m_targetNode);
}
void EditorCamera::setPosition(const Ogre::Vector3 &pos)
{
m_target = pos;
m_targetNode->setPosition(m_target);
m_cameraMan->setTarget(m_targetNode);
}
Ogre::Ray EditorCamera::getMouseRay(float screenX, float screenY) const
{
// Convert pixel coordinates to normalized viewport coordinates (0-1)
Ogre::Viewport *viewport = m_camera->getViewport();
if (!viewport)
return Ogre::Ray();
float normX = screenX / viewport->getActualWidth();
float normY = screenY / viewport->getActualHeight();
return m_camera->getCameraToViewportRay(normX, normY);
}
void EditorCamera::updateCameraPosition()
{
// CameraMan handles the positioning
m_targetNode->setPosition(m_target);
m_cameraMan->setYawPitchDist(Ogre::Degree(m_yaw), Ogre::Degree(m_pitch),
m_distance);
}
@@ -0,0 +1,113 @@
#ifndef EDITSCENE_EDITORCAMERA_HPP
#define EDITSCENE_EDITORCAMERA_HPP
#pragma once
#include <memory>
#include <Ogre.h>
#include <OgreCamera.h>
#include <OgreSceneManager.h>
#include <OgreRenderWindow.h>
#include <OgreInput.h>
#include <OgreCameraMan.h>
/**
* Editor camera controller using OgreBites::CameraMan
* Supports orbit (default) and FPS modes
*/
class EditorCamera {
public:
EditorCamera(Ogre::SceneManager *sceneMgr, Ogre::RenderWindow *window);
~EditorCamera();
/**
* Update camera (called each frame)
*/
void update(float deltaTime);
/**
* Handle input events
*/
void handleMouseMove(const OgreBites::MouseMotionEvent &evt);
void handleMousePress(const OgreBites::MouseButtonEvent &evt);
void handleMouseRelease(const OgreBites::MouseButtonEvent &evt);
void handleKeyboard(const OgreBites::KeyboardEvent &evt);
/**
* Get the camera
*/
Ogre::Camera *getCamera() const
{
return m_camera;
}
/**
* Focus camera on a point
*/
void focusOn(const Ogre::Vector3 &point);
/**
* Set camera position
*/
void setPosition(const Ogre::Vector3 &pos);
/**
* Get camera position
*/
Ogre::Vector3 getPosition() const
{
return m_position;
}
/**
* Get ray from mouse position
*/
Ogre::Ray getMouseRay(float screenX, float screenY) const;
/**
* Check if in FPS mode
*/
bool isFPSMode() const
{
return m_fpsMode;
}
private:
void updateCameraPosition();
void updateFPSMovement(float deltaTime);
Ogre::SceneManager *m_sceneMgr;
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;
// Camera state
Ogre::Vector3 m_position;
Ogre::Vector3 m_target;
float m_distance;
float m_yaw;
float m_pitch;
// Input state
bool m_rotating;
bool m_panning;
bool m_fpsMode;
int m_lastMouseX;
int m_lastMouseY;
// Keyboard state for FPS movement
bool m_keyW;
bool m_keyS;
bool m_keyA;
bool m_keyD;
bool m_keyQ;
bool m_keyE;
// Movement speeds
static constexpr float ROTATION_SPEED = 0.3f;
static constexpr float PAN_SPEED = 0.01f;
static constexpr float FPS_SPEED = 10.0f;
};
#endif // EDITSCENE_EDITORCAMERA_HPP
@@ -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();
});
}
@@ -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
@@ -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>();
});
}
@@ -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
@@ -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>();
});
}
@@ -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
@@ -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>();
});
}
@@ -0,0 +1,9 @@
#include "AnimationTree.hpp"
AnimationTreeComponent::AnimationTreeComponent()
: root()
, enabled(true)
, useRootMotion(false)
, dirty(true)
{
}
@@ -0,0 +1,165 @@
#ifndef EDITSCENE_ANIMATIONTREE_HPP
#define EDITSCENE_ANIMATIONTREE_HPP
#pragma once
#include <Ogre.h>
#include <vector>
#include <unordered_map>
/**
* A node in the animation tree.
*
* Node types:
* "output" - Root output node, optional speed multiplier (1 child)
* "stateMachine" - Cross-fades between child states, named for lookup
* "state" - A named state within a state machine (1 child)
* "speed" - Playback speed multiplier (1 child)
* "animation" - Leaf referencing an Ogre animation by name
*/
struct AnimationTreeNode {
Ogre::String type = "animation";
Ogre::String name;
Ogre::String animationName;
float speed = 1.0f;
float fadeSpeed = 7.5f;
std::vector<AnimationTreeNode> children;
/* For stateMachine nodes: auto-transition when animation ends */
std::unordered_map<Ogre::String, Ogre::String> endTransitions;
AnimationTreeNode() = default;
AnimationTreeNode *findChild(const Ogre::String &childName)
{
for (auto &child : children) {
if (child.name == childName)
return &child;
}
return nullptr;
}
const AnimationTreeNode *findChild(
const Ogre::String &childName) const
{
for (const auto &child : children) {
if (child.name == childName)
return &child;
}
return nullptr;
}
AnimationTreeNode *findStateMachine(const Ogre::String &smName)
{
if (type == "stateMachine" && name == smName)
return this;
for (auto &child : children) {
auto *found = child.findStateMachine(smName);
if (found)
return found;
}
return nullptr;
}
const AnimationTreeNode *findStateMachine(
const Ogre::String &smName) const
{
if (type == "stateMachine" && name == smName)
return this;
for (const auto &child : children) {
auto *found = child.findStateMachine(smName);
if (found)
return found;
}
return nullptr;
}
AnimationTreeNode *findState(const Ogre::String &stateName)
{
if (type == "state" && name == stateName)
return this;
for (auto &child : children) {
auto *found = child.findState(stateName);
if (found)
return found;
}
return nullptr;
}
const AnimationTreeNode *findState(
const Ogre::String &stateName) const
{
if (type == "state" && name == stateName)
return this;
for (const auto &child : children) {
auto *found = child.findState(stateName);
if (found)
return found;
}
return nullptr;
}
AnimationTreeNode *findAnimationLeaf()
{
if (type == "animation")
return this;
for (auto &child : children) {
auto *found = child.findAnimationLeaf();
if (found)
return found;
}
return nullptr;
}
const AnimationTreeNode *findAnimationLeaf() const
{
if (type == "animation")
return this;
for (const auto &child : children) {
auto *found = child.findAnimationLeaf();
if (found)
return found;
}
return nullptr;
}
void collectStateMachines(std::vector<AnimationTreeNode *> &out)
{
if (type == "stateMachine")
out.push_back(this);
for (auto &child : children)
child.collectStateMachines(out);
}
void collectStateMachines(
std::vector<const AnimationTreeNode *> &out) const
{
if (type == "stateMachine")
out.push_back(this);
for (const auto &child : children)
child.collectStateMachines(out);
}
};
/**
* Animation tree component for hierarchical state-machine-based animation.
*
* The tree is evaluated each frame by AnimationTreeSystem to determine
* which Ogre AnimationStates are active and their blend weights.
*/
struct AnimationTreeComponent {
AnimationTreeNode root;
bool enabled = true;
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;
AnimationTreeComponent();
};
#endif // EDITSCENE_ANIMATIONTREE_HPP
@@ -0,0 +1,57 @@
#include "AnimationTree.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/AnimationTreeEditor.hpp"
#include "Transform.hpp"
static AnimationTreeComponent createDefaultTree()
{
AnimationTreeComponent at;
at.root.type = "output";
at.root.speed = 1.0f;
AnimationTreeNode sm;
sm.type = "stateMachine";
sm.name = "main";
sm.fadeSpeed = 7.5f;
AnimationTreeNode state;
state.type = "state";
state.name = "idle";
AnimationTreeNode anim;
anim.type = "animation";
anim.animationName = "idle";
state.children.push_back(anim);
sm.children.push_back(state);
at.root.children.push_back(sm);
return at;
}
REGISTER_COMPONENT_GROUP("Animation Tree", "Rendering",
AnimationTreeComponent, AnimationTreeEditor)
{
registry.registerComponent<AnimationTreeComponent>(
"Animation Tree", "Rendering",
std::make_unique<AnimationTreeEditor>(sceneMgr),
/* Adder */
[sceneMgr](flecs::entity e) {
if (!e.has<TransformComponent>()) {
TransformComponent transform;
transform.node =
sceneMgr->getRootSceneNode()
->createChildSceneNode();
e.set<TransformComponent>(transform);
}
AnimationTreeComponent at = createDefaultTree();
at.dirty = true;
e.set<AnimationTreeComponent>(at);
},
/* Remover */
[sceneMgr](flecs::entity e) {
(void)sceneMgr;
e.remove<AnimationTreeComponent>();
});
}
@@ -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>();
}
});
}
@@ -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
@@ -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
@@ -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>();
}
});
}
@@ -0,0 +1,37 @@
#ifndef EDITSCENE_CAMERA_HPP
#define EDITSCENE_CAMERA_HPP
#pragma once
#include <Ogre.h>
/**
* Camera component - attaches an Ogre::Camera to the entity's SceneNode
*/
struct CameraComponent {
// Camera properties
float fovY = 45.0f; // Vertical field of view in degrees
float nearClip = 0.1f; // Near clip distance
float farClip = 1000.0f; // Far clip distance
float aspectRatio = 16.0f / 9.0f; // Aspect ratio (width/height)
// For orthographic camera
bool orthographic = false;
float orthoWidth = 10.0f; // Width for orthographic view
float orthoHeight = 10.0f; // Height for orthographic view
// The Ogre camera object (created by CameraSystem)
Ogre::Camera* camera = nullptr;
// For preview (optional RTT - created on demand)
Ogre::RenderTarget* previewTarget = nullptr;
Ogre::TexturePtr previewTexture;
bool showPreview = false;
int previewWidth = 400;
int previewHeight = 300;
void markDirty() { needsRebuild = true; }
bool needsRebuild = true;
bool needsPreviewUpdate = false;
};
#endif // EDITSCENE_CAMERA_HPP
@@ -0,0 +1,53 @@
#include "Camera.hpp"
#include "Transform.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/CameraEditor.hpp"
#include "../systems/CameraSystem.hpp"
// Register Camera component
REGISTER_COMPONENT("Camera", CameraComponent, CameraEditor)
{
registry.registerComponent<CameraComponent>(
"Camera",
std::make_unique<CameraEditor>(sceneMgr, renderWindow),
// Adder
[sceneMgr](flecs::entity e) {
if (!e.has<CameraComponent>()) {
// Camera requires Transform
if (!e.has<TransformComponent>()) {
// Auto-add transform if missing
TransformComponent transform;
transform.node = sceneMgr->getRootSceneNode()->createChildSceneNode();
e.set<TransformComponent>(transform);
}
e.set<CameraComponent>({});
}
},
// Remover
[sceneMgr](flecs::entity e) {
if (e.has<CameraComponent>()) {
auto& camera = e.get_mut<CameraComponent>();
// Clean up Ogre camera and preview resources
if (camera.camera) {
// Clean up preview
if (camera.previewTarget) {
camera.previewTarget->removeAllListeners();
}
if (camera.previewTexture) {
Ogre::TextureManager::getSingleton().remove(
camera.previewTexture->getName());
}
// Detach and destroy camera
Ogre::SceneNode* parent = camera.camera->getParentSceneNode();
if (parent) {
parent->detachObject(camera.camera);
}
sceneMgr->destroyCamera(camera.camera);
camera.camera = nullptr;
}
e.remove<CameraComponent>();
}
}
);
}
@@ -0,0 +1,178 @@
#include "CellGrid.hpp"
#include <algorithm>
Cell* CellGridComponent::findCell(int x, int y, int z)
{
int64_t key = ((int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z);
for (auto& cell : cells) {
if (cell.getKey() == key) {
return &cell;
}
}
return nullptr;
}
const Cell* CellGridComponent::findCell(int x, int y, int z) const
{
int64_t key = ((int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z);
for (const auto& cell : cells) {
if (cell.getKey() == key) {
return &cell;
}
}
return nullptr;
}
Cell& CellGridComponent::getOrCreateCell(int x, int y, int z)
{
Cell* existing = findCell(x, y, z);
if (existing) {
return *existing;
}
Cell newCell;
newCell.x = x;
newCell.y = y;
newCell.z = z;
cells.push_back(newCell);
return cells.back();
}
void CellGridComponent::removeCell(int x, int y, int z)
{
int64_t key = ((int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z);
auto it = std::remove_if(cells.begin(), cells.end(),
[key](const Cell& c) { return c.getKey() == key; });
cells.erase(it, cells.end());
}
FurnitureCell* CellGridComponent::findFurnitureCell(int x, int y, int z)
{
int64_t key = ((int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z);
for (auto& cell : furnitureCells) {
if (cell.getKey() == key) {
return &cell;
}
}
return nullptr;
}
FurnitureCell& CellGridComponent::getOrCreateFurnitureCell(int x, int y, int z)
{
FurnitureCell* existing = findFurnitureCell(x, y, z);
if (existing) {
return *existing;
}
FurnitureCell newCell;
newCell.x = x;
newCell.y = y;
newCell.z = z;
furnitureCells.push_back(newCell);
return furnitureCells.back();
}
void CellGridComponent::removeFurnitureCell(int x, int y, int z)
{
int64_t key = ((int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z);
auto it = std::remove_if(furnitureCells.begin(), furnitureCells.end(),
[key](const FurnitureCell& c) { return c.getKey() == key; });
furnitureCells.erase(it, furnitureCells.end());
}
void CellGridComponent::clear()
{
cells.clear();
furnitureCells.clear();
markDirty();
}
Ogre::Vector3 CellGridComponent::cellToWorld(int x, int y, int z) const
{
return Ogre::Vector3(
x * cellSize,
y * cellHeight,
z * cellSize
);
}
void CellGridComponent::worldToCell(const Ogre::Vector3& worldPos, int& x, int& y, int& z) const
{
x = Ogre::Math::Floor(worldPos.x / cellSize);
y = Ogre::Math::Floor(worldPos.y / cellHeight);
z = Ogre::Math::Floor(worldPos.z / cellSize);
}
const char* CellGridComponent::getFlagName(uint64_t flag)
{
switch (flag) {
case CellFlags::Floor: return "Floor";
case CellFlags::Ceiling: return "Ceiling";
case CellFlags::WallXNeg: return "Ext Wall X-";
case CellFlags::WallXPos: return "Ext Wall X+";
case CellFlags::WallZPos: return "Ext Wall Z+";
case CellFlags::WallZNeg: return "Ext Wall Z-";
case CellFlags::DoorXNeg: return "Ext Door X-";
case CellFlags::DoorXPos: return "Ext Door X+";
case CellFlags::DoorZPos: return "Ext Door Z+";
case CellFlags::DoorZNeg: return "Ext Door Z-";
case CellFlags::WindowXNeg: return "Ext Window X-";
case CellFlags::WindowXPos: return "Ext Window X+";
case CellFlags::WindowZPos: return "Ext Window Z+";
case CellFlags::WindowZNeg: return "Ext Window Z-";
case CellFlags::IntWallXNeg: return "Int Wall X-";
case CellFlags::IntWallXPos: return "Int Wall X+";
case CellFlags::IntWallZPos: return "Int Wall Z+";
case CellFlags::IntWallZNeg: return "Int Wall Z-";
case CellFlags::IntDoorXNeg: return "Int Door X-";
case CellFlags::IntDoorXPos: return "Int Door X+";
case CellFlags::IntDoorZPos: return "Int Door Z+";
case CellFlags::IntDoorZNeg: return "Int Door Z-";
case CellFlags::IntWindowXNeg: return "Int Window X-";
case CellFlags::IntWindowXPos: return "Int Window X+";
case CellFlags::IntWindowZPos: return "Int Window Z+";
case CellFlags::IntWindowZNeg: return "Int Window Z-";
default: return "Unknown";
}
}
uint64_t CellGridComponent::getFlagByName(const std::string& name)
{
auto flags = getAllFlags();
for (const auto& [flag, flagName] : flags) {
if (name == flagName) {
return flag;
}
}
return 0;
}
std::vector<std::pair<uint64_t, const char*>> CellGridComponent::getAllFlags()
{
return {
{ CellFlags::Floor, "Floor" },
{ CellFlags::Ceiling, "Ceiling" },
{ CellFlags::WallXNeg, "Ext Wall X-" },
{ CellFlags::WallXPos, "Ext Wall X+" },
{ CellFlags::WallZPos, "Ext Wall Z+" },
{ CellFlags::WallZNeg, "Ext Wall Z-" },
{ CellFlags::DoorXNeg, "Ext Door X-" },
{ CellFlags::DoorXPos, "Ext Door X+" },
{ CellFlags::DoorZPos, "Ext Door Z+" },
{ CellFlags::DoorZNeg, "Ext Door Z-" },
{ CellFlags::WindowXNeg, "Ext Window X-" },
{ CellFlags::WindowXPos, "Ext Window X+" },
{ CellFlags::WindowZPos, "Ext Window Z+" },
{ CellFlags::WindowZNeg, "Ext Window Z-" },
{ CellFlags::IntWallXNeg, "Int Wall X-" },
{ CellFlags::IntWallXPos, "Int Wall X+" },
{ CellFlags::IntWallZPos, "Int Wall Z+" },
{ CellFlags::IntWallZNeg, "Int Wall Z-" },
{ CellFlags::IntDoorXNeg, "Int Door X-" },
{ CellFlags::IntDoorXPos, "Int Door X+" },
{ CellFlags::IntDoorZPos, "Int Door Z+" },
{ CellFlags::IntDoorZNeg, "Int Door Z-" },
{ CellFlags::IntWindowXNeg, "Int Window X-" },
{ CellFlags::IntWindowXPos, "Int Window X+" },
{ CellFlags::IntWindowZPos, "Int Window Z+" },
{ CellFlags::IntWindowZNeg, "Int Window Z-" },
};
}
@@ -0,0 +1,521 @@
#pragma once
#include <cstdint>
#include <vector>
#include <string>
#include <unordered_map>
#include <chrono>
#include <flecs.h>
#include <Ogre.h>
/**
* @brief Cell flags for building geometry
*
* Each cell in the 3D grid can have these features:
* - Floor/Ceiling
* - External walls (4 directions)
* - External doors/windows (4 directions each)
* - Internal walls (4 directions)
* - Internal doors/windows (4 directions each)
*/
namespace CellFlags {
constexpr uint64_t Floor = 1ULL << 0;
constexpr uint64_t Ceiling = 1ULL << 1;
// External walls
constexpr uint64_t WallXNeg = 1ULL << 2; // Wall on -X side
constexpr uint64_t WallXPos = 1ULL << 3; // Wall on +X side
constexpr uint64_t WallZPos = 1ULL << 4; // Wall on +Z side
constexpr uint64_t WallZNeg = 1ULL << 5; // Wall on -Z side
// External doors
constexpr uint64_t DoorXNeg = 1ULL << 6;
constexpr uint64_t DoorXPos = 1ULL << 7;
constexpr uint64_t DoorZPos = 1ULL << 8;
constexpr uint64_t DoorZNeg = 1ULL << 9;
// External windows
constexpr uint64_t WindowXNeg = 1ULL << 10;
constexpr uint64_t WindowXPos = 1ULL << 11;
constexpr uint64_t WindowZPos = 1ULL << 12;
constexpr uint64_t WindowZNeg = 1ULL << 13;
// Internal walls
constexpr uint64_t IntWallXNeg = 1ULL << 14;
constexpr uint64_t IntWallXPos = 1ULL << 15;
constexpr uint64_t IntWallZPos = 1ULL << 16;
constexpr uint64_t IntWallZNeg = 1ULL << 17;
// Internal doors
constexpr uint64_t IntDoorXNeg = 1ULL << 18;
constexpr uint64_t IntDoorXPos = 1ULL << 19;
constexpr uint64_t IntDoorZPos = 1ULL << 20;
constexpr uint64_t IntDoorZNeg = 1ULL << 21;
// Internal windows
constexpr uint64_t IntWindowXNeg = 1ULL << 22;
constexpr uint64_t IntWindowXPos = 1ULL << 23;
constexpr uint64_t IntWindowZPos = 1ULL << 24;
constexpr uint64_t IntWindowZNeg = 1ULL << 25;
// Masks
constexpr uint64_t AllExternalWalls = WallXNeg | WallXPos | WallZPos | WallZNeg;
constexpr uint64_t AllInternalWalls = IntWallXNeg | IntWallXPos | IntWallZPos | IntWallZNeg;
constexpr uint64_t AllWalls = AllExternalWalls | AllInternalWalls;
constexpr uint64_t AllDoors = DoorXNeg | DoorXPos | DoorZPos | DoorZNeg |
IntDoorXNeg | IntDoorXPos | IntDoorZPos | IntDoorZNeg;
constexpr uint64_t AllWindows = WindowXNeg | WindowXPos | WindowZPos | WindowZNeg |
IntWindowXNeg | IntWindowXPos | IntWindowZPos | IntWindowZNeg;
// Combined masks for corners (walls + doors + windows in each direction)
constexpr uint64_t AllXNeg = WallXNeg | DoorXNeg | WindowXNeg;
constexpr uint64_t AllXPos = WallXPos | DoorXPos | WindowXPos;
constexpr uint64_t AllZPos = WallZPos | DoorZPos | WindowZPos;
constexpr uint64_t AllZNeg = WallZNeg | DoorZNeg | WindowZNeg;
constexpr uint64_t AllIntXNeg = IntWallXNeg | IntDoorXNeg | IntWindowXNeg;
constexpr uint64_t AllIntXPos = IntWallXPos | IntDoorXPos | IntWindowXPos;
constexpr uint64_t AllIntZPos = IntWallZPos | IntDoorZPos | IntWindowZPos;
constexpr uint64_t AllIntZNeg = IntWallZNeg | IntDoorZNeg | IntWindowZNeg;
}
/**
* @brief A single cell in the 3D grid
*/
struct Cell {
int x = 0, y = 0, z = 0;
uint64_t flags = 0;
// Generate unique key for this cell
int64_t getKey() const {
// Support -1024 to 1024 range for each axis
return (int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z;
}
bool hasFlag(uint64_t flag) const { return (flags & flag) != 0; }
void setFlag(uint64_t flag) { flags |= flag; }
void clearFlag(uint64_t flag) { flags &= ~flag; }
void toggleFlag(uint64_t flag) { flags ^= flag; }
};
/**
* @brief Furniture placement in a cell
*/
struct FurnitureCell {
int x = 0, y = 0, z = 0;
std::vector<std::string> tags;
std::string furnitureType;
int rotation = 0; // 0-3, representing 0, 90, 180, 270 degrees
int64_t getKey() const {
return (int64_t)x + 2048 * (int64_t)y + 2048 * 2048 * (int64_t)z;
}
};
/**
* @brief Cell grid for procedural building generation
*
* This component stores a sparse 3D grid of cells that define
* building geometry (walls, floors, doors, windows).
*
* Used by: House/Lot generation, dungeon generation
*/
struct CellGridComponent {
// Grid dimensions (in cells)
int width = 10; // X dimension
int height = 1; // Y dimension (floors)
int depth = 10; // Z dimension
// Cell size in world units
float cellSize = 4.0f;
float cellHeight = 4.0f;
// The cell data (sparse storage)
std::vector<Cell> cells;
std::vector<FurnitureCell> furnitureCells;
// Generation script (Lua or custom format)
std::string generationScript;
// Texture rectangle names for different parts (from ProceduralTexture)
std::string floorRectName;
std::string ceilingRectName;
std::string extWallRectName;
std::string intWallRectName;
// Frame texture rectangles
std::string extDoorFrameRectName;
std::string intDoorFrameRectName;
std::string extWindowFrameRectName;
std::string intWindowFrameRectName;
// Roof texture rectangles
std::string roofTopRectName;
std::string roofSideRectName;
// Physics properties for generated colliders
float friction = 0.5f;
// Dirty flag - triggers rebuild
bool dirty = true;
unsigned int version = 0;
// Find cell at position (returns nullptr if not found)
Cell* findCell(int x, int y, int z);
const Cell* findCell(int x, int y, int z) const;
// Get or create cell at position
Cell& getOrCreateCell(int x, int y, int z);
// Remove cell at position
void removeCell(int x, int y, int z);
// Find furniture cell
FurnitureCell* findFurnitureCell(int x, int y, int z);
// Get or create furniture cell
FurnitureCell& getOrCreateFurnitureCell(int x, int y, int z);
// Remove furniture cell
void removeFurnitureCell(int x, int y, int z);
// Clear all cells
void clear();
// Mark for rebuild
void markDirty() { dirty = true; version++; }
// Convert local cell position to world position
Ogre::Vector3 cellToWorld(int x, int y, int z) const;
// Convert world position to cell coordinates
void worldToCell(const Ogre::Vector3& worldPos, int& x, int& y, int& z) const;
// Get bit name for editor display
static const char* getFlagName(uint64_t flag);
static uint64_t getFlagByName(const std::string& name);
static std::vector<std::pair<uint64_t, const char*>> getAllFlags();
};
/**
* @brief Room definition within a cell grid
*
* Rooms are rectangular areas that can be connected by doors.
* This is the ECS version of the Lua room() function.
*/
struct RoomComponent {
// Room bounds (in cell coordinates)
// A room from (minX, minZ) to (maxX-1, maxZ-1) inclusive
int minX = 0, minY = 0, minZ = 0;
int maxX = 1, maxY = 1, maxZ = 1; // exclusive max (size = max - min)
// Room name/tag for furniture placement rules
std::string roomType; // e.g., "bedroom", "kitchen", "hallway"
std::vector<std::string> tags;
// Generation flags
bool createFloor = true; // Create floor cells
bool createCeiling = true; // Create ceiling cells
bool createInteriorWalls = true; // Create iwallx-/+, iwallz-/+ around the room
bool createWindows = false; // Convert exterior-facing walls to windows
bool fillRoomWithFurniture = false; // Automatically place furniture based on tags
unsigned int furnitureSeed = 42; // Seed for deterministic furniture placement
float furnitureYOffset = 0.05f; // Y offset for all furniture in this room
// Dirty flag - triggers regeneration of cell grid
bool dirty = true;
void markDirty() { dirty = true; }
// Helper to get size
int getSizeX() const { return maxX - minX; }
int getSizeY() const { return maxY - minY; }
int getSizeZ() const { return maxZ - minZ; }
// Check if a cell position is inside this room
bool contains(int x, int y, int z) const {
return x >= minX && x < maxX &&
y >= minY && y < maxY &&
z >= minZ && z < maxZ;
}
// Check if a cell is on the room edge (for wall placement)
bool isOnEdge(int x, int z) const {
return x == minX || x == maxX - 1 || z == minZ || z == maxZ - 1;
}
// Get the side of the room this cell is on (0=Z-, 1=Z+, 2=X-, 3=X+, -1=not on edge)
int getEdgeSide(int x, int z) const {
if (!isOnEdge(x, z)) return -1;
if (z == minZ) return 0; // Z- (north)
if (z == maxZ - 1) return 1; // Z+ (south)
if (x == minX) return 2; // X- (west)
if (x == maxX - 1) return 3; // X+ (east)
return -1;
}
// Persistent unique ID for this room (survives save/load)
// If empty, will be auto-generated during serialization
std::string persistentId;
// Connected room persistent IDs (bidirectional connections)
// Using persistent IDs instead of entity IDs because entity IDs change on save/load
std::vector<std::string> connectedRoomIds;
// Exit doors for outside-facing walls (0=Z-, 1=Z+, 2=X-, 3=X+)
// An exit is always a door placed on a wall that faces outside (not connected to another room)
bool exits[4] = { false, false, false, false }; // Z-, Z+, X-, X+
// Generate a persistent ID if one doesn't exist
void ensurePersistentId(flecs::entity_t entityId = 0) {
if (persistentId.empty()) {
// Generate unique ID based on timestamp + random + optional entity ID
auto now = std::chrono::high_resolution_clock::now();
auto nanos = std::chrono::duration_cast<std::chrono::nanoseconds>(
now.time_since_epoch()).count();
persistentId = "room_" + std::to_string(nanos);
if (entityId != 0) {
persistentId += "_" + std::to_string(entityId);
}
}
}
// Add a connection to another room (bidirectional, by persistent ID)
void addConnection(const std::string& otherRoomId) {
// Check if not already connected
for (const auto& id : connectedRoomIds) {
if (id == otherRoomId) return;
}
connectedRoomIds.push_back(otherRoomId);
}
// Remove a connection to another room
void removeConnection(const std::string& otherRoomId) {
connectedRoomIds.erase(
std::remove(connectedRoomIds.begin(), connectedRoomIds.end(), otherRoomId),
connectedRoomIds.end()
);
}
// Check if connected to a room (by persistent ID)
bool isConnectedTo(const std::string& otherRoomId) const {
for (const auto& id : connectedRoomIds) {
if (id == otherRoomId) return true;
}
return false;
}
// Helper to set all exits at once
void setAllExits(bool value) {
for (int i = 0; i < 4; i++) {
exits[i] = value;
}
}
// Helper to set a specific exit
void setExit(int side, bool value) {
if (side >= 0 && side < 4) {
exits[side] = value;
}
}
};
/**
* @brief Room exits - defines external exits from a room
*
* This replaces the Lua create_exit0/1/2/3() functions.
* Creates external walls/doors/windows on the room edges that face outside.
*/
/**
* @brief Clear Area - clears cells in a region before room generation
*
* This replaces the Lua clear_area() and clear_furniture_area() functions.
* It clears all cells within the specified bounds before any room generation happens.
* This component is processed first in the RoomLayoutSystem.
*/
struct ClearAreaComponent {
// Area bounds (in cell coordinates, inclusive min, exclusive max)
int minX = 0, minY = 0, minZ = 0;
int maxX = 1, maxY = 1, maxZ = 1;
// What to clear
bool clearCells = true; // Clear cell flags (walls, floors, etc.)
bool clearFurniture = true; // Clear furniture placements
bool clearRoofs = false; // Clear roof definitions (children with RoofComponent)
bool clearRooms = false; // Remove existing room entities (children with RoomComponent)
// Processed flag - cleared each time dirty is set
bool processed = false;
// Dirty flag - clear again when true
bool dirty = true;
void markDirty() { dirty = true; }
// Helper to set bounds
void setBounds(int x1, int z1, int x2, int z2, int y = 0) {
minX = x1; minZ = z1; minY = y;
maxX = x2; maxZ = z2; maxY = y + 1;
}
// Helper to get size
int getSizeX() const { return maxX - minX; }
int getSizeY() const { return maxY - minY; }
int getSizeZ() const { return maxZ - minZ; }
};
/**
* @brief Roof definition for a lot
*/
struct RoofComponent {
enum Type {
Flat = 0,
Normal = 1,
Normal2 = 2,
Cone = 3,
Cylinder = 4
};
Type type = Flat;
// Position (in cell coordinates)
int posX = 0, posY = 0, posZ = 0;
// Size (in cells)
int sizeX = 1, sizeZ = 1;
// Offset from cell position (world units)
float offsetX = 0.0f, offsetY = 0.0f, offsetZ = 0.0f;
// Height parameters
float baseHeight = 0.5f;
float maxHeight = 0.5f;
// Dirty flag - triggers rebuild when changed
bool dirty = true;
void markDirty() { dirty = true; }
};
/**
* @brief Lot component - represents a single house/building lot
*
* A lot contains:
* - Cell grid (walls, floors, etc.)
* - Room definitions
* - Roof definitions
* - Furniture placement rules
*/
struct LotComponent {
// Lot dimensions (in cells)
int width = 10;
int depth = 10;
// Elevation offset from district
float elevation = 0.0f;
// Rotation around district center (degrees)
float angle = 0.0f;
// Position offset (for manual placement)
float offsetX = 0.0f, offsetZ = 0.0f;
// Template name if this lot was created from a template
std::string templateName;
// Procedural material for lot base (optional - falls back to District, then Town)
flecs::entity proceduralMaterialEntity;
std::string proceduralMaterialEntityId; // For serialization
// Texture rectangle name from ProceduralTexture for UV mapping
std::string textureRectName;
// Physics properties for generated colliders
float friction = 0.5f;
// Dirty flag
bool dirty = true;
void markDirty() { dirty = true; }
};
/**
* @brief District component - contains multiple lots arranged in a circle
*/
struct DistrictComponent {
// District radius (distance from center to lots)
float radius = 50.0f;
// Elevation offset
float elevation = 0.0f;
// Height of district base (for plaza)
float height = 0.2f;
// Is this a plaza (circular base)?
bool isPlaza = false;
// Lot templates for this district
std::vector<std::string> lotTemplateNames;
// Procedural material for plaza (references entity with ProceduralMaterialComponent)
flecs::entity proceduralMaterialEntity;
std::string proceduralMaterialEntityId; // For serialization
// Texture rectangle name from ProceduralTexture for UV mapping
std::string textureRectName;
// Physics properties for generated colliders
float friction = 0.5f;
// Dirty flag
bool dirty = true;
void markDirty() { dirty = true; }
};
/**
* @brief Town component - top-level container
*/
struct TownComponent {
std::string townName = "New Town";
// Color rectangles for texture atlas
struct ColorRect {
float left = 0.0f, top = 0.0f, right = 1.0f, bottom = 1.0f;
Ogre::ColourValue color = Ogre::ColourValue::White;
};
std::unordered_map<std::string, ColorRect> colorRects;
// Material name (created from color rects)
std::string materialName;
// Procedural material for town (used by districts and lots)
flecs::entity proceduralMaterialEntity;
std::string proceduralMaterialEntityId; // For serialization
// Texture rectangle name from ProceduralTexture for UV mapping
std::string textureRectName;
// Dirty flag
bool dirty = true;
bool materialDirty = true;
void markDirty() { dirty = true; }
void markMaterialDirty() { materialDirty = true; }
};
/**
* @brief Furniture template - defines a furniture item
*/
struct FurnitureTemplateComponent {
std::string templateName;
std::vector<std::string> tags; // e.g., "bed", "essential", "bedroom"
// Mesh/model info
std::string meshName;
std::string materialName;
// Placement rules
bool requiresWall = false;
bool requiresFloor = true;
bool blocksPath = true;
// Offset from cell center
float offsetX = 0.0f, offsetY = 0.0f, offsetZ = 0.0f;
// Random selection weight
float weight = 1.0f;
};
@@ -0,0 +1,167 @@
#include "CellGrid.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/CellGridEditor.hpp"
#include "../ui/LotEditor.hpp"
#include "../ui/DistrictEditor.hpp"
#include "../ui/TownEditor.hpp"
#include "../ui/RoofEditor.hpp"
#include "../ui/RoomEditor.hpp"
#include "../ui/ClearAreaEditor.hpp"
#include "../ui/FurnitureTemplateEditor.hpp"
// Register CellGrid component
REGISTER_COMPONENT_GROUP("Cell Grid", "Cell Grid", CellGridComponent, CellGridEditor)
{
registry.registerComponent<CellGridComponent>(
"Cell Grid", "Cell Grid",
std::make_unique<CellGridEditor>(),
[sceneMgr](flecs::entity e) {
if (!e.has<CellGridComponent>()) {
e.set<CellGridComponent>({});
}
},
[sceneMgr](flecs::entity e) {
if (e.has<CellGridComponent>()) {
e.remove<CellGridComponent>();
}
}
);
}
// Register Lot component
REGISTER_COMPONENT_GROUP("Lot", "Cell Grid", LotComponent, LotEditor)
{
registry.registerComponent<LotComponent>(
"Lot", "Cell Grid",
std::make_unique<LotEditor>(),
[sceneMgr](flecs::entity e) {
if (!e.has<LotComponent>()) {
e.set<LotComponent>({});
}
},
[sceneMgr](flecs::entity e) {
if (e.has<LotComponent>()) {
e.remove<LotComponent>();
}
}
);
}
// Register District component
REGISTER_COMPONENT_GROUP("District", "Cell Grid", DistrictComponent, DistrictEditor)
{
registry.registerComponent<DistrictComponent>(
"District", "Cell Grid",
std::make_unique<DistrictEditor>(),
[sceneMgr](flecs::entity e) {
if (!e.has<DistrictComponent>()) {
e.set<DistrictComponent>({});
}
},
[sceneMgr](flecs::entity e) {
if (e.has<DistrictComponent>()) {
e.remove<DistrictComponent>();
}
}
);
}
// Register Town component
REGISTER_COMPONENT_GROUP("Town", "Cell Grid", TownComponent, TownEditor)
{
registry.registerComponent<TownComponent>(
"Town", "Cell Grid",
std::make_unique<TownEditor>(),
[sceneMgr](flecs::entity e) {
if (!e.has<TownComponent>()) {
e.set<TownComponent>({});
}
},
[sceneMgr](flecs::entity e) {
if (e.has<TownComponent>()) {
e.remove<TownComponent>();
}
}
);
}
// Register Roof component
REGISTER_COMPONENT_GROUP("Roof", "Cell Grid", RoofComponent, RoofEditor)
{
registry.registerComponent<RoofComponent>(
"Roof", "Cell Grid",
std::make_unique<RoofEditor>(),
[sceneMgr](flecs::entity e) {
if (!e.has<RoofComponent>()) {
e.set<RoofComponent>({});
}
},
[sceneMgr](flecs::entity e) {
if (e.has<RoofComponent>()) {
e.remove<RoofComponent>();
}
}
);
}
// Register Room component (in Room Layout group)
REGISTER_COMPONENT_GROUP("Room", "Room Layout", RoomComponent, RoomEditor)
{
registry.registerComponent<RoomComponent>(
"Room", "Room Layout",
std::make_unique<RoomEditor>(),
[sceneMgr](flecs::entity e) {
if (!e.has<RoomComponent>()) {
e.set<RoomComponent>({});
}
},
[sceneMgr](flecs::entity e) {
if (e.has<RoomComponent>()) {
e.remove<RoomComponent>();
}
}
);
}
// Register FurnitureTemplate component
REGISTER_COMPONENT_GROUP("Furniture Template", "Cell Grid", FurnitureTemplateComponent, FurnitureTemplateEditor)
{
registry.registerComponent<FurnitureTemplateComponent>(
"Furniture Template", "Cell Grid",
std::make_unique<FurnitureTemplateEditor>(),
[sceneMgr](flecs::entity e) {
if (!e.has<FurnitureTemplateComponent>()) {
e.set<FurnitureTemplateComponent>({});
}
},
[sceneMgr](flecs::entity e) {
if (e.has<FurnitureTemplateComponent>()) {
e.remove<FurnitureTemplateComponent>();
}
}
);
}
// Register ClearArea component (in Room Layout group)
REGISTER_COMPONENT_GROUP("Clear Area", "Room Layout", ClearAreaComponent, ClearAreaEditor)
{
registry.registerComponent<ClearAreaComponent>(
"Clear Area", "Room Layout",
std::make_unique<ClearAreaEditor>(),
[sceneMgr](flecs::entity e) {
if (!e.has<ClearAreaComponent>()) {
e.set<ClearAreaComponent>({});
}
},
[sceneMgr](flecs::entity e) {
if (e.has<ClearAreaComponent>()) {
e.remove<ClearAreaComponent>();
}
}
);
}
// Note: ExteriorGenerationComponent has been removed.
// Exterior generation now automatically runs at the end of the RoomLayoutSystem pipeline.
// This mirrors the original Lua behavior where create_exterior() was called after all rooms were defined.
@@ -0,0 +1,48 @@
#include "CellGridModule.hpp"
#include "CellGrid.hpp"
#include "GeneratedPhysicsTag.hpp"
#include <nlohmann/json.hpp>
#include <Ogre.h>
namespace CellGridModule {
using json = nlohmann::json;
void registerComponents(flecs::world& world)
{
// Register components without .member<> for complex types (std::string, std::vector)
// Flecs doesn't have built-in reflection for these, but JSON serialization handles them
// CellGridComponent
world.component<CellGridComponent>();
// Cell struct
world.component<Cell>();
// FurnitureCell struct
world.component<FurnitureCell>();
// LotComponent
world.component<LotComponent>();
// DistrictComponent
world.component<DistrictComponent>();
// TownComponent
world.component<TownComponent>();
// TownComponent::ColorRect
world.component<TownComponent::ColorRect>();
// RoomComponent
world.component<RoomComponent>();
// RoofComponent
world.component<RoofComponent>();
// FurnitureTemplateComponent
world.component<FurnitureTemplateComponent>();
world.component<GeneratedPhysicsTag>();
}
} // namespace CellGridModule
@@ -0,0 +1,7 @@
#pragma once
#include <flecs.h>
namespace CellGridModule {
void registerComponents(flecs::world& world);
}
@@ -0,0 +1,61 @@
#ifndef EDITSCENE_CHARACTER_HPP
#define EDITSCENE_CHARACTER_HPP
#pragma once
#include <Ogre.h>
/**
* Character physics component
*
* Attaches a Jolt JPH::Character (kinematic capsule) to the entity.
* The entity may also have CharacterSlotsComponent; the character
* physics lives on the same entity as the visual character.
*
* Child entities can add extra collision shapes via PhysicsColliderComponent.
*/
struct CharacterComponent {
/* Capsule dimensions */
float radius = 0.3f;
float height = 1.8f; /* cylinder height (excluding spherical caps) */
/* Offset from the entity's scene node */
Ogre::Vector3 offset = Ogre::Vector3::ZERO;
/* Current linear velocity (m/s), applied each frame by CharacterSystem */
Ogre::Vector3 linearVelocity = Ogre::Vector3::ZERO;
/* 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;
}
};
#endif // EDITSCENE_CHARACTER_HPP
@@ -0,0 +1,442 @@
#include "CharacterClassDatabase.hpp"
#include <OgreLogManager.h>
#include <OgreResourceGroupManager.h>
#include <nlohmann/json.hpp>
#include <fstream>
#include <filesystem>
// ---------------------------------------------------------------------------
// Singleton
// ---------------------------------------------------------------------------
CharacterClassDatabase &CharacterClassDatabase::getSingleton()
{
static CharacterClassDatabase instance;
return instance;
}
CharacterClassDatabase *CharacterClassDatabase::getSingletonPtr()
{
return &getSingleton();
}
// ---------------------------------------------------------------------------
// Lookup
// ---------------------------------------------------------------------------
const CharacterClassDatabase::StatDef *
CharacterClassDatabase::findStat(const Ogre::String &name) const
{
auto it = m_stats.find(name);
if (it != m_stats.end())
return &it->second;
return nullptr;
}
const CharacterClassDatabase::SkillDef *
CharacterClassDatabase::findSkill(const Ogre::String &name) const
{
auto it = m_skills.find(name);
if (it != m_skills.end())
return &it->second;
return nullptr;
}
const CharacterClassDatabase::NeedDef *
CharacterClassDatabase::findNeed(const Ogre::String &name) const
{
auto it = m_needs.find(name);
if (it != m_needs.end())
return &it->second;
return nullptr;
}
const CharacterClassDatabase::ClassDef *
CharacterClassDatabase::findClass(const Ogre::String &name) const
{
auto it = m_classes.find(name);
if (it != m_classes.end())
return &it->second;
return nullptr;
}
CharacterClassDatabase::StatDef *
CharacterClassDatabase::findStat(const Ogre::String &name)
{
auto it = m_stats.find(name);
if (it != m_stats.end())
return &it->second;
return nullptr;
}
CharacterClassDatabase::SkillDef *
CharacterClassDatabase::findSkill(const Ogre::String &name)
{
auto it = m_skills.find(name);
if (it != m_skills.end())
return &it->second;
return nullptr;
}
CharacterClassDatabase::NeedDef *
CharacterClassDatabase::findNeed(const Ogre::String &name)
{
auto it = m_needs.find(name);
if (it != m_needs.end())
return &it->second;
return nullptr;
}
CharacterClassDatabase::ClassDef *
CharacterClassDatabase::findClass(const Ogre::String &name)
{
auto it = m_classes.find(name);
if (it != m_classes.end())
return &it->second;
return nullptr;
}
// ---------------------------------------------------------------------------
// Mutators
// ---------------------------------------------------------------------------
void CharacterClassDatabase::addOrReplaceStat(const StatDef &def)
{
bool wasNew = m_stats.find(def.name) == m_stats.end();
m_stats[def.name] = def;
if (wasNew)
m_statNames.push_back(def.name);
}
void CharacterClassDatabase::addOrReplaceSkill(const SkillDef &def)
{
bool wasNew = m_skills.find(def.name) == m_skills.end();
m_skills[def.name] = def;
if (wasNew)
m_skillNames.push_back(def.name);
}
void CharacterClassDatabase::addOrReplaceNeed(const NeedDef &def)
{
bool wasNew = m_needs.find(def.name) == m_needs.end();
m_needs[def.name] = def;
if (wasNew)
m_needNames.push_back(def.name);
}
void CharacterClassDatabase::addOrReplaceClass(const ClassDef &def)
{
bool wasNew = m_classes.find(def.name) == m_classes.end();
m_classes[def.name] = def;
if (wasNew)
m_classNames.push_back(def.name);
}
bool CharacterClassDatabase::removeStat(const Ogre::String &name)
{
if (m_stats.erase(name) == 0)
return false;
auto it = std::remove(m_statNames.begin(), m_statNames.end(),
name);
m_statNames.erase(it, m_statNames.end());
return true;
}
bool CharacterClassDatabase::removeSkill(const Ogre::String &name)
{
if (m_skills.erase(name) == 0)
return false;
auto it = std::remove(m_skillNames.begin(), m_skillNames.end(),
name);
m_skillNames.erase(it, m_skillNames.end());
return true;
}
bool CharacterClassDatabase::removeNeed(const Ogre::String &name)
{
if (m_needs.erase(name) == 0)
return false;
auto it = std::remove(m_needNames.begin(), m_needNames.end(),
name);
m_needNames.erase(it, m_needNames.end());
return true;
}
bool CharacterClassDatabase::removeClass(const Ogre::String &name)
{
if (m_classes.erase(name) == 0)
return false;
auto it = std::remove(m_classNames.begin(), m_classNames.end(),
name);
m_classNames.erase(it, m_classNames.end());
return true;
}
void CharacterClassDatabase::clear()
{
m_stats.clear();
m_skills.clear();
m_needs.clear();
m_classes.clear();
m_statNames.clear();
m_skillNames.clear();
m_needNames.clear();
m_classNames.clear();
}
// ---------------------------------------------------------------------------
// Runtime helpers
// ---------------------------------------------------------------------------
int64_t CharacterClassDatabase::computeXPForLevel(int level,
const ClassDef &cls) const
{
return static_cast<int64_t>(cls.xpForLevel.evaluate(level));
}
int CharacterClassDatabase::computePointsForLevel(int level,
const ClassDef &cls) const
{
return static_cast<int>(cls.pointsPerLevel.evaluate(level));
}
int CharacterClassDatabase::computeStatGrowth(const Ogre::String &stat,
int level,
const ClassDef &cls) const
{
auto it = cls.statGrowth.find(stat);
if (it == cls.statGrowth.end())
return 0;
return static_cast<int>(it->second.evaluate(level));
}
int CharacterClassDatabase::computeSkillGrowth(const Ogre::String &skill,
int level,
const ClassDef &cls) const
{
auto it = cls.skillGrowth.find(skill);
if (it == cls.skillGrowth.end())
return 0;
return static_cast<int>(it->second.evaluate(level));
}
int CharacterClassDatabase::computeStatCost(int currentValue,
const ClassDef &cls) const
{
return static_cast<int>(cls.statCost.evaluate(0, static_cast<double>(currentValue)));
}
// ---------------------------------------------------------------------------
// JSON serialization
// ---------------------------------------------------------------------------
bool CharacterClassDatabase::saveToJson(const std::string &filename)
{
auto &db = getSingleton();
nlohmann::json json;
json["version"] = 1;
// Stats
json["stat_definitions"] = nlohmann::json::object();
for (const auto &name : db.m_statNames) {
const auto &def = db.m_stats.at(name);
nlohmann::json j;
j["display_name"] = def.displayName;
j["kind"] = (def.kind == StatKind::Attribute) ? "attribute" :
"resource_pool";
j["min"] = def.minValue;
j["max"] = def.maxValue;
json["stat_definitions"][name] = j;
}
// Skills
json["skill_definitions"] = nlohmann::json::object();
for (const auto &name : db.m_skillNames) {
const auto &def = db.m_skills.at(name);
nlohmann::json j;
j["display_name"] = def.displayName;
j["max"] = def.maxValue;
json["skill_definitions"][name] = j;
}
// Needs
json["need_definitions"] = nlohmann::json::object();
for (const auto &name : db.m_needNames) {
const auto &def = db.m_needs.at(name);
nlohmann::json j;
j["display_name"] = def.displayName;
j["max"] = def.maxValue;
j["accumulation_rate"] = def.accumulationRate;
j["low_threshold"] = def.lowThreshold;
j["high_threshold"] = def.highThreshold;
j["bit_name"] = def.bitName;
json["need_definitions"][name] = j;
}
// Classes
json["classes"] = nlohmann::json::object();
for (const auto &name : db.m_classNames) {
const auto &cls = db.m_classes.at(name);
nlohmann::json j;
j["description"] = cls.description;
j["primary_stats"] = cls.primaryStats;
j["base_stats"] = cls.baseStats;
j["base_skills"] = cls.baseSkills;
j["base_needs"] = cls.baseNeeds;
j["xp_for_level"] = cls.xpForLevel.getExpression();
j["points_per_level"] = cls.pointsPerLevel.getExpression();
j["stat_cost"] = cls.statCost.getExpression();
j["stat_growth"] = nlohmann::json::object();
for (const auto &pair : cls.statGrowth)
j["stat_growth"][pair.first] =
pair.second.getExpression();
j["skill_growth"] = nlohmann::json::object();
for (const auto &pair : cls.skillGrowth)
j["skill_growth"][pair.first] =
pair.second.getExpression();
json["classes"][name] = j;
}
try {
std::filesystem::path outPath =
std::filesystem::current_path() / filename;
std::ofstream f(outPath);
if (!f.is_open()) {
Ogre::LogManager::getSingleton().logMessage(
"CharacterClassDatabase: Failed to open " +
filename + " for writing");
return false;
}
f << json.dump(4);
return true;
} catch (...) {
Ogre::LogManager::getSingleton().logMessage(
"CharacterClassDatabase: Exception saving " + filename);
return false;
}
}
bool CharacterClassDatabase::loadFromJson(const std::string &filename)
{
auto &db = getSingleton();
db.clear();
std::filesystem::path inPath =
std::filesystem::current_path() / filename;
std::ifstream f(inPath);
if (!f.is_open()) {
Ogre::LogManager::getSingleton().logMessage(
"CharacterClassDatabase: Could not load " + filename +
", using defaults.");
return false;
}
nlohmann::json json;
try {
f >> json;
} catch (...) {
Ogre::LogManager::getSingleton().logMessage(
"CharacterClassDatabase: JSON parse error in " +
filename);
return false;
}
// Stats
if (json.contains("stat_definitions") &&
json["stat_definitions"].is_object()) {
for (auto &[key, val] : json["stat_definitions"].items()) {
StatDef def;
def.name = key;
def.displayName = val.value("display_name", key);
Ogre::String kindStr = val.value("kind", "attribute");
def.kind = (kindStr == "resource_pool") ? StatKind::ResourcePool :
StatKind::Attribute;
def.minValue = val.value("min", 1);
def.maxValue = val.value("max", 999);
db.addOrReplaceStat(def);
}
}
// Skills
if (json.contains("skill_definitions") &&
json["skill_definitions"].is_object()) {
for (auto &[key, val] : json["skill_definitions"].items()) {
SkillDef def;
def.name = key;
def.displayName = val.value("display_name", key);
def.maxValue = val.value("max", 100);
db.addOrReplaceSkill(def);
}
}
// Needs
if (json.contains("need_definitions") &&
json["need_definitions"].is_object()) {
for (auto &[key, val] : json["need_definitions"].items()) {
NeedDef def;
def.name = key;
def.displayName = val.value("display_name", key);
def.maxValue = val.value("max", 1000);
def.accumulationRate = val.value("accumulation_rate", 0.0f);
def.lowThreshold = val.value("low_threshold", 0);
def.highThreshold = val.value("high_threshold", 1000);
def.bitName = val.value("bit_name", "");
// Backward compat: old files used low_bit_name / high_bit_name
if (def.bitName.empty()) {
def.bitName = val.value("high_bit_name", "");
if (def.bitName.empty())
def.bitName = val.value("low_bit_name", "");
}
db.addOrReplaceNeed(def);
}
}
// Classes
if (json.contains("classes") && json["classes"].is_object()) {
for (auto &[key, val] : json["classes"].items()) {
ClassDef cls;
cls.name = key;
cls.description = val.value("description", "");
if (val.contains("primary_stats") &&
val["primary_stats"].is_array()) {
for (const auto &item : val["primary_stats"])
cls.primaryStats.push_back(
item.get<std::string>());
}
if (val.contains("base_stats") &&
val["base_stats"].is_object()) {
for (auto &[k, v] : val["base_stats"].items())
cls.baseStats[k] = v.get<int>();
}
if (val.contains("base_skills") &&
val["base_skills"].is_object()) {
for (auto &[k, v] : val["base_skills"].items())
cls.baseSkills[k] = v.get<int>();
}
if (val.contains("base_needs") &&
val["base_needs"].is_object()) {
for (auto &[k, v] : val["base_needs"].items())
cls.baseNeeds[k] = v.get<int>();
}
cls.xpForLevel = Formula(val.value("xp_for_level", "0"));
cls.pointsPerLevel =
Formula(val.value("points_per_level", "0"));
cls.statCost = Formula(val.value("stat_cost", "1"));
if (val.contains("stat_growth") &&
val["stat_growth"].is_object()) {
for (auto &[k, v] : val["stat_growth"].items())
cls.statGrowth[k] =
Formula(v.get<std::string>());
}
if (val.contains("skill_growth") &&
val["skill_growth"].is_object()) {
for (auto &[k, v] : val["skill_growth"].items())
cls.skillGrowth[k] =
Formula(v.get<std::string>());
}
db.addOrReplaceClass(cls);
}
}
return true;
}
@@ -0,0 +1,153 @@
#ifndef EDITSCENE_CHARACTER_CLASS_DATABASE_HPP
#define EDITSCENE_CHARACTER_CLASS_DATABASE_HPP
#pragma once
#include "Formula.hpp"
#include <Ogre.h>
#include <unordered_map>
#include <vector>
/**
* Global character class database singleton.
*
* Holds the master list of stat/skill/need definitions and class templates.
* This is a singleton accessible from anywhere in the codebase.
* Persisted to character_class.json, read-only in game mode.
*/
class CharacterClassDatabase {
public:
static CharacterClassDatabase &getSingleton();
static CharacterClassDatabase *getSingletonPtr();
// -----------------------------------------------------------------------
// Definitions
// -----------------------------------------------------------------------
enum class StatKind {
Attribute, // permanent value (strength, agility)
ResourcePool // depletable pool (health, stamina, mana)
};
struct StatDef {
Ogre::String name;
Ogre::String displayName;
StatKind kind = StatKind::Attribute;
int minValue = 1;
int maxValue = 999;
};
struct SkillDef {
Ogre::String name;
Ogre::String displayName;
int maxValue = 100;
};
struct NeedDef {
Ogre::String name;
Ogre::String displayName;
int maxValue = 1000;
float accumulationRate = 0.0f;
int lowThreshold = 0; // clear bit when need <= this
int highThreshold = 1000; // set bit when need >= this
Ogre::String bitName; // GOAP blackboard bit (hysteresis)
};
struct ClassDef {
Ogre::String name;
Ogre::String description;
std::vector<Ogre::String> primaryStats;
std::unordered_map<Ogre::String, int> baseStats;
std::unordered_map<Ogre::String, int> baseSkills;
std::unordered_map<Ogre::String, int> baseNeeds;
Formula xpForLevel;
Formula pointsPerLevel;
Formula statCost;
std::unordered_map<Ogre::String, Formula> statGrowth;
std::unordered_map<Ogre::String, Formula> skillGrowth;
};
// -----------------------------------------------------------------------
// Lookup
// -----------------------------------------------------------------------
const StatDef *findStat(const Ogre::String &name) const;
const SkillDef *findSkill(const Ogre::String &name) const;
const NeedDef *findNeed(const Ogre::String &name) const;
const ClassDef *findClass(const Ogre::String &name) const;
StatDef *findStat(const Ogre::String &name);
SkillDef *findSkill(const Ogre::String &name);
NeedDef *findNeed(const Ogre::String &name);
ClassDef *findClass(const Ogre::String &name);
// -----------------------------------------------------------------------
// Mutators
// -----------------------------------------------------------------------
void addOrReplaceStat(const StatDef &def);
void addOrReplaceSkill(const SkillDef &def);
void addOrReplaceNeed(const NeedDef &def);
void addOrReplaceClass(const ClassDef &def);
bool removeStat(const Ogre::String &name);
bool removeSkill(const Ogre::String &name);
bool removeNeed(const Ogre::String &name);
bool removeClass(const Ogre::String &name);
void clear();
// -----------------------------------------------------------------------
// Lists
// -----------------------------------------------------------------------
const std::vector<Ogre::String> &getStatNames() const
{
return m_statNames;
}
const std::vector<Ogre::String> &getSkillNames() const
{
return m_skillNames;
}
const std::vector<Ogre::String> &getNeedNames() const
{
return m_needNames;
}
const std::vector<Ogre::String> &getClassNames() const
{
return m_classNames;
}
// -----------------------------------------------------------------------
// Persistence
// -----------------------------------------------------------------------
static bool saveToJson(const std::string &filename);
static bool loadFromJson(const std::string &filename);
// -----------------------------------------------------------------------
// Runtime helpers
// -----------------------------------------------------------------------
int64_t computeXPForLevel(int level, const ClassDef &cls) const;
int computePointsForLevel(int level, const ClassDef &cls) const;
int computeStatGrowth(const Ogre::String &stat, int level,
const ClassDef &cls) const;
int computeSkillGrowth(const Ogre::String &skill, int level,
const ClassDef &cls) const;
int computeStatCost(int currentValue, const ClassDef &cls) const;
private:
CharacterClassDatabase() = default;
std::unordered_map<Ogre::String, StatDef> m_stats;
std::unordered_map<Ogre::String, SkillDef> m_skills;
std::unordered_map<Ogre::String, NeedDef> m_needs;
std::unordered_map<Ogre::String, ClassDef> m_classes;
std::vector<Ogre::String> m_statNames;
std::vector<Ogre::String> m_skillNames;
std::vector<Ogre::String> m_needNames;
std::vector<Ogre::String> m_classNames;
};
#endif // EDITSCENE_CHARACTER_CLASS_DATABASE_HPP
@@ -0,0 +1,18 @@
#ifndef EDITSCENE_CHARACTERIDENTITY_HPP
#define EDITSCENE_CHARACTERIDENTITY_HPP
#pragma once
#include <cstdint>
/**
* Links a spawned entity to a global character registry entry.
*
* When a character entity is created from a prefab or manually,
* this component stores the persistent registry ID so the entity
* knows who it is in the social graph.
*/
struct CharacterIdentityComponent {
uint64_t registryId = 0;
};
#endif // EDITSCENE_CHARACTERIDENTITY_HPP
@@ -0,0 +1,20 @@
#include "../ui/ComponentRegistration.hpp"
#include "Character.hpp"
#include "../ui/CharacterEditor.hpp"
REGISTER_COMPONENT_GROUP("Character Physics", "Physics",
CharacterComponent, CharacterEditor)
{
registry.registerComponent<CharacterComponent>(
"Character Physics", "Physics", std::make_unique<CharacterEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<CharacterComponent>())
e.set<CharacterComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<CharacterComponent>())
e.remove<CharacterComponent>();
});
}
@@ -0,0 +1,58 @@
#ifndef EDITSCENE_CHARACTERSLOTS_HPP
#define EDITSCENE_CHARACTERSLOTS_HPP
#pragma once
#include <Ogre.h>
#include <unordered_map>
#include <vector>
/**
* Selection criteria for a single character slot.
* Layer 0 (nude base) is always implicit.
* Layer 1 and 2 are selected via combo boxes.
*/
struct SlotSelection {
Ogre::String layer1Mesh; // "none" or exact mesh name for layer 1
Ogre::String layer2Mesh; // "none" or exact mesh name for layer 2
Ogre::String explicitMesh; // backward-compat override
};
/**
* Multi-slot mesh component for character parts sharing a skeleton.
* The "face" slot (or first available slot) serves as the master skeleton.
*
* Age is now stored in CharacterRegistry::CharacterRecord::age
* and should be retrieved from there when needed.
*/
struct CharacterSlotsComponent {
Ogre::String sex = "male";
/* Backward-compat: old mesh-name map. Deserialized into slotSelections on load. */
std::unordered_map<Ogre::String, Ogre::String> slots;
/* Per-slot layer selections (runtime) */
std::unordered_map<Ogre::String, SlotSelection> slotSelections;
bool dirty = true;
/* 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.
*/
Ogre::Vector3 frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z;
CharacterSlotsComponent() = default;
};
/**
* Global shape key weights for a character.
* Same name applies to all slots uniformly.
*/
struct CharacterShapeKeysComponent {
std::unordered_map<Ogre::String, float> weights;
bool dirty = true;
};
#endif // EDITSCENE_CHARACTERSLOTS_HPP
@@ -0,0 +1,35 @@
#include "CharacterSlots.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/CharacterSlotsEditor.hpp"
#include "../systems/CharacterSlotSystem.hpp"
#include "Transform.hpp"
REGISTER_COMPONENT_GROUP("Character Slots", "Rendering",
CharacterSlotsComponent, CharacterSlotsEditor)
{
CharacterSlotSystem::loadCatalog();
registry.registerComponent<CharacterSlotsComponent>(
"Character Slots",
"Rendering",
std::make_unique<CharacterSlotsEditor>(sceneMgr),
/* Adder */
[sceneMgr](flecs::entity e) {
if (!e.has<TransformComponent>()) {
TransformComponent transform;
transform.node =
sceneMgr->getRootSceneNode()
->createChildSceneNode();
e.set<TransformComponent>(transform);
}
CharacterSlotsComponent cs;
cs.dirty = true;
e.set<CharacterSlotsComponent>(cs);
},
/* Remover */
[sceneMgr](flecs::entity e) {
(void)sceneMgr;
e.remove<CharacterSlotsComponent>();
}
);
}
@@ -0,0 +1,13 @@
#ifndef EDITSCENE_EDITORMARKER_HPP
#define EDITSCENE_EDITORMARKER_HPP
#pragma once
/**
* Marker component for entities created in the editor
* Used to filter out flecs internal entities (components, systems, etc.)
*/
struct EditorMarkerComponent {
// Empty marker component
};
#endif // EDITSCENE_EDITORMARKER_HPP
@@ -0,0 +1,21 @@
#ifndef EDITSCENE_ENTITYNAME_HPP
#define EDITSCENE_ENTITYNAME_HPP
#pragma once
#include <Ogre.h>
/**
* Name component for Flecs entities
* Used for display in the hierarchy window
*/
struct EntityNameComponent {
Ogre::String name;
EntityNameComponent() = default;
explicit EntityNameComponent(const Ogre::String &n)
: name(n)
{
}
};
#endif // EDITSCENE_ENTITYNAME_HPP
@@ -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
@@ -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>();
});
}
@@ -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
@@ -0,0 +1,192 @@
#include "Formula.hpp"
#include <cctype>
#include <cmath>
#include <OgreLogManager.h>
Formula::Formula(const Ogre::String &expr)
: m_expression(expr)
{
m_valid = !expr.empty();
}
double Formula::evaluate(const std::unordered_map<std::string, double> &vars) const
{
if (!m_valid || m_expression.empty())
return 0.0;
Parser p;
p.s = m_expression.c_str();
p.vars = &vars;
try {
return p.parseExpression();
} catch (...) {
return 0.0;
}
}
double Formula::evaluate(int level) const
{
std::unordered_map<std::string, double> vars;
vars["level"] = static_cast<double>(level);
return evaluate(vars);
}
double Formula::evaluate(int level, double current) const
{
std::unordered_map<std::string, double> vars;
vars["level"] = static_cast<double>(level);
vars["current"] = current;
return evaluate(vars);
}
// ---------------------------------------------------------------------------
// Parser implementation
// ---------------------------------------------------------------------------
void Formula::Parser::skipWhitespace()
{
while (*s == ' ' || *s == '\t')
s++;
}
bool Formula::Parser::consume(char c)
{
skipWhitespace();
if (*s == c) {
s++;
return true;
}
return false;
}
double Formula::Parser::parseExpression()
{
double value = parseTerm();
while (true) {
skipWhitespace();
if (consume('+')) {
value += parseTerm();
} else if (consume('-')) {
value -= parseTerm();
} else {
break;
}
}
return value;
}
double Formula::Parser::parseTerm()
{
double value = parsePower();
while (true) {
skipWhitespace();
if (consume('*')) {
value *= parsePower();
} else if (consume('/')) {
double rhs = parsePower();
if (rhs != 0.0)
value /= rhs;
else
value = 0.0;
} else {
break;
}
}
return value;
}
double Formula::Parser::parsePower()
{
double value = parseUnary();
while (true) {
skipWhitespace();
if (consume('^')) {
value = std::pow(value, parseUnary());
} else {
break;
}
}
return value;
}
double Formula::Parser::parseUnary()
{
skipWhitespace();
if (consume('+'))
return parseUnary();
if (consume('-'))
return -parseUnary();
return parsePrimary();
}
double Formula::Parser::parsePrimary()
{
skipWhitespace();
// Number
if (std::isdigit(*s) || (*s == '.' && std::isdigit(*(s + 1)))) {
char *end = nullptr;
double val = std::strtod(s, &end);
s = end;
return val;
}
// Parenthesized expression
if (consume('(')) {
double val = parseExpression();
skipWhitespace();
if (!consume(')'))
return 0.0;
return val;
}
// Identifier (variable or function)
if (std::isalpha(*s) || *s == '_') {
const char *start = s;
while (std::isalnum(*s) || *s == '_')
s++;
std::string name(start, static_cast<size_t>(s - start));
skipWhitespace();
if (consume('(')) {
// Function call
std::vector<double> args;
if (!consume(')')) {
args.push_back(parseExpression());
while (consume(',')) {
args.push_back(parseExpression());
}
skipWhitespace();
if (!consume(')'))
return 0.0;
}
if (name == "floor" && args.size() >= 1)
return std::floor(args[0]);
if (name == "ceil" && args.size() >= 1)
return std::ceil(args[0]);
if (name == "min" && args.size() >= 2)
return args[0] < args[1] ? args[0] : args[1];
if (name == "max" && args.size() >= 2)
return args[0] > args[1] ? args[0] : args[1];
if (name == "clamp" && args.size() >= 3) {
if (args[0] < args[1])
return args[1];
if (args[0] > args[2])
return args[2];
return args[0];
}
return 0.0;
}
// Variable lookup
if (vars) {
auto it = vars->find(name);
if (it != vars->end())
return it->second;
}
return 0.0;
}
return 0.0;
}
@@ -0,0 +1,65 @@
#ifndef EDITSCENE_FORMULA_HPP
#define EDITSCENE_FORMULA_HPP
#pragma once
#include <Ogre.h>
#include <string>
#include <unordered_map>
/**
* Lightweight expression evaluator for RPG formulas.
*
* Supports:
* Variables: level, current, base, value
* Operators: +, -, *, /, ^, ()
* Functions: floor(x), ceil(x), min(a,b), max(a,b), clamp(x,lo,hi)
*
* Example: "level * level * 100 + floor(current / 10)"
*/
class Formula {
public:
Formula() = default;
explicit Formula(const Ogre::String &expr);
bool isValid() const { return m_valid; }
const Ogre::String &getExpression() const { return m_expression; }
/**
* Evaluate the formula with given variable bindings.
*
* @param vars Map of variable names to values.
* @return The computed result. Returns 0.0 if formula is invalid.
*/
double evaluate(const std::unordered_map<std::string, double> &vars) const;
/**
* Convenience: evaluate with just a level.
*/
double evaluate(int level) const;
/**
* Convenience: evaluate with level and current value.
*/
double evaluate(int level, double current) const;
private:
Ogre::String m_expression;
bool m_valid = false;
// Recursive descent parser
struct Parser {
const char *s;
const std::unordered_map<std::string, double> *vars;
double parseExpression();
double parseTerm();
double parsePower();
double parseUnary();
double parsePrimary();
void skipWhitespace();
bool consume(char c);
};
};
#endif // EDITSCENE_FORMULA_HPP
@@ -0,0 +1,11 @@
#ifndef EDITSCENE_GENERATEDPHYSICSTAG_HPP
#define EDITSCENE_GENERATEDPHYSICSTAG_HPP
#pragma once
/**
* Marker component for entities auto-generated by systems (e.g. physics colliders).
* Entities with this tag are hidden from the editor hierarchy and not serialized.
*/
struct GeneratedPhysicsTag {};
#endif // EDITSCENE_GENERATEDPHYSICSTAG_HPP
@@ -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
@@ -0,0 +1,227 @@
#include "GoapBlackboard.hpp"
#include <cstdlib>
std::array<std::string, 64> &GoapBlackboard::getBitNameRegistry()
{
static std::array<std::string, 64> registry;
static bool initialized = false;
if (!initialized) {
for (auto &s : registry)
s.clear();
initialized = true;
}
return registry;
}
void GoapBlackboard::setBitName(int index, const std::string &name)
{
if (index < 0 || index >= 64)
return;
getBitNameRegistry()[index] = name;
}
const char *GoapBlackboard::getBitName(int index)
{
if (index < 0 || index >= 64)
return nullptr;
const auto &name = getBitNameRegistry()[index];
return name.empty() ? nullptr : name.c_str();
}
int GoapBlackboard::findBitByName(const std::string &name)
{
if (name.empty())
return -1;
const auto &registry = getBitNameRegistry();
for (int i = 0; i < 64; i++) {
if (registry[i] == name)
return i;
}
return -1;
}
bool GoapBlackboard::satisfies(const GoapBlackboard &other) const
{
// Only compare bits that both sides consider relevant
uint64_t relevantMask = bitmask & other.bitmask;
// Check bits: for every bit set in other's mask (within relevant mask),
// our bit must match
uint64_t commonMask = mask & other.mask & relevantMask;
if ((bits & commonMask) != (other.bits & commonMask))
return false;
// Also check bits that other has set but we don't (within relevant mask)
uint64_t missingMask = other.mask & ~mask & relevantMask;
if (missingMask) {
// Other requires bits we don't have set -> fail
// But only if other has those bits set to 1
if ((other.bits & missingMask) != 0)
return false;
}
// Check values: for every key in other, our value must match
for (const auto &pair : other.values) {
auto it = values.find(pair.first);
if (it == values.end()) {
// If we don't have the key, only satisfy if target is 0
if (pair.second != 0)
return false;
} else if (it->second != pair.second) {
return false;
}
}
return true;
}
void GoapBlackboard::apply(const GoapBlackboard &other)
{
// Apply bit effects
bits = (bits & ~other.mask) | (other.bits & other.mask);
mask |= other.mask;
// Apply value effects
for (const auto &pair : other.values)
values[pair.first] = pair.second;
}
bool GoapBlackboard::getScalarValue(const std::string &key,
float &out) const
{
auto itf = floatValues.find(key);
if (itf != floatValues.end()) {
out = itf->second;
return true;
}
auto iti = values.find(key);
if (iti != values.end()) {
out = static_cast<float>(iti->second);
return true;
}
return false;
}
int GoapBlackboard::distanceTo(const GoapBlackboard &target,
bool ignoreValues) const
{
int distance = 0;
// Only compare bits that both sides consider relevant
uint64_t relevantMask = bitmask & target.bitmask;
// Bit differences (within relevant mask)
uint64_t commonMask = mask & target.mask & relevantMask;
distance += __builtin_popcountll((bits ^ target.bits) & commonMask);
// Bits target cares about but we don't have (within relevant mask)
uint64_t missingInUs = target.mask & ~mask & relevantMask;
distance += __builtin_popcountll(target.bits & missingInUs);
// Bits we care about but target doesn't (within relevant mask)
uint64_t missingInTarget = mask & ~target.mask & relevantMask;
distance += __builtin_popcountll(bits & missingInTarget);
if (ignoreValues)
return distance;
// Value differences (int only — planner ignores float/vec3)
for (const auto &pair : target.values) {
auto it = values.find(pair.first);
if (it == values.end())
distance += std::abs(pair.second);
else
distance += std::abs(it->second - pair.second);
}
// Values we have that target doesn't (may need to be cleared)
for (const auto &pair : values) {
if (target.values.find(pair.first) == target.values.end())
distance += std::abs(pair.second);
}
return distance;
}
Ogre::String GoapBlackboard::dump() const
{
Ogre::String result = "Blackboard:\n";
result += " Bits:\n";
for (int i = 0; i < 64; i++) {
if (hasBit(i)) {
const char *name = getBitName(i);
if (name)
result += " " + Ogre::String(name) + " = " +
(getBit(i) ? "true" : "false") + "\n";
else
result += " bit[" +
Ogre::StringConverter::toString(i) + "] = " +
(getBit(i) ? "true" : "false") + "\n";
}
}
if (!values.empty()) {
result += " Int values:\n";
for (const auto &pair : values)
result += " " + pair.first + " = " +
Ogre::StringConverter::toString(pair.second) +
"\n";
}
if (!floatValues.empty()) {
result += " Float values:\n";
for (const auto &pair : floatValues)
result += " " + pair.first + " = " +
Ogre::StringConverter::toString(pair.second) +
"\n";
}
if (!vec3Values.empty()) {
result += " Vec3 values:\n";
for (const auto &pair : vec3Values)
result += " " + pair.first + " = (" +
Ogre::StringConverter::toString(pair.second.x) +
", " +
Ogre::StringConverter::toString(pair.second.y) +
", " +
Ogre::StringConverter::toString(pair.second.z) +
")\n";
}
if (!stringValues.empty()) {
result += " String values:\n";
for (const auto &pair : stringValues)
result += " " + pair.first + " = " + pair.second + "\n";
}
return result;
}
void GoapBlackboard::merge(const GoapBlackboard &other)
{
// Merge bits
bits = (bits & ~other.mask) | (other.bits & other.mask);
mask |= other.mask;
// Merge values
for (const auto &pair : other.values)
values[pair.first] = pair.second;
for (const auto &pair : other.floatValues)
floatValues[pair.first] = pair.second;
for (const auto &pair : other.vec3Values)
vec3Values[pair.first] = pair.second;
for (const auto &pair : other.stringValues)
stringValues[pair.first] = pair.second;
}
std::vector<int> GoapBlackboard::getSetBits() const
{
std::vector<int> result;
uint64_t m = mask;
while (m) {
int bit = __builtin_ctzll(m);
result.push_back(bit);
m &= m - 1;
}
return result;
}
@@ -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
@@ -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>();
});
}
@@ -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();
}
@@ -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
@@ -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;
}
@@ -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
@@ -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
@@ -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>();
});
}
@@ -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
@@ -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>();
});
}
@@ -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
@@ -0,0 +1,141 @@
#ifndef EDITSCENE_INVENTORY_HPP
#define EDITSCENE_INVENTORY_HPP
#pragma once
#include <Ogre.h>
#include <flecs.h>
#include <vector>
#include <string>
#include <cstdint>
#include "../systems/ItemRegistry.hpp"
/**
* 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 or no world entity)
flecs::entity_t itemEntity = 0;
// Item registry key
std::string itemId;
// Stack size
int stackSize = 0;
bool isEmpty() const
{
return itemId.empty() && itemEntity == 0;
}
void clear()
{
itemEntity = 0;
itemId.clear();
stackSize = 0;
}
};
/**
* Inventory component.
*
* Attached to a character entity to hold items.
* Can also be attached to container entities (chests, barrels, etc.)
* to define their contents.
*/
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.)
bool isContainer = false;
// Whether this inventory is currently open (for containers being browsed)
bool isOpen = false;
// Persistent ID for scene containers (empty for character inventories)
std::string containerId;
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 std::string &itemId) const
{
for (int i = 0; i < (int)slots.size(); i++) {
if (!slots[i].isEmpty() && slots[i].itemId == itemId)
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 std::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 std::string &itemId) const
{
return findItem(itemId) >= 0;
}
/** Recalculate total weight from registry. */
void recalculateWeight()
{
totalWeight = 0.0f;
for (const auto &slot : slots) {
if (!slot.isEmpty())
totalWeight +=
ItemRegistry::getSingleton().getWeight(slot.itemId) *
slot.stackSize;
}
}
};
#endif // EDITSCENE_INVENTORY_HPP
@@ -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>();
});
}
@@ -0,0 +1,45 @@
#ifndef EDITSCENE_ITEM_HPP
#define EDITSCENE_ITEM_HPP
#pragma once
#include <Ogre.h>
#include <string>
/**
* Item reference component.
*
* Attached to a world entity that represents a pickable / interactable item.
* All item properties (name, type, weight, etc.) are stored in
* the ItemRegistry singleton and looked up by itemId.
*
* action: Optional GOAP action name to execute on interact (E key).
* If empty, the item is picked up into inventory.
* instanceId: Optional unique ID for global state tracking (save/load).
* disabled: Set to true when the item has been picked up / consumed.
*/
struct ItemComponent {
// Registry key for this item definition
Ogre::String itemId;
// Stack size for this world entity
int stackSize = 1;
// Optional action to execute on interact (instead of pickup)
Ogre::String action;
// Unique instance ID for global state tracking
Ogre::String instanceId;
// True when item has been picked up or consumed
bool disabled = false;
ItemComponent() = default;
explicit ItemComponent(const Ogre::String &id, int stack = 1)
: itemId(id)
, stackSize(stack)
{
}
};
#endif // EDITSCENE_ITEM_HPP
@@ -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>();
});
}
@@ -0,0 +1,48 @@
#ifndef EDITSCENE_LIGHT_HPP
#define EDITSCENE_LIGHT_HPP
#pragma once
#include <Ogre.h>
/**
* Light component - attaches an Ogre::Light to the entity's SceneNode
*/
struct LightComponent {
enum class LightType {
Point, // Omnidirectional light
Directional, // Parallel rays (sun/moon)
Spotlight // Cone-shaped light
};
LightType lightType = LightType::Point;
// Common properties
Ogre::ColourValue diffuseColor{0.8f, 0.8f, 0.8f};
Ogre::ColourValue specularColor{0.5f, 0.5f, 0.5f};
float intensity = 1.0f;
// Attenuation (for Point and Spot)
float range = 100.0f;
float constantAttenuation = 1.0f;
float linearAttenuation = 0.0f;
float quadraticAttenuation = 0.0f;
// Spotlight specific
float spotlightInnerAngle = 30.0f; // Degrees
float spotlightOuterAngle = 45.0f; // Degrees
float spotlightFalloff = 1.0f;
// Direction (for Directional and Spot, relative to node orientation)
Ogre::Vector3 direction{0, -1, 0};
// Cast shadows
bool castShadows = true;
// The Ogre light object (created by LightSystem)
Ogre::Light* light = nullptr;
void markDirty() { needsRebuild = true; }
bool needsRebuild = true;
};
#endif // EDITSCENE_LIGHT_HPP
@@ -0,0 +1,43 @@
#include "Light.hpp"
#include "Transform.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/LightEditor.hpp"
#include "../systems/LightSystem.hpp"
// Register Light component
REGISTER_COMPONENT("Light", LightComponent, LightEditor)
{
registry.registerComponent<LightComponent>(
"Light",
std::make_unique<LightEditor>(),
// Adder
[sceneMgr](flecs::entity e) {
if (!e.has<LightComponent>()) {
// Light requires Transform
if (!e.has<TransformComponent>()) {
// Auto-add transform if missing
TransformComponent transform;
transform.node = sceneMgr->getRootSceneNode()->createChildSceneNode();
e.set<TransformComponent>(transform);
}
e.set<LightComponent>({});
}
},
// Remover
[sceneMgr](flecs::entity e) {
if (e.has<LightComponent>()) {
auto& light = e.get_mut<LightComponent>();
// Clean up Ogre light
if (light.light) {
Ogre::SceneNode* parent = light.light->getParentSceneNode();
if (parent) {
parent->detachObject(light.light);
}
sceneMgr->destroyLight(light.light);
light.light = nullptr;
}
e.remove<LightComponent>();
}
}
);
}
+47
View File
@@ -0,0 +1,47 @@
#ifndef EDITSCENE_LOD_HPP
#define EDITSCENE_LOD_HPP
#pragma once
#include <Ogre.h>
#include <flecs.h>
#include <string>
#include "LodSettings.hpp"
/**
* LodComponent - Per-entity LOD configuration
*
* This component references a LodSettingsComponent by its settingsId
* for persistent identification across scene loads.
*/
struct LodComponent {
// Reference to the settings by ID (persistent across scene loads)
std::string settingsId;
// Runtime entity reference (resolved from settingsId)
flecs::entity settingsEntity = flecs::entity::null();
// Per-entity distance multiplier (scales all LOD distances)
float distanceMultiplier = 1.0f;
// Whether LOD has been applied to the mesh
bool lodApplied = false;
// Last settings version that was applied
uint32_t appliedVersion = 0;
// Dirty flag for regenerating LOD
bool dirty = true;
void markDirty() { dirty = true; }
// Helper to check if LOD needs to be regenerated
bool needsUpdate() const { return dirty; }
// Helper to check if settings reference is valid
bool hasValidSettings() const {
return settingsEntity.is_alive() &&
settingsEntity.has<LodSettingsComponent>();
}
};
#endif // EDITSCENE_LOD_HPP
@@ -0,0 +1,56 @@
#include "Lod.hpp"
#include "LodSettings.hpp"
#include "Renderable.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/LodEditor.hpp"
#include "../ui/LodSettingsEditor.hpp"
// Register LodSettings component
REGISTER_COMPONENT("LOD Settings", LodSettingsComponent, LodSettingsEditor)
{
registry.registerComponent<LodSettingsComponent>(
"LOD Settings",
std::make_unique<LodSettingsEditor>(),
// Adder
[sceneMgr](flecs::entity e) {
if (!e.has<LodSettingsComponent>()) {
auto settings = LodSettingsComponent();
// Create default LOD levels
settings.createDefaultLevels();
e.set<LodSettingsComponent>(settings);
}
},
// Remover
[sceneMgr](flecs::entity e) {
if (e.has<LodSettingsComponent>()) {
e.remove<LodSettingsComponent>();
}
}
);
}
// Register Lod component
REGISTER_COMPONENT("LOD", LodComponent, LodEditor)
{
registry.registerComponent<LodComponent>(
"LOD",
std::make_unique<LodEditor>(),
// Adder
[sceneMgr](flecs::entity e) {
if (!e.has<LodComponent>()) {
// LOD requires Renderable
if (!e.has<RenderableComponent>()) {
// Auto-add renderable if missing (will need mesh assignment later)
e.set<RenderableComponent>({});
}
e.set<LodComponent>({});
}
},
// Remover
[sceneMgr](flecs::entity e) {
if (e.has<LodComponent>()) {
e.remove<LodComponent>();
}
}
);
}

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