From c557950cca794878d2fa9614afb041ed7c2d0031 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Thu, 12 Mar 2026 14:03:58 +0300 Subject: [PATCH] Refactoring editing tools --- CMakeLists.txt | 1 + assets/blender/characters/CMakeLists.txt | 11 +- .../characters/clothes-female-bottom.blend | 3 + .../characters/clothes-male-bottom.blend | 4 +- assets/blender/characters/combine_clothes.py | 22 +- .../characters/edited-normal-female.blend | 4 +- .../blender/characters/transfer_shape_keys.py | 201 +- src/gamedata/CMakeLists.txt | 2 +- src/gamedata/EditorGUIModule.cpp | 5 +- src/gamedata/items/CMakeLists.txt | 2 +- src/gamedata/items/items.cpp | 848 +++++++ src/gamedata/items/items.h | 1 + src/gamedata/items/town.cpp | 2211 +++++++++++------ 13 files changed, 2543 insertions(+), 772 deletions(-) create mode 100644 assets/blender/characters/clothes-female-bottom.blend diff --git a/CMakeLists.txt b/CMakeLists.txt index 059910c..6e406c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,6 +77,7 @@ add_subdirectory(assets/blender/vehicles) add_subdirectory(assets/blender/buildings/parts) add_subdirectory(assets/blender/characters) add_subdirectory(resources) +add_subdirectory(src/text_editor) add_executable(Game Game.cpp ${WATER_SRC}) target_include_directories(Game PRIVATE src/gamedata) diff --git a/assets/blender/characters/CMakeLists.txt b/assets/blender/characters/CMakeLists.txt index b05084f..dd1a3d2 100644 --- a/assets/blender/characters/CMakeLists.txt +++ b/assets/blender/characters/CMakeLists.txt @@ -74,7 +74,7 @@ add_custom_command( OUTPUT ${CMAKE_BINARY_DIR}/characters/female/normal-female.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-female.blend + ${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated.blend ${CMAKE_BINARY_DIR}/characters/female/normal-female.glb "${FEMALE_OBJECTS}" "female" @@ -85,7 +85,7 @@ add_custom_command( -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py ${VRM_IMPORTED_BLENDS} - ${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female.blend + ${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated.blend ${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed WORKING_DIRECTORY ${CMAKE_BINARY_DIR} VERBATIM @@ -423,6 +423,7 @@ function(add_clothes_pipeline INPUT_BLEND WEIGHTED_BLEND FINAL_OUTPUT_BLEND) endfunction() weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-male-bottom.blend) +weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-bottom.blend) add_clothes_pipeline( "${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-male.blend" # INPUT_BLEND @@ -430,3 +431,9 @@ add_clothes_pipeline( "${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated.blend" # FINAL_OUTPUT_BLEND ) +add_clothes_pipeline( + "${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-female.blend" # INPUT_BLEND + "${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-female-bottom_weighted.blend" # WEIGHTED_BLEND + "${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated.blend" # FINAL_OUTPUT_BLEND +) + diff --git a/assets/blender/characters/clothes-female-bottom.blend b/assets/blender/characters/clothes-female-bottom.blend new file mode 100644 index 0000000..26a9fea --- /dev/null +++ b/assets/blender/characters/clothes-female-bottom.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55218aaeab82db2d0d1fc87846a06b9d8a9c4604a1c07eba97aab150e7b242f6 +size 10176516 diff --git a/assets/blender/characters/clothes-male-bottom.blend b/assets/blender/characters/clothes-male-bottom.blend index b38ae27..82d746e 100644 --- a/assets/blender/characters/clothes-male-bottom.blend +++ b/assets/blender/characters/clothes-male-bottom.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b99b0f6c88d970d4cfb72a13d1815a0aa579107c358bddff76194dac7b2d6d1 -size 1835992 +oid sha256:0f3a804260bf6fb3c15852de047a2fd1d39666d17a4645b897bef4f883fe81c1 +size 1433176 diff --git a/assets/blender/characters/combine_clothes.py b/assets/blender/characters/combine_clothes.py index 0920066..03271a7 100644 --- a/assets/blender/characters/combine_clothes.py +++ b/assets/blender/characters/combine_clothes.py @@ -35,7 +35,7 @@ def get_transformation_matrices(obj): return m, m_inv, m_normal -def raycast_and_adjust_vertices(target_body, bvh_cloth): +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) @@ -49,7 +49,7 @@ def raycast_and_adjust_vertices(target_body, bvh_cloth): n_world = (m_body_normal @ v.normal).normalized() # Raycast forward (into cloth) and backward (from inside cloth) - hit_f, _, _, _ = bvh_cloth.ray_cast(v_world, n_world, 0.015) + 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: @@ -151,9 +151,19 @@ def process_clothing_pair(clothing_obj, target_obj, whitelist, is_clothing_copy= print(f"Processing: {clothing_name_for_final} -> {target_name} (using copy)") + # NEW CODE: Store custom properties from clothing before they might be lost + clothing_props = {} + for prop_name in ["ref_shapes", "ref_ray_length"]: + if prop_name in clothing_obj: + clothing_props[prop_name] = clothing_obj[prop_name] + print(f" Stored property '{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: + out_ray_length = clothing_obj["ref_ray_length"] bvh_cloth = setup_bvh_and_matrices(clothing_obj) - hit_values = raycast_and_adjust_vertices(new_target, bvh_cloth) + hit_values = raycast_and_adjust_vertices(new_target, bvh_cloth, out_ray_length) # Step B: Remove hidden geometry protect_and_remove_hidden_geometry(new_target, hit_values) @@ -188,6 +198,12 @@ def process_clothing_pair(clothing_obj, target_obj, whitelist, is_clothing_copy= bpy.context.view_layer.objects.active = new_target bpy.ops.object.join() + # NEW CODE: Copy stored custom properties to combined object + # After joining, new_target is the active object and contains the combined mesh + for prop_name, prop_value in clothing_props.items(): + new_target[prop_name] = prop_value + print(f" Copied property '{prop_name}' = {prop_value} to combined object") + # Rename the combined object using the appropriate clothing name new_target.name = f"{target_name}_{clothing_name_for_final}" whitelist.add(new_target) diff --git a/assets/blender/characters/edited-normal-female.blend b/assets/blender/characters/edited-normal-female.blend index f32c9d3..2c66b3f 100644 --- a/assets/blender/characters/edited-normal-female.blend +++ b/assets/blender/characters/edited-normal-female.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3a722d4acf4b4e17770b4696496852caa40c8b875a13c707c8e9c94269c0437 -size 18561336 +oid sha256:24b51f926ef0407e7ef5cc8f51f745f5b7f3cd00dd0b911908748f216617c582 +size 19202013 diff --git a/assets/blender/characters/transfer_shape_keys.py b/assets/blender/characters/transfer_shape_keys.py index b8f7795..08219f6 100644 --- a/assets/blender/characters/transfer_shape_keys.py +++ b/assets/blender/characters/transfer_shape_keys.py @@ -29,35 +29,63 @@ def parse_args(): print("Usage: blender -b --python transfer_shape_keys.py -- ") sys.exit(1) -def load_source_data(source_path): - """Load source file and extract shape key information""" +def get_source_object_name(target_obj_info): + """Get source object name from target's custom property or fallback values""" + # Check for custom property "ref_shapes" in the stored info + if target_obj_info.get('ref_shapes'): + ref_name = target_obj_info['ref_shapes'] + print(f" Using ref_shapes custom property: '{ref_name}'") + return ref_name + + # Fallback to "Body_shapes" then "Body" + print(f" No ref_shapes property found, trying fallbacks") + return None + +def load_source_data(source_path, target_obj_info): + """Load source file and extract shape key information using object name from target info""" print(f"\nLoading source file: {source_path}") if not os.path.exists(source_path): print(f"Error: Source file not found: {source_path}") sys.exit(1) - current_file = bpy.data.filepath if bpy.data.filepath else None + # Get source object name based on target's custom property from stored info + source_obj_name = get_source_object_name(target_obj_info) + # Open source file bpy.ops.wm.open_mainfile(filepath=source_path) body_object = None - for obj in bpy.data.objects: - if obj.name == "Body_shapes" and obj.type == 'MESH': - body_object = obj - break + + # If we have a specific name from ref_shapes, try that first + if source_obj_name: + for obj in bpy.data.objects: + if obj.name == source_obj_name and obj.type == 'MESH': + body_object = obj + print(f" Found source object: '{source_obj_name}' from ref_shapes") + break + + # If not found or no ref_shapes, try fallbacks + if not body_object: + fallback_names = ["Body_shapes", "Body"] + for fallback_name in fallback_names: + print(f" Trying fallback: '{fallback_name}'") + for obj in bpy.data.objects: + if obj.name == fallback_name and obj.type == 'MESH': + body_object = obj + source_obj_name = fallback_name + print(f" Found source object: '{fallback_name}'") + break + if body_object: + break if not body_object: - for obj in bpy.data.objects: - if obj.name == "Body" and obj.type == 'MESH': - body_object = obj - break - if not body_object: - print("Error: Could not find mesh object named 'Body' in source file") + print("Error: Could not find mesh object in source file") + print("Tried: ref_shapes property (if present), 'Body_shapes', 'Body'") sys.exit(1) if not body_object.data.shape_keys: - print("Error: Body object has no shape keys") + print(f"Error: Source object '{source_obj_name}' has no shape keys") sys.exit(1) shape_key_data = { @@ -73,7 +101,7 @@ def load_source_data(source_path): shape_key_data['polygons'].append([v for v in poly.vertices]) source_shape_keys = body_object.data.shape_keys.key_blocks - print(f"Found Body object with {len(source_shape_keys)} shape keys") + print(f"Found source object '{source_obj_name}' with {len(source_shape_keys)} shape keys") for sk in source_shape_keys: shape_key_data['names'].append(sk.name) @@ -85,15 +113,13 @@ def load_source_data(source_path): shape_key_data['vertex_positions'][sk.name] = vertex_positions - if current_file and os.path.exists(current_file): - bpy.ops.wm.open_mainfile(filepath=current_file) - elif current_file: - bpy.ops.wm.read_homefile() + # Store the source object name that was actually used + shape_key_data['source_obj_name'] = source_obj_name return shape_key_data def load_target_file(target_path): - """Load the target .blend file and find objects with custom properties""" + """Load the target .blend file and collect information about target objects""" print(f"\nLoading target file: {target_path}") if not os.path.exists(target_path): @@ -103,23 +129,41 @@ def load_target_file(target_path): temp_dir = os.path.join(os.path.dirname(target_path), "temp_blend_files") os.makedirs(temp_dir, exist_ok=True) - temp_target = os.path.join(temp_dir, os.path.basename(target_path)) + # Create a unique temp file path + temp_target = os.path.join(temp_dir, f"temp_processing_{os.path.basename(target_path)}") shutil.copy2(target_path, temp_target) bpy.ops.wm.open_mainfile(filepath=temp_target) - target_objects = [] + # Store information about target objects without keeping references + target_objects_info = [] for obj in bpy.data.objects: if obj.type == 'MESH' and obj.data: if all(prop in obj for prop in ['age', 'sex', 'slot']): - target_objects.append(obj) - print(f"Found target object: {obj.name}") - print(f" - age: {obj['age']}") - print(f" - sex: {obj['sex']}") - print(f" - slot: {obj['slot']}") - print(f" - vertices: {len(obj.data.vertices)}") + obj_info = { + 'name': obj.name, + 'age': obj['age'], + 'sex': obj['sex'], + 'slot': obj['slot'], + 'vertex_count': len(obj.data.vertices), + 'ref_shapes': obj.get('ref_shapes', None) # Store ref_shapes if present + } + target_objects_info.append(obj_info) + print(f"Found target object: {obj_info['name']}") + print(f" - age: {obj_info['age']}") + print(f" - sex: {obj_info['sex']}") + print(f" - slot: {obj_info['slot']}") + print(f" - vertices: {obj_info['vertex_count']}") + if obj_info['ref_shapes']: + print(f" - ref_shapes: '{obj_info['ref_shapes']}'") - return target_objects, temp_target + return target_objects_info, temp_target + +def get_target_object_by_name(obj_name): + """Get a valid reference to a target object by name""" + if obj_name in bpy.data.objects: + return bpy.data.objects[obj_name] + return None def delete_existing_shape_keys(target_obj): """Delete all existing shape keys from target object""" @@ -921,6 +965,31 @@ def save_output_file(output_path, temp_target): print(f"File saved successfully") +def process_target_object(target_obj_info, source_data, current_file_path): + """Process a single target object using its stored information""" + print(f"\nProcessing: {target_obj_info['name']}") + print(f" {'=' * 40}") + + # Get a fresh reference to the target object + target_obj = get_target_object_by_name(target_obj_info['name']) + if not target_obj: + print(f" Error: Could not find target object '{target_obj_info['name']}'") + return False + + ensure_shape_keys_structure(source_data['names'], target_obj) + 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 main(): print("=" * 60) print("Blender Shape Key Transfer Script - Boundary Velocity Limiting") @@ -932,29 +1001,60 @@ def main(): print(f"Output: {output_file}") try: - source_data = load_source_data(source_file) - target_objects, temp_target = load_target_file(target_file) + # First, load target file and collect information about target objects + target_objects_info, temp_target = load_target_file(target_file) - if target_objects: - for idx, target_obj in enumerate(target_objects, 1): - print(f"\n[{idx}/{len(target_objects)}] Processing: {target_obj.name}") - print(f" {'=' * 40}") - - ensure_shape_keys_structure(source_data['names'], target_obj) - mapping = transfer_shape_keys(source_data, target_obj) - verify_transfer(source_data['names'], target_obj) - - 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") - - save_output_file(output_file, temp_target) - else: + if not target_objects_info: print("\nNo target objects found with required properties") + sys.exit(1) + + print(f"\nFound {len(target_objects_info)} target objects to process") + + # Create a working file that we'll update after each object + working_file = temp_target + + # Process each target object + for idx, target_obj_info in enumerate(target_objects_info, 1): + print(f"\n{'=' * 60}") + print(f"[{idx}/{len(target_objects_info)}] Processing: {target_obj_info['name']}") + print(f"{'=' * 60}") + + # Load source data using the target object's information + source_data = load_source_data(source_file, target_obj_info) + + # Load the current working file + bpy.ops.wm.open_mainfile(filepath=working_file) + + # Process the target object + success = process_target_object(target_obj_info, source_data, source_file) + + if not success: + print(f" Failed to process {target_obj_info['name']}") + continue + + # Save progress to a new temp file + temp_progress_file = os.path.join(os.path.dirname(working_file), f"progress_{idx}_{os.path.basename(target_file)}") + bpy.ops.wm.save_as_mainfile(filepath=temp_progress_file) + + # Update working file for next iteration + working_file = temp_progress_file + + print(f"\nProgress saved to: {temp_progress_file}") + + # Copy the final working file to the output location + print(f"\nCopying final result to: {output_file}") + shutil.copy2(working_file, output_file) + + # Clean up temp files + try: + temp_dir = os.path.dirname(temp_target) + for file in os.listdir(temp_dir): + if file.startswith("temp_") or file.startswith("progress_"): + os.remove(os.path.join(temp_dir, file)) + if os.path.exists(temp_dir) and not os.listdir(temp_dir): + os.rmdir(temp_dir) + except: + pass print("\n" + "=" * 60) print("Script completed successfully!") @@ -968,3 +1068,4 @@ def main(): if __name__ == "__main__": main() + diff --git a/src/gamedata/CMakeLists.txt b/src/gamedata/CMakeLists.txt index 923451e..371be30 100644 --- a/src/gamedata/CMakeLists.txt +++ b/src/gamedata/CMakeLists.txt @@ -13,7 +13,7 @@ add_library(GameData STATIC GameData.cpp CharacterModule.cpp WaterModule.cpp Sun VehicleManagerModule.cpp AppModule.cpp StaticGeometryModule.cpp SmartObject.cpp SlotsModule.cpp QuestModule.cpp PlayerActionModule.cpp CharacterAIModule.cpp goap.cpp AnimationSystem.cpp) target_link_libraries(GameData PUBLIC - items luamodule + items luamodule text_editor flecs::flecs_static nlohmann_json::nlohmann_json OgreMain diff --git a/src/gamedata/EditorGUIModule.cpp b/src/gamedata/EditorGUIModule.cpp index e176f12..0ee6bce 100644 --- a/src/gamedata/EditorGUIModule.cpp +++ b/src/gamedata/EditorGUIModule.cpp @@ -1227,6 +1227,7 @@ struct EditorGUIListener : public Ogre::RenderTargetListener { if (changed) StaticGeometryModule::saveTemplates(); } + Items::itemsEditor(); if (ImGui::SmallButton("Run script for all towns...")) Items::runScriptsForAllTowns(); displayItems(); @@ -1399,8 +1400,8 @@ EditorGUIModule::EditorGUIModule(flecs::world &ecs) .without() .each([](const RenderWindow &window, const App &app, GUI &gui) { float vpScale = window.dpi / 96; - if (vpScale < 1.0f) - vpScale = 1.0f; + if (vpScale < 1.0f) + vpScale = 1.0f; Ogre::OverlayManager::getSingleton().setPixelRatio( vpScale); std::cout << "Editor GUI configure\n"; diff --git a/src/gamedata/items/CMakeLists.txt b/src/gamedata/items/CMakeLists.txt index 0b9fff8..f571769 100644 --- a/src/gamedata/items/CMakeLists.txt +++ b/src/gamedata/items/CMakeLists.txt @@ -13,6 +13,6 @@ target_link_libraries(items PRIVATE OgreMain OgreBites editor - physics + physics text_editor Tracy::TracyClient ) diff --git a/src/gamedata/items/items.cpp b/src/gamedata/items/items.cpp index 52cde85..193bd06 100644 --- a/src/gamedata/items/items.cpp +++ b/src/gamedata/items/items.cpp @@ -208,7 +208,855 @@ void createItemsMenu() ImGui::EndMenu(); } } +static void _setCameraSelectedPos(flecs::entity e) +{ + Ogre::Vector3 cursorPos; + Ogre::Quaternion cursorOrientation; + // setCursorSelectedPos(e, cursorPos, cursorOrientation); + StaticGeometryModule::getItemPositionAndRotation(e, cursorPos, + cursorOrientation); + ECS::get().sceneNode->_setDerivedPosition(cursorPos); + ECS::get().sceneNode->_setDerivedOrientation( + cursorOrientation); + Ogre::Vector3 cameraPos = + ECS::get().mCameraPivot->_getDerivedPosition(); + Ogre::Vector3 cameraOffset = + cursorOrientation * Ogre::Vector3::UNIT_Z * 30.0f; + cameraPos.x = cursorPos.x; + cameraPos.z = cursorPos.z; + cameraPos += cameraOffset; + cameraPos.y = TerrainModule::get_height( + ECS::get().mTerrainGroup, cameraPos) + + 10.0f; + Ogre::TerrainGroup *tg = ECS::get().mTerrainGroup; + long x, y; + tg->convertWorldPositionToTerrainSlot(cameraPos, &x, &y); + if (tg->getTerrain(x, y) && tg->getTerrain(x, y)->isLoaded()) { + float height = tg->getHeightAtWorldPosition(cameraPos); + cameraPos.y = height + 10.0f; + } + ECS::get().mCameraPivot->_setDerivedPosition(cameraPos); + ECS::get().mCameraPivot->_setDerivedOrientation( + cursorOrientation); + cameraPos = ECS::get().mCameraGoal->_getDerivedPosition(); + ECS::get().mCameraNode->_setDerivedPosition(cameraPos); + ECS::get().mCameraNode->_setDerivedOrientation( + ECS::get().mCameraGoal->_getDerivedOrientation()); + // updateHeightmap(); +} +void drawRectPreview(nlohmann::json::json_pointer ptr, + const nlohmann::json &item) +{ + // Reserve a square area spanning across the table + ImVec2 canvas_p0 = ImGui::GetCursorScreenPos(); + ImVec2 canvas_sz = ImVec2(ImGui::GetContentRegionAvail().x, 200); + ImVec2 canvas_p1 = + ImVec2(canvas_p0.x + canvas_sz.x, canvas_p0.y + canvas_sz.y); + Ogre::String rectName = ptr.back(); + nlohmann::json::json_pointer tmpptr = ptr; + tmpptr.pop_back(); + const nlohmann::json &colorRects = item[tmpptr]; + ImDrawList *draw_list = ImGui::GetWindowDrawList(); + // Draw background + draw_list->AddRectFilled(canvas_p0, canvas_p1, + IM_COL32(50, 50, 50, 255)); + + for (auto &[name, data] : colorRects.items()) { + std::cout << data.dump() << std::endl; + // Map 0..1 to Screen Pixels + ImVec2 p_min = + ImVec2(canvas_p0.x + (float)data["left"] * canvas_sz.x, + canvas_p0.y + (float)data["top"] * canvas_sz.y); + ImVec2 p_max = ImVec2( + canvas_p0.x + (float)data["right"] * canvas_sz.x, + canvas_p0.y + (float)data["bottom"] * canvas_sz.y); + + // Handle Color (assuming rgba are 0..255 or 0..1) + ImVec4 col = ImVec4(data["color"]["r"], data["color"]["g"], + data["color"]["b"], data["color"]["a"]); + + // Draw the rect + draw_list->AddRectFilled(p_min, p_max, + ImGui::ColorConvertFloat4ToU32(col)); + + // Outline if selected + if (rectName == name) + draw_list->AddRect(p_min, p_max, + IM_COL32(255, 255, 0, 255), 0.0f, 0, + 2.0f); + } + ImGui::Dummy(canvas_sz); // Advance cursor +} +#if 1 +bool DrawGridSelectorInTableCell(const char *id, ImVec2 &outValue, + float cellSize = 20.0f) +{ + bool changed = false; + static ImVec2 hoveredCell(-1, -1); + + // Push style to ensure consistent spacing + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + + ImDrawList *drawList = ImGui::GetWindowDrawList(); + ImVec2 cursorPos = ImGui::GetCursorScreenPos(); + + for (int y = 0; y < 10; y++) { + for (int x = 0; x < 10; x++) { + ImVec2 cellMin = ImVec2(cursorPos.x + x * cellSize, + cursorPos.y + y * cellSize); + ImVec2 cellMax = ImVec2(cellMin.x + cellSize, + cellMin.y + cellSize); + + // Use a dummy for spacing and hit detection + ImGui::PushID(y * 10 + x); + ImGui::Dummy(ImVec2(cellSize, cellSize)); + + // Check for interaction + if (ImGui::IsItemHovered()) { + hoveredCell = ImVec2(x / 9.0f, y / 9.0f); + + if (ImGui::IsMouseClicked(0)) { + outValue = hoveredCell; + changed = true; + } + } + ImGui::PopID(); + + // Draw cell + bool isSelected = (outValue.x == x / 9.0f && + outValue.y == y / 9.0f); + bool isHovered = (hoveredCell.x == x / 9.0f && + hoveredCell.y == y / 9.0f); + + ImU32 color; + if (isSelected) + color = IM_COL32(100, 150, 255, 255); + else if (isHovered) + color = IM_COL32(80, 80, 120, 255); + else + color = IM_COL32(60, 60, 70, 255); + + drawList->AddRectFilled(cellMin, cellMax, color); + drawList->AddRect(cellMin, cellMax, + IM_COL32(200, 200, 200, 255)); + + if (x < 9) + ImGui::SameLine(); + } + } + + // Move cursor past the grid + ImGui::Dummy(ImVec2(1, 1)); + + ImGui::PopStyleVar(2); + + return changed; +} +enum EditType { + EditNone = 0, + EditString, + EditMultilineString, + EditColorRect, + EditFloat, + EditBool, + EditInt, + ViewJson, + EditItemPosition, + EditLuaScript, +}; +struct PropertyEditor; +// Property editor definition +struct PropertyEditor { + int editType; + std::string name; + std::function + matches; + std::function + render; + std::function + apply; +}; +struct DPropertyEditor : PropertyEditor { +private: + virtual bool _matches(const std::string &itemName, + const nlohmann::json::json_pointer &ptr, + const nlohmann::json &j) = 0; + virtual void _render(flecs::entity entity, + const nlohmann::json::json_pointer &ptr, + nlohmann::json &j) = 0; + virtual void _apply(flecs::entity entity, + const nlohmann::json::json_pointer &ptr, + nlohmann::json &j) + { + } + +public: + DPropertyEditor(int type, const std::string &name) + { + editType = type; + this->name = name; + matches = [this](const std::string &itemName, + const nlohmann::json::json_pointer &ptr, + const nlohmann::json &j) -> bool { + return _matches(itemName, ptr, j); + }; + render = [this](flecs::entity entity, + const nlohmann::json::json_pointer &ptr, + nlohmann::json &j) { _render(entity, ptr, j); }; + apply = [this](flecs::entity entity, + const nlohmann::json::json_pointer &ptr, + nlohmann::json &j) { _apply(entity, ptr, j); }; + } + PropertyEditor getPropertyEditor() + { + return { editType, name, matches, render, apply }; + } + virtual ~DPropertyEditor() + { + } +}; +struct StringPropertyEditor : DPropertyEditor { + bool _matches(const std::string &itemName, + const nlohmann::json::json_pointer &ptr, + const nlohmann::json &j) override + { + return j[ptr].is_string(); + } + void _render(flecs::entity entity, + const nlohmann::json::json_pointer &ptr, + nlohmann::json &j) override + { + static char buffer[260]; + strncpy(buffer, j[ptr].get().c_str(), + sizeof(buffer)); + + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", ptr.back().c_str()); + ImGui::TableSetColumnIndex(1); + ImGui::InputText("##value", buffer, sizeof(buffer)); + + if (ImGui::IsItemDeactivatedAfterEdit()) { + j[ptr] = buffer; + StaticGeometryModule::setItemProperties(entity, + j.dump()); + } + } + StringPropertyEditor() + : DPropertyEditor(EditString, "String Editor") + { + } + ~StringPropertyEditor() override + { + } +}; +// EditLuaScript +struct LuaScriptPropertyEditor : DPropertyEditor { + bool _matches(const std::string &itemName, + const nlohmann::json::json_pointer &ptr, + const nlohmann::json &j) override + { + return itemName == "cellScript" && j[ptr].is_string(); + } + void _render(flecs::entity entity, + const nlohmann::json::json_pointer &ptr, + nlohmann::json &j) override + { + static char buffer[16384]; + strncpy(buffer, j[ptr].get().c_str(), + sizeof(buffer)); + + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", ptr.back().c_str()); + ImGui::TableSetColumnIndex(1); + ImGui::InputTextMultiline("##value", buffer, sizeof(buffer), + ImVec2(0, 0), + ImGuiInputTextFlags_AllowTabInput); + + if (ImGui::IsItemDeactivatedAfterEdit()) { + j[ptr] = buffer; + StaticGeometryModule::setItemProperties(entity, + j.dump()); + } + } + LuaScriptPropertyEditor() + : DPropertyEditor(EditLuaScript, "Lua Script Editor") + { + } + ~LuaScriptPropertyEditor() override + { + } +}; +struct MultilineStringPropertyEditor : DPropertyEditor { + bool _matches(const std::string &itemName, + const nlohmann::json::json_pointer &ptr, + const nlohmann::json &j) override + { + return j[ptr].is_string() && + (j[ptr].get().find("\n") != + std::string::npos || + j[ptr].get().length() >= 100); + } + void _render(flecs::entity entity, + const nlohmann::json::json_pointer &ptr, + nlohmann::json &j) override + { + static char buffer[4096]; + strncpy(buffer, j[ptr].get().c_str(), + sizeof(buffer)); + + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", ptr.back().c_str()); + ImGui::TableSetColumnIndex(1); + ImGui::InputTextMultiline("##value", buffer, sizeof(buffer)); + + if (ImGui::IsItemDeactivatedAfterEdit()) { + j[ptr] = buffer; + StaticGeometryModule::setItemProperties(entity, + j.dump()); + } + } + MultilineStringPropertyEditor() + : DPropertyEditor(EditMultilineString, + "Multiline String Editor") + { + } + ~MultilineStringPropertyEditor() override + { + } +}; +// Float editor +struct FloatPropertyEditor : DPropertyEditor { + bool _matches(const std::string &itemName, + const nlohmann::json::json_pointer &ptr, + const nlohmann::json &j) override + { + return j[ptr].is_number_float(); + } + void _render(flecs::entity entity, + const nlohmann::json::json_pointer &ptr, + nlohmann::json &j) override + { + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", ptr.back().c_str()); + ImGui::TableSetColumnIndex(1); + + float val = j[ptr].get(); + if (ImGui::InputFloat("##value", &val, 0.001f, 0.01f)) { + j[ptr] = val; + StaticGeometryModule::setItemProperties(entity, + j.dump()); + StaticGeometryModule::updateItemGeometry(entity); + } + } + FloatPropertyEditor() + : DPropertyEditor(EditFloat, "Float Editor") + { + } + ~FloatPropertyEditor() override + { + } +}; +// int editor +struct IntPropertyEditor : DPropertyEditor { + bool _matches(const std::string &itemName, + const nlohmann::json::json_pointer &ptr, + const nlohmann::json &j) override + { + return j[ptr].is_number_integer(); + } + void _render(flecs::entity entity, + const nlohmann::json::json_pointer &ptr, + nlohmann::json &j) override + { + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", ptr.back().c_str()); + ImGui::TableSetColumnIndex(1); + + int val = j[ptr].get(); + if (ImGui::InputInt("##value", &val, 1, 10)) { + j[ptr] = val; + StaticGeometryModule::setItemProperties(entity, + j.dump()); + StaticGeometryModule::updateItemGeometry(entity); + } + } + IntPropertyEditor() + : DPropertyEditor(EditInt, "Int Editor") + { + } + ~IntPropertyEditor() override + { + } +}; +// bool editor +struct BoolPropertyEditor : DPropertyEditor { + bool _matches(const std::string &itemName, + const nlohmann::json::json_pointer &ptr, + const nlohmann::json &j) override + { + return j[ptr].is_boolean(); + } + void _render(flecs::entity entity, + const nlohmann::json::json_pointer &ptr, + nlohmann::json &j) override + { + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", ptr.back().c_str()); + ImGui::TableSetColumnIndex(1); + + bool val = j[ptr].get(); + if (ImGui::Checkbox("##value", &val)) { + j[ptr] = val; + StaticGeometryModule::setItemProperties(entity, + j.dump()); + StaticGeometryModule::updateItemGeometry(entity); + } + } + BoolPropertyEditor() + : DPropertyEditor(EditBool, "Bool Editor") + { + } + ~BoolPropertyEditor() override + { + } +}; +// color rect editor +struct ColorRectPropertyEditor : DPropertyEditor { + bool _matches(const std::string &itemName, + const nlohmann::json::json_pointer &ptr, + const nlohmann::json &j) override + { + if (ptr.empty()) + return false; + if (!ptr.parent_pointer().empty() && + ptr.parent_pointer().back() == "colorRects") { + return true; + } + return false; + } + void _render(flecs::entity entity, + const nlohmann::json::json_pointer &ptr, + nlohmann::json &j) override + { + bool updateMaterial = false; + + ImGui::TableSetColumnIndex(1); + drawRectPreview(ptr, j); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Bounds"); + ImGui::TableSetColumnIndex(1); + + nlohmann::json &d = j[ptr]; + nlohmann::json &c = j[ptr]["color"]; + + float bounds[2] = { d["left"], d["top"] }; + if (ImGui::InputFloat("Left", &bounds[0], 0.1f, 0.1f, "%.1f")) { + d["left"] = std::clamp(std::round(bounds[0] * 10.0f) / + 10.0f, + 0.0f, 1.0f); + d["right"] = std::clamp( + std::round((bounds[0] + 0.1f) * 10.0f) / 10.0f, + 0.0f, 1.0f); + updateMaterial = true; + } + + if (ImGui::InputFloat("Top", &bounds[1], 0.1f, 0.1f, "%.1f")) { + d["top"] = std::clamp(std::round(bounds[1] * 10.0f) / + 10.0f, + 0.0f, 1.0f); + d["bottom"] = std::clamp( + std::round((bounds[1] + 0.1f) * 10.0f) / 10.0f, + 0.0f, 1.0f); + updateMaterial = true; + } + ImVec2 value; + value.x = bounds[0]; + value.y = bounds[1]; + if (DrawGridSelectorInTableCell("##selectRect", value)) { + bounds[0] = value.x; + bounds[1] = value.y; + d["left"] = std::clamp(std::round(bounds[0] * 10.0f) / + 10.0f, + 0.0f, 1.0f); + d["right"] = std::clamp( + std::round((bounds[0] + 0.1f) * 10.0f) / 10.0f, + 0.0f, 1.0f); + d["top"] = std::clamp(std::round(bounds[1] * 10.0f) / + 10.0f, + 0.0f, 1.0f); + d["bottom"] = std::clamp( + std::round((bounds[1] + 0.1f) * 10.0f) / 10.0f, + 0.0f, 1.0f); + updateMaterial = true; + } + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Color"); + ImGui::TableSetColumnIndex(1); + + float col[4] = { c["r"], c["g"], c["b"], c["a"] }; + if (ImGui::ColorEdit4("##color", col)) { + c["r"] = col[0]; + c["g"] = col[1]; + c["b"] = col[2]; + c["a"] = col[3]; + updateMaterial = true; + } + if (updateMaterial) { + StaticGeometryModule::setItemProperties(entity, + j.dump()); + Geometry::createTownMaterial(entity, true); + } + } + ColorRectPropertyEditor() + : DPropertyEditor(EditColorRect, "Color Rect Editor") + { + } + ~ColorRectPropertyEditor() override + { + } +}; +template struct registerPropertyEditor { + registerPropertyEditor(std::vector ®) + { + static T regdata; + reg.push_back(regdata); + } +}; +static std::vector propertyEditors; +// ViewJson editor (fallback) +struct FallbackPropertyEditor : DPropertyEditor { + bool _matches(const std::string &itemName, + const nlohmann::json::json_pointer &ptr, + const nlohmann::json &j) override + { + return true; + } + void _render(flecs::entity entity, + const nlohmann::json::json_pointer &ptr, + nlohmann::json &j) override + { + ImGui::TableSetColumnIndex(1); + ImGui::TextWrapped("%s", j[ptr].dump(4).c_str()); + } + FallbackPropertyEditor() + : DPropertyEditor(ViewJson, "JSON Viewer") + { + } +}; +static registerPropertyEditor + stringPropertyEditor(propertyEditors); +static registerPropertyEditor + luaScriptPropertyEditor(propertyEditors); +static registerPropertyEditor + multilineStringPropertyEditor(propertyEditors); +static registerPropertyEditor + floatPropertyEditor(propertyEditors); +static registerPropertyEditor + intPropertyEditor(propertyEditors); +static registerPropertyEditor + boolPropertyEditor(propertyEditors); +static registerPropertyEditor + colorRectPropertyEditor(propertyEditors); +static registerPropertyEditor + fallbackPropertyEditor(propertyEditors); +void itemsEditor() +{ + struct EditorState { + flecs::entity edited; + nlohmann::json::json_pointer edited_ptr; + int edited_type; + EditorState() + : edited_type(EditNone) + { + } + void clear() + { + edited = flecs::entity(); + edited_ptr = nlohmann::json::json_pointer(); + edited_type = EditNone; + } + }; + static EditorState state; + // Helper to find matching editor + auto findEditor = [](int editType, const std::string &itemName, + const nlohmann::json::json_pointer &ptr, + const nlohmann::json &j) -> PropertyEditor * { + for (auto &editor : propertyEditors) { + if (editor.editType == editType && + editor.matches(itemName, ptr, j)) { + return &editor; + } + } + return nullptr; + }; + // Leaf node detection - keeping your working logic + auto is_leaf = [](const std::string &itemName, + const nlohmann::json::json_pointer &ptr) -> bool { + // Direct key matches + if (itemName == "name" || itemName == "position" || + itemName == "orientation" || itemName == "rotation" || + itemName == "cells" || itemName == "furniture_cells" || + itemName == "roofs" || itemName == "pierPath" || + itemName == "cellScript") { + return true; + } + + // Parent-based matches + if (!ptr.empty() && !ptr.parent_pointer().empty()) { + std::string parent = ptr.parent_pointer().back(); + if (parent == "colorRects" || parent == "npcs" || + parent == "centerBuildings") { + return true; + } + } + + return false; + }; + // Edit type determination + auto set_edit_type = [&findEditor]( + const std::string &itemName, + const nlohmann::json::json_pointer &ptr, + const nlohmann::json &j) { + // Try to find by specific rules first + if (itemName == "name") { + state.edited_type = EditString; + } else if (itemName == "position" || + itemName == "orientation" || + itemName == "rotation" || itemName == "cells" || + itemName == "furniture_cells" || + itemName == "roofs" || itemName == "pierPath") { + state.edited_type = ViewJson; + } else if (itemName == "cellScript") { + state.edited_type = EditMultilineString; + } else if (!ptr.empty() && !ptr.parent_pointer().empty()) { + std::string parent = ptr.parent_pointer().back(); + if (parent == "colorRects") { + state.edited_type = EditColorRect; + } else if (parent == "npcs" || + parent == "centerBuildings") { + state.edited_type = ViewJson; + } else { + // Fall back to type-based detection + if (j[ptr].is_number_float()) + state.edited_type = EditFloat; + else if (j[ptr].is_number_integer()) + state.edited_type = EditInt; + else if (j[ptr].is_boolean()) + state.edited_type = EditBool; + else if (j[ptr].is_string()) + state.edited_type = EditString; + else + state.edited_type = ViewJson; + } + } else { + // Fall back to type-based detection + if (j[ptr].is_number_float()) + state.edited_type = EditFloat; + else if (j[ptr].is_number_integer()) + state.edited_type = EditInt; + else if (j[ptr].is_boolean()) + state.edited_type = EditBool; + else if (j[ptr].is_string()) + state.edited_type = EditString; + else + state.edited_type = ViewJson; + } + }; + // Recursive hierarchy renderer + std::function + renderHierarchy; + + renderHierarchy = [&renderHierarchy, &is_leaf, &set_edit_type]( + const Ogre::String &label, flecs::entity item, + ImGuiTreeNodeFlags flags, + const nlohmann::json::json_pointer &ptr, + nlohmann::json &j) { + // Generate a unique ID for this node + ImGui::PushID( + static_cast(reinterpret_cast(&j[ptr]))); + + // Set flags + ImGuiTreeNodeFlags nodeFlags = flags; + if (state.edited == item && state.edited_ptr == ptr) + nodeFlags |= ImGuiTreeNodeFlags_Selected; + + // Render the node + std::string displayLabel = label; + if (!ptr.empty() && j[ptr].is_string() && + ptr.back() == "name") { + // Optionally show value for name fields + displayLabel += ": " + j[ptr].get(); + } + + bool nodeOpen = ImGui::TreeNodeEx((void *)&j[ptr], nodeFlags, + "%s", displayLabel.c_str()); + + // Handle click + if (ImGui::IsItemClicked()) { + state.edited = item; + state.edited_ptr = ptr; + std::string itemName = ptr.empty() ? "" : ptr.back(); + set_edit_type(itemName, ptr, j); + std::cout << ptr << std::endl; + } + + // Render children + if (nodeOpen) { + std::string itemName = ptr.empty() ? "" : ptr.back(); + bool leaf = is_leaf(itemName, ptr); + + if (!leaf) { + if (j[ptr].is_array()) { + for (size_t i = 0; i < j[ptr].size(); + ++i) { + auto nptr = ptr / i; + renderHierarchy( + std::to_string(i), item, + flags, nptr, j); + } + } else if (j[ptr].is_object()) { + nlohmann::json &obj = j[ptr]; + for (auto it = obj.begin(); + it != obj.end(); ++it) { + auto nptr = ptr / it.key(); + renderHierarchy(it.key(), item, + flags, nptr, j); + } + } + } + + ImGui::TreePop(); + } + + ImGui::PopID(); + }; + // Property editor rendering using registry + auto renderPropertyEditor = + [&findEditor](flecs::entity entity, + const nlohmann::json::json_pointer &ptr, + int editType) { + Ogre::String jsonStr = + StaticGeometryModule::getItemProperties(entity); + nlohmann::json j = nlohmann::json::parse(jsonStr); + + std::string itemName = ptr.empty() ? "" : ptr.back(); + PropertyEditor *editor = + findEditor(editType, itemName, ptr, j); + + if (editor) { + editor->render(entity, ptr, j); + } else { + // Fallback + ImGui::TableSetColumnIndex(1); + ImGui::TextWrapped("%s", + j[ptr].dump(4).c_str()); + } + }; + if (ImGui::SmallButton("Edit items...")) + ImGui::OpenPopup("Edit Items Popup"); + if (ImGui::BeginPopupModal("Edit Items Popup", NULL, + ImGuiWindowFlags_MenuBar)) { + // Optional: Add a menu bar for "Add District/Lot" actions + if (ImGui::BeginMenuBar()) { + if (ImGui::BeginMenu("File")) { /* ... */ + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + + // --- LEFT SIDE: HIERARCHY --- + ImGui::BeginChild("HierarchyRegion", ImVec2(300, 400), true); + std::list > items; + StaticGeometryModule::getItemsProperties(&items); + ImGuiTreeNodeFlags baseFlags = + ImGuiTreeNodeFlags_OpenOnArrow | + ImGuiTreeNodeFlags_SpanAvailWidth | + ImGuiTreeNodeFlags_DefaultOpen; + for (const auto &itemPair : items) { + flecs::entity entity = itemPair.first; + const Ogre::String &jsonStr = itemPair.second; + + ImGui::PushID(static_cast(entity.id())); + + nlohmann::json j = nlohmann::json::parse(jsonStr); + + // Create label + Ogre::String label = + Ogre::StringConverter::toString(entity.id()); + label += ":" + j["type"].get(); + if (j.find("name") != j.end() && + !j["name"].get().empty()) + label = j["name"].get(); + + // Render hierarchy for this item + renderHierarchy(label, entity, baseFlags, + nlohmann::json::json_pointer(), j); + + ImGui::PopID(); + } + ImGui::EndChild(); + + ImGui::SameLine(); + + // --- RIGHT SIDE: PROPERTIES --- + ImGui::BeginChild("PropertiesRegion", ImVec2(0, 400), true); + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, + ImVec2(10.0f, 4.0f)); + if (ImGui::BeginTable("PropTable", 2, + ImGuiTableFlags_SizingStretchProp | + ImGuiTableFlags_Resizable)) { + ImGui::TableSetupColumn( + "Property", ImGuiTableColumnFlags_WidthFixed, + 0.0f); + ImGui::TableSetupColumn( + "Value", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableHeadersRow(); + ImGui::TableNextRow(); + if (state.edited.is_valid() && + !state.edited_ptr.empty()) { + renderPropertyEditor(state.edited, + state.edited_ptr, + state.edited_type); + } else if (state.edited.is_valid()) { + // Show item summary when no specific property is selected + ImGui::TableSetColumnIndex(0); + ImGui::Text("Item"); + ImGui::TableSetColumnIndex(1); + Ogre::String jsonStr = + StaticGeometryModule::getItemProperties( + state.edited); + nlohmann::json j = + nlohmann::json::parse(jsonStr); + ImGui::TextWrapped("%s", j.dump(4).c_str()); + } + ImGui::EndTable(); + } + ImGui::PopStyleVar(); + // DrawPropertyInspector(); // Uses the Table logic from before + ImGui::EndChild(); + + if (ImGui::Button("Close")) { + ImGui::CloseCurrentPopup(); + state.clear(); + } + ImGui::EndPopup(); + } +} +#endif } namespace Geometry { diff --git a/src/gamedata/items/items.h b/src/gamedata/items/items.h index bc0ab71..947e30a 100644 --- a/src/gamedata/items/items.h +++ b/src/gamedata/items/items.h @@ -12,6 +12,7 @@ void showItemPopup(const std::pair &item); void showItemButtons(const std::pair &item); void createItemsMenu(); void runScriptsForAllTowns(); +void itemsEditor(); } namespace Geometry { diff --git a/src/gamedata/items/town.cpp b/src/gamedata/items/town.cpp index dcc571d..88e8bf4 100644 --- a/src/gamedata/items/town.cpp +++ b/src/gamedata/items/town.cpp @@ -4267,6 +4267,647 @@ struct TownCells : TownTask { struct TownRoofs : TownTask { std::shared_future townRoofsComplete; + +private: + // Constants + static constexpr float CELL_WIDTH = 2.0f; + static constexpr float CELL_HEIGHT = 4.0f; + static constexpr float CELL_DEPTH = 2.0f; + static constexpr float EXTEND = 0.24f; + static constexpr float SQRT2 = 1.4142f; + struct DistrictContext { + Ogre::Vector3 centerPosition; + Ogre::Quaternion centerOrientation; + float radius; + }; + struct LotContext { + int width, depth; + float angle; + float elevation; + nlohmann::json jlot; + Ogre::Vector3 worldPosition; + Ogre::Quaternion worldOrientation; + }; + struct RoofPlacement { + int type; + int posX, posY, posZ; + int sizeX, sizeZ; + float baseHeight, maxHeight; + }; + struct RoofGenerationData { + flecs::entity entity; + Ogre::MaterialPtr material; + Ogre::StaticGeometry *geo; + Ogre::Vector3 districtPosition; + Ogre::Quaternion districtOrientation; + float districtRadius; + std::string baseMeshName; + }; + DistrictContext parseDistrictContext(const nlohmann::json &jp, + Ogre::SceneNode *sceneNode) + { + DistrictContext ctx; + + // Parse base position and rotation + Ogre::Vector3 localPosition(0, 0, 0); + Ogre::Quaternion localRotation = Ogre::Quaternion::IDENTITY; + from_json(jp["position"], localPosition); + from_json(jp["rotation"], localRotation); + + // Parse elevation and radius + float elevation = jp.value("elevation", 0.0f); + ctx.radius = jp.value("radius", 50.0f); + + // Calculate world transforms for district center + ctx.centerPosition = sceneNode->_getDerivedPosition() + + localPosition + + Ogre::Vector3(0, elevation, 0); + ctx.centerOrientation = + sceneNode->_getDerivedOrientation() * localRotation; + + return ctx; + } + LotContext createLotContext(const DistrictContext &districtCtx, + const nlohmann::json &jlot, int lotIndex) + { + LotContext lot; + lot.jlot = jlot; + + // Parse lot properties + lot.width = jlot.value("width", 10); + lot.depth = jlot.value("depth", 10); + lot.angle = jlot.value("angle", 0.0f); + lot.elevation = jlot.value("elevation", 0.0f); + + OgreAssert(lot.width > 1 && lot.depth > 1, + "Invalid lot dimensions"); + + // Calculate lot world transform + Ogre::Quaternion lotRotation(Ogre::Degree(lot.angle), + Ogre::Vector3::UNIT_Y); + Ogre::Vector3 offset = + districtCtx.centerOrientation * lotRotation * + (Ogre::Vector3::UNIT_Z * districtCtx.radius); + + lot.worldPosition = districtCtx.centerPosition + offset; + lot.worldOrientation = + districtCtx.centerOrientation * lotRotation; + + return lot; + } + std::vector + parseRoofPlacements(const nlohmann::json &jlot) + { + std::vector roofs; + + if (!jlot.contains("roofs") || !jlot["roofs"].is_array()) { + return roofs; + } + + for (const auto &jroof : jlot["roofs"]) { + RoofPlacement roof; + roof.type = jroof["type"].get(); + roof.posX = jroof["position_x"].get(); + roof.posY = jroof["position_y"].get(); + roof.posZ = jroof["position_z"].get(); + roof.sizeX = jroof["size_x"].get(); + roof.sizeZ = jroof["size_z"].get(); + roof.baseHeight = jroof["base_height"].get(); + roof.maxHeight = jroof["max_height"].get(); + + roofs.push_back(roof); + } + + return roofs; + } + + Ogre::Vector3 calculateLocalPosition(const RoofPlacement &roof) + { + float x = static_cast(roof.posX) * CELL_WIDTH - 1.0f + + static_cast(roof.sizeX) * CELL_WIDTH / 2.0f; + float z = static_cast(roof.posZ) * CELL_DEPTH - 1.0f + + static_cast(roof.sizeZ) * CELL_DEPTH / 2.0f; + return Ogre::Vector3(x, 0.0f, z); + } + void transformToWorld(Procedural::TriangleBuffer &tb, + const LotContext &lotCtx) + { + if (tb.getVertices().empty()) + return; + + std::vector &vertices = + tb.getVertices(); + for (auto &v : vertices) { + // Apply lot rotation to local position + Ogre::Vector3 rotatedPos = + lotCtx.worldOrientation * v.mPosition; + // Add lot world position and elevation + v.mPosition = lotCtx.worldPosition + rotatedPos + + Ogre::Vector3(0, lotCtx.elevation, 0); + + // Rotate normals + v.mNormal = lotCtx.worldOrientation * v.mNormal; + } + } + void generateRoofType0(const RoofPlacement &roof, + const LotContext &lotCtx, + Procedural::TriangleBuffer &tbTop, + Procedural::TriangleBuffer &tbSide) + { + float width = static_cast(roof.sizeX) * CELL_WIDTH; + float depth = static_cast(roof.sizeZ) * CELL_DEPTH; + Ogre::Vector3 localPos = calculateLocalPosition(roof); + + // Type 0 Y calculation from original code: + // (float)position_y * 4.0f + baseHeight / 2.0f + lot.elevation + float baseY = static_cast(roof.posY) * CELL_HEIGHT + + roof.baseHeight / 2.0f; + + // Main roof box - position already includes baseHeight/2 + Procedural::BoxGenerator() + .setSizeX(width) + .setSizeY(roof.baseHeight) + .setSizeZ(depth) + .setNumSegY(1) + .setNumSegX(1) + .setNumSegZ(1) + .setEnableNormals(true) + .setPosition( + Ogre::Vector3(localPos.x, baseY, localPos.z)) + .addToTriangleBuffer(tbTop); + + // Edge trim pieces - Y calculation from original: + // (float)position_y * 4.0f + baseHeight / 2.0f + lot.elevation + extend + float trimBaseY = static_cast(roof.posY) * CELL_HEIGHT + + roof.baseHeight / 2.0f; + + // Z+ edge + Procedural::BoxGenerator() + .setSizeX(width) + .setSizeY(roof.baseHeight + EXTEND * 2.0f) + .setSizeZ(EXTEND) + .setNumSegY(1) + .setNumSegX(1) + .setNumSegZ(1) + .setEnableNormals(true) + .setPosition(Ogre::Vector3( + localPos.x, trimBaseY + EXTEND, + localPos.z + depth / 2.0f + EXTEND / 2.0f)) + .addToTriangleBuffer(tbSide); + + // Z- edge + Procedural::BoxGenerator() + .setSizeX(width) + .setSizeY(roof.baseHeight + EXTEND * 2.0f) + .setSizeZ(EXTEND) + .setNumSegY(1) + .setNumSegX(1) + .setNumSegZ(1) + .setEnableNormals(true) + .setPosition(Ogre::Vector3( + localPos.x, trimBaseY + EXTEND, + localPos.z - depth / 2.0f - EXTEND / 2.0f)) + .addToTriangleBuffer(tbSide); + + // X+ edge + Procedural::BoxGenerator() + .setSizeX(EXTEND) + .setSizeY(roof.baseHeight + EXTEND * 2.0f) + .setSizeZ(depth + EXTEND * 2.0f) + .setNumSegY(1) + .setNumSegX(1) + .setNumSegZ(1) + .setEnableNormals(true) + .setPosition(Ogre::Vector3( + localPos.x + width / 2.0f + EXTEND / 2.0f, + trimBaseY + EXTEND, localPos.z)) + .addToTriangleBuffer(tbSide); + + // X- edge + Procedural::BoxGenerator() + .setSizeX(EXTEND) + .setSizeY(roof.baseHeight + EXTEND * 2.0f) + .setSizeZ(depth + EXTEND * 2.0f) + .setNumSegY(1) + .setNumSegX(1) + .setNumSegZ(1) + .setEnableNormals(true) + .setPosition(Ogre::Vector3( + localPos.x - width / 2.0f - EXTEND / 2.0f, + trimBaseY + EXTEND, localPos.z)) + .addToTriangleBuffer(tbSide); + } + void generateRoofType1(const RoofPlacement &roof, + const LotContext &lotCtx, + Procedural::TriangleBuffer &tbTop, + Procedural::TriangleBuffer &tbSide) + { + float width = static_cast(roof.sizeX) * CELL_WIDTH; + float depth = static_cast(roof.sizeZ) * CELL_DEPTH; + float q = width / 2.0f; + float m = 1.0f; + float d = SQRT2 * (q + m); + + Ogre::Vector3 localPos = calculateLocalPosition(roof); + float baseY = static_cast(roof.posY) * CELL_HEIGHT; + + // First angled box + Procedural::BoxGenerator() + .setSizeX(d) + .setSizeY(roof.baseHeight / 2.0f) + .setSizeZ(depth) + .setNumSegY(1) + .setNumSegX(1) + .setNumSegZ(1) + .setEnableNormals(true) + .setOrientation(Ogre::Quaternion( + Ogre::Degree(45), + Ogre::Vector3::NEGATIVE_UNIT_Z)) + .setPosition(Ogre::Vector3( + localPos.x - width / 2.0f + q + q / 2.0f + + m / 2.0f, + baseY + roof.baseHeight + q / 2.0f - m / 2.0f, + localPos.z)) + .addToTriangleBuffer(tbTop); + + // Second angled box + Procedural::BoxGenerator() + .setSizeX(d) + .setSizeY(roof.baseHeight / 2.0f) + .setSizeZ(depth) + .setNumSegY(1) + .setNumSegX(1) + .setNumSegZ(1) + .setEnableNormals(true) + .setOrientation(Ogre::Quaternion( + Ogre::Degree(-45), + Ogre::Vector3::NEGATIVE_UNIT_Z)) + .setPosition(Ogre::Vector3( + localPos.x - width / 2.0f + q / 2.0f - m / 2.0f, + baseY + roof.baseHeight + q / 2.0f - m / 2.0f, + localPos.z)) + .addToTriangleBuffer(tbTop); + + // Side trim pieces + float sideY = baseY + q / 2.0f; + float sideHeight = q + roof.baseHeight * 3.0f; + + // Z+ edge + Procedural::BoxGenerator() + .setSizeX(width + EXTEND * 2.0f) + .setSizeY(sideHeight) + .setSizeZ(EXTEND) + .setNumSegX(3) + .setNumSegY(3) + .setNumSegZ(1) + .setEnableNormals(true) + .setPosition(Ogre::Vector3( + localPos.x, sideY + roof.baseHeight, + localPos.z + depth / 2.0f + EXTEND / 2.0f)) + .addToTriangleBuffer(tbSide); + + // Z- edge + Procedural::BoxGenerator() + .setSizeX(width + EXTEND * 2.0f) + .setSizeY(sideHeight) + .setSizeZ(EXTEND) + .setNumSegX(3) + .setNumSegY(3) + .setNumSegZ(1) + .setEnableNormals(true) + .setPosition(Ogre::Vector3( + localPos.x, sideY + roof.baseHeight, + localPos.z - depth / 2.0f - EXTEND / 2.0f)) + .addToTriangleBuffer(tbSide); + + // Apply vertex deformation + deformSideVertices(tbSide, true); + } + void generateRoofType2(const RoofPlacement &roof, + const LotContext &lotCtx, + Procedural::TriangleBuffer &tbTop, + Procedural::TriangleBuffer &tbSide) + { + float width = static_cast(roof.sizeX) * CELL_WIDTH; + float depth = static_cast(roof.sizeZ) * CELL_DEPTH; + float q = depth / 2.0f; + float m = 1.0f; + float d = SQRT2 * (q + m); + + Ogre::Vector3 localPos = calculateLocalPosition(roof); + float baseY = static_cast(roof.posY) * CELL_HEIGHT; + + // First angled box + Procedural::BoxGenerator() + .setSizeX(width) + .setSizeY(roof.baseHeight / 2.0f) + .setSizeZ(d) + .setNumSegY(1) + .setNumSegX(1) + .setNumSegZ(1) + .setEnableNormals(true) + .setOrientation(Ogre::Quaternion( + Ogre::Degree(45), + Ogre::Vector3::NEGATIVE_UNIT_X)) + .setPosition(Ogre::Vector3(localPos.x, + baseY + roof.baseHeight + + q / 2.0f - m / 2.0f, + localPos.z - depth / 2.0f + + q / 2.0f - m / 2.0f)) + .addToTriangleBuffer(tbTop); + + // Second angled box + Procedural::BoxGenerator() + .setSizeX(width) + .setSizeY(roof.baseHeight / 2.0f) + .setSizeZ(d) + .setNumSegX(1) + .setNumSegY(1) + .setNumSegZ(1) + .setEnableNormals(true) + .setOrientation(Ogre::Quaternion( + Ogre::Degree(-45), + Ogre::Vector3::NEGATIVE_UNIT_X)) + .setPosition(Ogre::Vector3( + localPos.x, + baseY + roof.baseHeight + q / 2.0f - m / 2.0f, + localPos.z - depth / 2.0f + q + q / 2.0f + + m / 2.0f)) + .addToTriangleBuffer(tbTop); + + // Side trim pieces + float sideY = baseY + q / 2.0f; + float sideHeight = q + roof.baseHeight * 3.0f; + float sideDepth = depth + 2.0f * roof.baseHeight * 3.0f; + + // X+ edge + Procedural::BoxGenerator() + .setSizeX(EXTEND) + .setSizeY(sideHeight) + .setSizeZ(sideDepth) + .setNumSegX(2) + .setNumSegY(2) + .setNumSegZ(1) + .setEnableNormals(true) + .setPosition(Ogre::Vector3( + localPos.x + width / 2.0f + EXTEND / 2.0f, + sideY + roof.baseHeight, localPos.z)) + .addToTriangleBuffer(tbSide); + + // X- edge + Procedural::BoxGenerator() + .setSizeX(EXTEND) + .setSizeY(sideHeight) + .setSizeZ(sideDepth) + .setNumSegX(2) + .setNumSegY(2) + .setNumSegZ(1) + .setEnableNormals(true) + .setPosition(Ogre::Vector3( + localPos.x - width / 2.0f - EXTEND / 2.0f, + sideY + roof.baseHeight, localPos.z)) + .addToTriangleBuffer(tbSide); + + // Apply vertex deformation + deformSideVertices(tbSide, false); + } + void deformSideVertices(Procedural::TriangleBuffer &tbSide, + bool deformXAxis) + { + if (tbSide.getVertices().empty()) + return; + + float minY = std::numeric_limits::max(); + float maxY = std::numeric_limits::lowest(); + float minCoord = std::numeric_limits::max(); + float maxCoord = std::numeric_limits::lowest(); + + for (const auto &v : tbSide.getVertices()) { + minY = std::min(minY, v.mPosition.y); + maxY = std::max(maxY, v.mPosition.y); + float coord = deformXAxis ? v.mPosition.x : + v.mPosition.z; + minCoord = std::min(minCoord, coord); + maxCoord = std::max(maxCoord, coord); + } + + float midCoord = minCoord + (maxCoord - minCoord) / 2.0f; + float coordRange = (maxCoord - minCoord) / 2.0f; + if (coordRange < 0.001f) + return; + + for (auto &v : tbSide.getVertices()) { + float md = maxY - v.mPosition.y; + float hm = + (deformXAxis ? v.mPosition.x : v.mPosition.z) - + midCoord; + float he = (hm / coordRange) * md; + + if (deformXAxis) { + v.mPosition.x = he + midCoord; + } else { + v.mPosition.z = he + midCoord; + } + } + } + void processLot(flecs::entity e, const LotContext &lotCtx, + const RoofGenerationData &genData, + Procedural::TriangleBuffer &tbCombined) + { + std::vector roofs = + parseRoofPlacements(lotCtx.jlot); + + for (auto &roof : roofs) { + Procedural::TriangleBuffer tbTop; + Procedural::TriangleBuffer tbSide; + + // Generate roof in local space + switch (roof.type) { + case 0: + generateRoofType0(roof, lotCtx, tbTop, tbSide); + break; + case 1: + generateRoofType1(roof, lotCtx, tbTop, tbSide); + break; + case 2: + generateRoofType2(roof, lotCtx, tbTop, tbSide); + break; + default: + continue; + } + + // Transform to world space + if (tbTop.getVertices().size() > 0) { + transformToWorld(tbTop, lotCtx); + clampUV(e, tbTop, "roofTop"); + tbCombined.append(tbTop); + } + if (tbSide.getVertices().size() > 0) { + transformToWorld(tbSide, lotCtx); + clampUV(e, tbSide, "roofSide"); + tbCombined.append(tbSide); + } + } + } + +public: + void createTownRoofs(flecs::entity e, const nlohmann::json &jdistrict, + int index, Ogre::SceneNode *sceneNode, + Ogre::StaticGeometry *geo) + { + ZoneScoped; + + if (townRoofsComplete.valid() && + townRoofsComplete.wait_for(std::chrono::seconds(0)) != + std::future_status::ready) { + townRoofsComplete.wait(); + } + + auto promise = std::make_shared >(); + townRoofsComplete = promise->get_future(); + + Ogre::MaterialPtr townMaterial = + Ogre::MaterialManager::getSingleton().getByName( + "proceduralMaterialTown" + + Ogre::StringConverter::toString(e.id())); + + DistrictContext districtCtx = + parseDistrictContext(jdistrict, sceneNode); + + RoofGenerationData genData{ + .entity = e, + .material = townMaterial, + .geo = geo, + .districtPosition = districtCtx.centerPosition, + .districtOrientation = districtCtx.centerOrientation, + .districtRadius = districtCtx.radius, + .baseMeshName = + "roofbase_" + + Ogre::StringConverter::toString(e.id()) + "_" + + Ogre::StringConverter::toString(index) + }; + + Ogre::Root::getSingleton().getWorkQueue()->addTask([this, + jdistrict, + districtCtx, + genData, + promise]() { + ZoneScoped; + + std::vector > + lotBuffers; + + if (jdistrict.contains("lots") && + jdistrict["lots"].is_array()) { + const nlohmann::json &lots = jdistrict["lots"]; + int lotIndex = 0; + + for (const auto &jlot : lots) { + if (!jlot.contains("roofs")) + continue; + + LotContext lotCtx = createLotContext( + districtCtx, jlot, lotIndex); + Procedural::TriangleBuffer tbCombined; + + processLot(genData.entity, lotCtx, + genData, tbCombined); + + if (tbCombined.getVertices().size() > + 0) { + lotBuffers.emplace_back( + lotCtx, tbCombined); + } + + lotIndex++; + } + } + + Ogre::Root::getSingleton() + .getWorkQueue() + ->addMainThreadTask([this, genData, lotBuffers, + promise]() { + ZoneScopedN( + "createTownRoofs::MainThread"); + + for (size_t i = 0; + i < lotBuffers.size(); ++i) { + std::cout + << i << ": vertices: " + << lotBuffers[i] + .second + .getVertices() + .size() + << std::endl; + } + + int lotIndex = 0; + for (const auto &[lotCtx, tb] : + lotBuffers) { + std::string meshName = + genData.baseMeshName + + "_" + + std::to_string( + lotIndex); + + Ogre::MeshPtr existingMesh = + Ogre::MeshManager::getSingleton() + .getByName( + meshName); + if (existingMesh) { + Ogre::MeshManager::getSingleton() + .remove(existingMesh); + } + + // Create mesh (vertices are already in world space) + Ogre::MeshPtr mesh = + tb.transformToMesh( + meshName); + Ogre::LodConfig config(mesh); + setupLods(config); + + // Add to static geometry (position at origin since vertices are in world space) + addMeshToStatic( + genData.geo, mesh, + genData.material, + Ogre::Vector3::ZERO, + Ogre::Quaternion:: + IDENTITY); + + addStaticBodyMesh( + genData.entity, mesh, + Ogre::Vector3::ZERO, + Ogre::Quaternion:: + IDENTITY); + + lotIndex++; + } + }); + + promise->set_value(true); + }); + } + void operator()(flecs::entity e, const nlohmann::json &jdistrict, + int index, Ogre::SceneNode *sceneNode, + Ogre::StaticGeometry *geo) + { + ZoneScoped; + createTownRoofs(e, jdistrict, index, sceneNode, geo); + } + + void wait() + { + ZoneScoped; + if (townRoofsComplete.valid() && + townRoofsComplete.wait_for(std::chrono::seconds(0)) != + std::future_status::ready) { + townRoofsComplete.wait(); + } + } + +#if 0 + void createTownRoofs(flecs::entity e, const nlohmann::json &jdistrict, int index, Ogre::SceneNode *sceneNode, Ogre::StaticGeometry *geo) @@ -5119,237 +5760,405 @@ struct TownRoofs : TownTask { promise->set_value(true); }); } - void operator()(flecs::entity e, const nlohmann::json &jdistrict, - int index, Ogre::SceneNode *sceneNode, - Ogre::StaticGeometry *geo) - { - ZoneScoped; - createTownRoofs(e, jdistrict, index, sceneNode, geo); - } - void wait() +#else +#if 0 + void createTownRoofs(flecs::entity e, const nlohmann::json &jdistrict, + int index, Ogre::SceneNode *sceneNode, + Ogre::StaticGeometry *geo) { ZoneScoped; + + // Wait for previous task if (townRoofsComplete.valid() && townRoofsComplete.wait_for(std::chrono::seconds(0)) != - std::future_status::ready) + std::future_status::ready) { townRoofsComplete.wait(); + } + + // Setup async task + auto promise = std::make_shared >(); + townRoofsComplete = promise->get_future(); + + // Get town material + Ogre::MaterialPtr townMaterial = + Ogre::MaterialManager::getSingleton().getByName( + "proceduralMaterialTown" + + Ogre::StringConverter::toString(e.id())); + + // Parse district context + DistrictContext districtCtx = + parseDistrictContext(jdistrict, sceneNode); + + // Prepare roof generation data + RoofGenerationData genData{ + .entity = e, + .material = townMaterial, + .geo = geo, + .districtPosition = districtCtx.centerPosition, + .districtOrientation = districtCtx.centerOrientation, + .districtRadius = districtCtx.radius, + .baseMeshName = + "roofbase_" + + Ogre::StringConverter::toString(e.id()) + "_" + + Ogre::StringConverter::toString(index) + }; + + // Process lots in background + Ogre::Root::getSingleton().getWorkQueue()->addTask([this, + jdistrict, + districtCtx, + genData, + promise]() { + ZoneScoped; + + std::vector > + lotBuffers; + + if (jdistrict.contains("lots") && + jdistrict["lots"].is_array()) { + const nlohmann::json &lots = jdistrict["lots"]; + int lotIndex = 0; + + for (const auto &jlot : lots) { + if (!jlot.contains("roofs")) + continue; + + LotContext lotCtx = createLotContext( + districtCtx, jlot, lotIndex); + Procedural::TriangleBuffer tbCombined; + + processLot(genData.entity, lotCtx, + genData, tbCombined); + + if (tbCombined.getVertices().size() > + 0) { + lotBuffers.emplace_back( + lotCtx, tbCombined); + } + + lotIndex++; + } + } + + // Schedule main thread task for mesh creation + Ogre::Root::getSingleton() + .getWorkQueue() + ->addMainThreadTask([this, genData, lotBuffers, + promise]() { + ZoneScopedN( + "createTownRoofs::MainThread"); + + // Debug output + for (size_t i = 0; + i < lotBuffers.size(); ++i) { + std::cout + << i << ": vertices: " + << lotBuffers[i] + .second + .getVertices() + .size() + << std::endl; + } + + // Create meshes and add to static geometry + int lotIndex = 0; + for (const auto &[lotCtx, tb] : + lotBuffers) { + std::string meshName = + genData.baseMeshName + + "_" + + std::to_string( + lotIndex); + + // Remove existing mesh if any + Ogre::MeshPtr existingMesh = + Ogre::MeshManager::getSingleton() + .getByName( + meshName); + if (existingMesh) { + Ogre::MeshManager::getSingleton() + .remove(existingMesh); + } + + // Create new mesh + Ogre::MeshPtr mesh = + tb.transformToMesh( + meshName); + Ogre::LodConfig config(mesh); + setupLods(config); + + // Calculate final world transform + Ogre::Quaternion lotRotation( + Ogre::Degree( + lotCtx.angle), + Ogre::Vector3::UNIT_Y); + Ogre::Vector3 offset = + genData.districtOrientation * + lotRotation * + (Ogre::Vector3::UNIT_Z * + genData.districtRadius); + + // Add to static geometry and physics + addMeshToStatic( + genData.geo, mesh, + genData.material, + genData.districtPosition + + offset, + genData.districtOrientation * + lotRotation); + + addStaticBodyMesh( + genData.entity, mesh, + genData.districtPosition + + offset, + genData.districtOrientation * + lotRotation); + + lotIndex++; + } + }); + + promise->set_value(true); + }); } +#endif + +#endif }; struct TownDecorateWindows : TownTask { std::shared_future townDecorateWindowsComplete; + +private: + struct DistrictContext { + Ogre::Vector3 centerPosition; + Ogre::Quaternion centerOrientation; + float radius; + }; + struct LotContext { + Ogre::Vector3 worldPosition; + Ogre::Quaternion worldOrientation; + }; + struct CellInfo { + int x, y, z; + uint64_t flags; + Ogre::Vector3 offsetX; + Ogre::Vector3 offsetY; + Ogre::Vector3 offsetZ; + }; + enum class WindowDirection { + PositiveZ, + NegativeZ, + PositiveX, + NegativeX + }; + + struct WindowPlacement { + WindowDirection direction; + Ogre::Vector3 position; + Ogre::Quaternion orientation; + bool valid; + }; + + // Constants + static constexpr float OUT_OFFSET = 1.05f; + static constexpr float WINDOW_VERTICAL_OFFSET = 0.8f; + static constexpr float CELL_WIDTH = 2.0f; + static constexpr float CELL_HEIGHT = 4.0f; + static constexpr float CELL_DEPTH = 2.0f; + DistrictContext parseDistrictContext(const nlohmann::json &jp, + Ogre::SceneNode *sceneNode) + { + DistrictContext ctx; + + // Parse base position and rotation + Ogre::Vector3 localPosition(0, 0, 0); + Ogre::Quaternion localRotation = Ogre::Quaternion::IDENTITY; + from_json(jp["position"], localPosition); + from_json(jp["rotation"], localRotation); + + // Parse elevation and radius + float elevation = jp.value("elevation", 0.0f); + ctx.radius = jp.value("radius", 50.0f); + + // Calculate world transforms for district center + ctx.centerPosition = sceneNode->_getDerivedPosition() + + localPosition + + Ogre::Vector3(0, elevation, 0); + ctx.centerOrientation = + sceneNode->_getDerivedOrientation() * localRotation; + + return ctx; + } + LotContext createLotContext(const DistrictContext &districtCtx, + const nlohmann::json &jlot) + { + LotContext lot; + + // Parse lot properties + float angle = jlot.value("angle", 0.0f); + float elevation = jlot.value("elevation", 0.0f); + + // Calculate lot rotation and position (offset from district center) + Ogre::Quaternion lotRotation(Ogre::Degree(angle), + Ogre::Vector3::UNIT_Y); + Ogre::Vector3 offset = + districtCtx.centerOrientation * lotRotation * + (Ogre::Vector3::UNIT_Z * districtCtx.radius); + + // Lot world position = district center + offset + lot elevation + lot.worldPosition = districtCtx.centerPosition + offset + + Ogre::Vector3(0, elevation, 0); + + // Lot orientation = district orientation * lot rotation + lot.worldOrientation = + districtCtx.centerOrientation * lotRotation; + + return lot; + } + CellInfo createCellInfo(const nlohmann::json &jcell, + const LotContext &lotCtx) + { + CellInfo cell; + cell.x = jcell.value("x", 0); + cell.y = jcell.value("y", 0); + cell.z = jcell.value("z", 0); + cell.flags = jcell.value("flags", 0ULL); + + // Calculate cell offsets + cell.offsetX = lotCtx.worldOrientation * Ogre::Vector3::UNIT_X * + static_cast(cell.x) * CELL_WIDTH; + cell.offsetZ = lotCtx.worldOrientation * Ogre::Vector3::UNIT_Z * + static_cast(cell.z) * CELL_DEPTH; + cell.offsetY = Ogre::Vector3(0, cell.y * CELL_HEIGHT, 0); + + return cell; + } + std::vector + checkForWindows(const nlohmann::json &jcell, const CellInfo &cell, + const LotContext &lotCtx) + { + std::vector windows; + + // Helper lambda to create window placement + auto addWindow = [&](WindowDirection dir, + const Ogre::Vector3 &offsetDir, + float rotationDegrees) { + WindowPlacement window; + window.direction = dir; + window.valid = true; + + // Calculate offset with vertical adjustment + Ogre::Vector3 offset = + lotCtx.worldOrientation * offsetDir * + OUT_OFFSET + + Ogre::Vector3::UNIT_Y * WINDOW_VERTICAL_OFFSET; + + window.position = lotCtx.worldPosition + cell.offsetX + + cell.offsetZ + cell.offsetY + offset; + + window.orientation = + lotCtx.worldOrientation * + Ogre::Quaternion(Ogre::Degree(rotationDegrees), + Ogre::Vector3::UNIT_Y); + windows.push_back(window); + }; + + // Check each window direction using the original jcell JSON object + if (Items::CellsScript::isBit(jcell, "windowz+")) { + addWindow(WindowDirection::PositiveZ, + Ogre::Vector3::UNIT_Z, 0.0f); + } + if (Items::CellsScript::isBit(jcell, "windowz-")) { + addWindow(WindowDirection::NegativeZ, + Ogre::Vector3::NEGATIVE_UNIT_Z, 180.0f); + } + if (Items::CellsScript::isBit(jcell, "windowx+")) { + addWindow(WindowDirection::PositiveX, + Ogre::Vector3::UNIT_X, 90.0f); + } + if (Items::CellsScript::isBit(jcell, "windowx-")) { + addWindow(WindowDirection::NegativeX, + Ogre::Vector3::NEGATIVE_UNIT_X, -90.0f); + } + + return windows; + } + + void placeWindow(Ogre::StaticGeometry *geo, + const WindowPlacement &window, + Ogre::MaterialPtr townMaterial) + { + // Create window entity + Ogre::Entity *ent = + ECS::get().mScnMgr->createEntity( + "window-cell", "window-frame1"); + ent->setMaterial(townMaterial); + + // Add to static geometry + geo->addEntity(ent, window.position, window.orientation); + + // Clean up + ECS::get().mScnMgr->destroyEntity(ent); + } + void processLot(flecs::entity e, const nlohmann::json &jlot, + const DistrictContext &districtCtx, + Ogre::StaticGeometry *geo, + Ogre::MaterialPtr townMaterial) + { + // Validate lot dimensions + int depth = jlot.value("depth", 10); + int width = jlot.value("width", 10); + OgreAssert(width > 1 && depth > 1, "Invalid lot dimensions"); + + // Create lot context with position offset from district center + LotContext lotCtx = createLotContext(districtCtx, jlot); + + // Process cells + if (jlot.contains("cells") && jlot["cells"].is_array()) { + const nlohmann::json &cells = jlot["cells"]; + for (nlohmann::json::const_iterator it = cells.begin(); + it != cells.end(); ++it) { + const nlohmann::json &jcell = *it; + + CellInfo cell = createCellInfo(jcell, lotCtx); + std::vector windows = + checkForWindows(jcell, cell, lotCtx); + + for (const auto &window : windows) { + placeWindow(geo, window, townMaterial); + } + } + } + } + +public: void createDecorateWindows(flecs::entity e, - const nlohmann::json &jdistrict, int index, - Ogre::SceneNode *sceneNode, + const nlohmann::json &jdistrict, + int /*index*/, Ogre::SceneNode *sceneNode, Ogre::StaticGeometry *geo) { ZoneScoped; - // FIXME: make furniture placement a background task. + // FIXME: make window placement a background task. + + // Create town material Ogre::MaterialPtr townMaterial = createTownMaterial(e); - const nlohmann::json &jp = jdistrict; - nlohmann::json jlots = nlohmann::json::array(); - float baseHeight = 4.0f; - Ogre::Vector3 localPosition(0, 0, 0); - Ogre::Quaternion localRotation = Ogre::Quaternion::IDENTITY; - Ogre::Vector3 centerPosition = sceneNode->_getDerivedPosition(); - Ogre::Quaternion centerOrientation = - sceneNode->_getDerivedOrientation(); - float delevation = 0.0f; - float radius = 50.0f; - if (jp.find("elevation") != jp.end()) - delevation = jp["elevation"].get(); - if (jp.find("radius") != jp.end()) - radius = jp["radius"].get(); - from_json(jp["position"], localPosition); - from_json(jp["rotation"], localRotation); - centerPosition = centerPosition + localPosition + - Ogre::Vector3(0, delevation, 0); - centerOrientation = centerOrientation * localRotation; - if (jdistrict.find("lots") != jdistrict.end()) - jlots = jdistrict["lots"]; - for (const auto &jb : jlots) { - float angle = 0.0f; - int depth = 10; - int width = 10; - float distance = radius; - float elevation = 0.0f; - std::cout << jb.dump() << std::endl; - if (jb.find("angle") != jb.end()) - angle = jb["angle"].get(); - if (jb.find("depth") != jb.end()) - depth = jb["depth"].get(); - if (jb.find("width") != jb.end()) - width = jb["width"].get(); - if (jb.find("elevation") != jb.end()) - elevation = jb["elevation"].get(); - OgreAssert(width > 1 && depth > 1 && baseHeight > 1, - "Bad stuff happen"); + // Parse district context (center point) + DistrictContext districtCtx = + parseDistrictContext(jdistrict, sceneNode); - Ogre::Quaternion rotation = Ogre::Quaternion( - Ogre::Degree(angle), Ogre::Vector3::UNIT_Y); - Ogre::Vector3 offset = - centerOrientation * rotation * - (Ogre::Vector3::UNIT_Z * distance); - Ogre::Vector3 worldCenterPosition = - centerPosition + offset + - Ogre::Vector3(0, elevation, 0); - Ogre::Quaternion worldCenterOrientation = - centerOrientation * rotation; - float outOffset = 1.05f; - float windowVerticalOffset = 0.8f; - if (jb.find("cells") != jb.end()) { - for (auto &jcell : jb["cells"]) { - int x = jcell["x"].get(); - int y = jcell["y"].get(); - int z = jcell["z"].get(); - uint64_t flags = - jcell["flags"].get(); - Ogre::Vector3 cellOffset( - x * 2.0f, y * 4.0f, z * 2.0f); - Ogre::Vector3 offsetX = - worldCenterOrientation * - Ogre::Vector3::UNIT_X * - (float)x * 2.0f; - Ogre::Vector3 offsetZ = - worldCenterOrientation * - Ogre::Vector3::UNIT_Z * - (float)z * 2.0f; - Ogre::Vector3 offsetY(0, y * 4.0f, 0); - if (Items::CellsScript::isBit( - jcell, "windowz+")) { - Ogre::Entity *ent = - ECS::get() - .mScnMgr - ->createEntity( - "window-cell", - "window-frame1"); - ent->setMaterial(townMaterial); - Ogre::Vector3 offset = - worldCenterOrientation * - Ogre::Vector3:: - UNIT_Z * - outOffset + - Ogre::Vector3::UNIT_Y * - windowVerticalOffset; - geo->addEntity( - ent, - worldCenterPosition + - offsetX + - offsetZ + - offsetY + - offset, - worldCenterOrientation); - ECS::get() - .mScnMgr->destroyEntity( - ent); - } - if (Items::CellsScript::isBit( - jcell, "windowz-")) { - Ogre::Entity *ent = - ECS::get() - .mScnMgr - ->createEntity( - "window-cell", - "window-frame1"); - ent->setMaterial(townMaterial); - Ogre::Vector3 offset = - worldCenterOrientation * - Ogre::Vector3:: - NEGATIVE_UNIT_Z * - outOffset + - Ogre::Vector3::UNIT_Y * - windowVerticalOffset; - Ogre::Quaternion rotation = - worldCenterOrientation * - Ogre::Quaternion( - Ogre::Degree( - 180), - Ogre::Vector3:: - UNIT_Y); - geo->addEntity( - ent, - worldCenterPosition + - offsetX + - offsetZ + - offsetY + - offset, - rotation); - ECS::get() - .mScnMgr->destroyEntity( - ent); - } - if (Items::CellsScript::isBit( - jcell, "windowx+")) { - Ogre::Entity *ent = - ECS::get() - .mScnMgr - ->createEntity( - "window-cell", - "window-frame1"); - ent->setMaterial(townMaterial); - Ogre::Vector3 offset = - worldCenterOrientation * - Ogre::Vector3:: - UNIT_X * - outOffset + - Ogre::Vector3::UNIT_Y * - windowVerticalOffset; - Ogre::Quaternion rotation = - worldCenterOrientation * - Ogre::Quaternion( - Ogre::Degree( - 90), - Ogre::Vector3:: - UNIT_Y); - geo->addEntity( - ent, - worldCenterPosition + - offsetX + - offsetZ + - offsetY + - offset, - rotation); - ECS::get() - .mScnMgr->destroyEntity( - ent); - } - if (Items::CellsScript::isBit( - jcell, "windowx-")) { - Ogre::Entity *ent = - ECS::get() - .mScnMgr - ->createEntity( - "window-cell", - "window-frame1"); - ent->setMaterial(townMaterial); - Ogre::Vector3 offset = - worldCenterOrientation * - Ogre::Vector3:: - NEGATIVE_UNIT_X * - outOffset + - Ogre::Vector3::UNIT_Y * - windowVerticalOffset; - Ogre::Quaternion rotation = - worldCenterOrientation * - Ogre::Quaternion( - Ogre::Degree( - -90), - Ogre::Vector3:: - UNIT_Y); - geo->addEntity( - ent, - worldCenterPosition + - offsetX + - offsetZ + - offsetY + - offset, - rotation); - ECS::get() - .mScnMgr->destroyEntity( - ent); - } - } + // Process lots if they exist + if (jdistrict.contains("lots") && + jdistrict["lots"].is_array()) { + const nlohmann::json &lots = jdistrict["lots"]; + for (nlohmann::json::const_iterator it = lots.begin(); + it != lots.end(); ++it) { + const nlohmann::json &jlot = *it; + std::cout << jlot.dump() << std::endl; + processLot(e, jlot, districtCtx, geo, + townMaterial); } } } @@ -5371,6 +6180,262 @@ struct TownDecorateWindows : TownTask { }; struct TownDecorateDoors : TownTask { std::shared_future townDecorateDoorsComplete; + +private: + struct DistrictContext { + Ogre::Vector3 centerPosition; + Ogre::Quaternion centerOrientation; + float radius; + }; + + struct LotContext { + Ogre::Vector3 worldPosition; + Ogre::Quaternion worldOrientation; + }; + + struct CellInfo { + int x, y, z; + uint64_t flags; + Ogre::Vector3 offsetX; + Ogre::Vector3 offsetY; + Ogre::Vector3 offsetZ; + }; + + enum class DoorType { External, Internal }; + + enum class DoorDirection { PositiveZ, NegativeZ, PositiveX, NegativeX }; + + struct DoorPlacement { + DoorType type; + DoorDirection direction; + Ogre::Vector3 position; + Ogre::Quaternion orientation; + bool valid; + }; + DistrictContext parseDistrictContext(const nlohmann::json &jp, + Ogre::SceneNode *sceneNode) + { + DistrictContext ctx; + + // Parse base position and rotation + Ogre::Vector3 localPosition(0, 0, 0); + Ogre::Quaternion localRotation = Ogre::Quaternion::IDENTITY; + from_json(jp["position"], localPosition); + from_json(jp["rotation"], localRotation); + + // Parse elevation and radius + float elevation = jp.value("elevation", 0.0f); + ctx.radius = jp.value("radius", 50.0f); + + // Calculate world transforms for district center + ctx.centerPosition = sceneNode->_getDerivedPosition() + + localPosition + + Ogre::Vector3(0, elevation, 0); + ctx.centerOrientation = + sceneNode->_getDerivedOrientation() * localRotation; + + return ctx; + } + LotContext createLotContext(const DistrictContext &districtCtx, + const nlohmann::json &jlot) + { + LotContext lot; + + // Parse lot properties + float angle = jlot.value("angle", 0.0f); + float elevation = jlot.value("elevation", 0.0f); + + // Calculate lot rotation and position (offset from district center) + Ogre::Quaternion lotRotation(Ogre::Degree(angle), + Ogre::Vector3::UNIT_Y); + Ogre::Vector3 offset = + districtCtx.centerOrientation * lotRotation * + (Ogre::Vector3::UNIT_Z * districtCtx.radius); + + // Lot world position = district center + offset + lot elevation + lot.worldPosition = districtCtx.centerPosition + offset + + Ogre::Vector3(0, elevation, 0); + + // Lot orientation = district orientation * lot rotation + lot.worldOrientation = + districtCtx.centerOrientation * lotRotation; + + return lot; + } + CellInfo createCellInfo(const nlohmann::json &jcell, + const LotContext &lotCtx) + { + CellInfo cell; + cell.x = jcell.value("x", 0); + cell.y = jcell.value("y", 0); + cell.z = jcell.value("z", 0); + cell.flags = jcell.value("flags", 0ULL); + + // Calculate cell offsets + cell.offsetX = lotCtx.worldOrientation * Ogre::Vector3::UNIT_X * + static_cast(cell.x) * 2.0f; + cell.offsetZ = lotCtx.worldOrientation * Ogre::Vector3::UNIT_Z * + static_cast(cell.z) * 2.0f; + cell.offsetY = Ogre::Vector3(0, cell.y * 4.0f, 0); + + return cell; + } + std::vector checkForDoors(const nlohmann::json &jcell, + const CellInfo &cell, + const LotContext &lotCtx) + { + std::vector doors; + const float outOffset = 1.05f; + auto addDoor = [&](DoorType type, DoorDirection dir, + const Ogre::Vector3 &offsetDir, + float rotationDegrees) { + DoorPlacement door; + door.type = type; + door.direction = dir; + door.valid = true; + + // Calculate offset and rotation + Ogre::Vector3 offset = + lotCtx.worldOrientation * offsetDir * outOffset; + door.position = lotCtx.worldPosition + cell.offsetX + + cell.offsetZ + cell.offsetY + offset; + door.orientation = + lotCtx.worldOrientation * + Ogre::Quaternion(Ogre::Degree(rotationDegrees), + Ogre::Vector3::UNIT_Y); + doors.push_back(door); + }; + if (Items::CellsScript::isBit(jcell, "doorz+")) { + addDoor(DoorType::External, DoorDirection::PositiveZ, + Ogre::Vector3::UNIT_Z, 0.0f); + } + if (Items::CellsScript::isBit(jcell, "idoorz+")) { + addDoor(DoorType::Internal, DoorDirection::PositiveZ, + Ogre::Vector3::UNIT_Z, 0.0f); + } + if (Items::CellsScript::isBit(jcell, "doorz-")) { + addDoor(DoorType::External, DoorDirection::NegativeZ, + Ogre::Vector3::NEGATIVE_UNIT_Z, 180.0f); + } + if (Items::CellsScript::isBit(jcell, "idoorz-")) { + addDoor(DoorType::Internal, DoorDirection::NegativeZ, + Ogre::Vector3::NEGATIVE_UNIT_Z, 180.0f); + } + if (Items::CellsScript::isBit(jcell, "doorx+")) { + addDoor(DoorType::External, DoorDirection::PositiveX, + Ogre::Vector3::UNIT_X, 90.0f); + } + if (Items::CellsScript::isBit(jcell, "idoorx+")) { + addDoor(DoorType::Internal, DoorDirection::PositiveX, + Ogre::Vector3::UNIT_X, 90.0f); + } + if (Items::CellsScript::isBit(jcell, "doorx-")) { + addDoor(DoorType::External, DoorDirection::NegativeX, + Ogre::Vector3::NEGATIVE_UNIT_X, -90.0f); + } + if (Items::CellsScript::isBit(jcell, "idoorx-")) { + addDoor(DoorType::Internal, DoorDirection::NegativeX, + Ogre::Vector3::NEGATIVE_UNIT_X, -90.0f); + } + + return doors; + } + + void placeDoor(Ogre::StaticGeometry *geo, const DoorPlacement &door, + Ogre::MaterialPtr townMaterial) + { + const char *meshName = (door.type == DoorType::External) ? + "external-door-frame1" : + "internal-door-frame1"; + + if (door.direction == DoorDirection::PositiveZ || + door.direction == DoorDirection::NegativeZ) { + // Use entity approach for Z-axis doors (original code used this) + const char *entityName = + (door.type == DoorType::External) ? + "door-cell" : + "idoor-cell"; + + Ogre::Entity *ent = + ECS::get().mScnMgr->createEntity( + entityName, meshName); + ent->setMaterial(townMaterial); + + geo->addEntity(ent, door.position, door.orientation); + ECS::get().mScnMgr->destroyEntity(ent); + } else { + // Use mesh approach for X-axis doors (original code used addMeshToStatic) + Ogre::MeshPtr mesh = + Ogre::MeshManager::getSingleton().getByName( + meshName); + if (mesh) { + addMeshToStatic(geo, mesh, townMaterial, + door.position, + door.orientation); + } + } + } + void processLot(flecs::entity e, const nlohmann::json &jlot, + const DistrictContext &districtCtx, + Ogre::StaticGeometry *geo, + Ogre::MaterialPtr townMaterial) + { + // Validate lot dimensions + int depth = jlot.value("depth", 10); + int width = jlot.value("width", 10); + OgreAssert(width > 1 && depth > 1, "Invalid lot dimensions"); + + // Create lot context with position offset from district center + LotContext lotCtx = createLotContext(districtCtx, jlot); + + // Process cells + if (jlot.contains("cells") && jlot["cells"].is_array()) { + const nlohmann::json &cells = jlot["cells"]; + for (nlohmann::json::const_iterator it = cells.begin(); + it != cells.end(); ++it) { + const nlohmann::json &jcell = *it; + + CellInfo cell = createCellInfo(jcell, lotCtx); + std::vector doors = + checkForDoors(jcell, cell, lotCtx); + + for (const auto &door : doors) { + placeDoor(geo, door, townMaterial); + } + } + } + } + +public: + void createDecorateDoors(flecs::entity e, + const nlohmann::json &jdistrict, int /*index*/, + Ogre::SceneNode *sceneNode, + Ogre::StaticGeometry *geo) + { + ZoneScoped; + // FIXME: make door placement a background task. + + // Create town material + Ogre::MaterialPtr townMaterial = createTownMaterial(e); + + // Parse district context (center point) + DistrictContext districtCtx = + parseDistrictContext(jdistrict, sceneNode); + + // Process lots if they exist + if (jdistrict.contains("lots") && + jdistrict["lots"].is_array()) { + const nlohmann::json &lots = jdistrict["lots"]; + for (nlohmann::json::const_iterator it = lots.begin(); + it != lots.end(); ++it) { + const nlohmann::json &jlot = *it; + std::cout << jlot.dump() << std::endl; + processLot(e, jlot, districtCtx, geo, + townMaterial); + } + } + } +#if 0 void createDecorateDoors(flecs::entity e, const nlohmann::json &jdistrict, int index, Ogre::SceneNode *sceneNode, @@ -5670,6 +6735,7 @@ struct TownDecorateDoors : TownTask { } } } +#endif void operator()(flecs::entity e, const nlohmann::json &jdistrict, int index, Ogre::SceneNode *sceneNode, Ogre::StaticGeometry *geo) @@ -5689,494 +6755,233 @@ struct TownDecorateDoors : TownTask { struct TownDecorateFurniture : TownTask { std::shared_future townDecorateFurnitureComplete; /* here we place all the furniture as scene objects */ +private: + struct DistrictContext { + Ogre::Vector3 centerPosition; + Ogre::Quaternion centerOrientation; + float radius; + }; + + struct LotContext { + Ogre::Vector3 worldPosition; + Ogre::Quaternion worldOrientation; + float elevation; + }; + + struct CellPlacement { + int x, y, z; + int rotation; + Ogre::String meshName; + Ogre::Vector3 position; + Ogre::Quaternion orientation; + bool valid; + }; + + Ogre::MeshPtr getFurnitureMesh(const Ogre::String &meshName) + { + Ogre::MeshPtr mesh = + Ogre::MeshManager::getSingleton().getByName(meshName); + if (!mesh) { + if (Ogre::ResourceGroupManager::getSingleton() + .resourceExists("General", meshName)) { + mesh = Ogre::MeshManager::getSingleton().load( + meshName, "General"); + Ogre::LodConfig meshconf(mesh); + setupLods(meshconf); + } + } + return mesh; + } + void placeFurnitureMesh(flecs::entity e, Ogre::MeshPtr mesh, + const Ogre::Vector3 &position, + const Ogre::Quaternion &rotation) + { + static Ogre::String materialName1 = ""; + static Ogre::String materialName2 = ""; + Ogre::Entity *ent = + ECS::get().mScnMgr->createEntity( + mesh->getName()); + Ogre::String tmpMatName = + mesh->getSubMesh(0)->getMaterialName(); + if (tmpMatName.substr(0, 14) == "furniture-sofa") { + if (materialName2 == "") + materialName2 = tmpMatName; + else + ent->setMaterialName( + + materialName2); + } else { + if (materialName1 == "") + materialName1 = tmpMatName; + else + ent->setMaterialName(materialName1); + } + Ogre::SceneNode *node = ECS::get() + .mScnMgr->getRootSceneNode() + ->createChildSceneNode(); + node->_setDerivedPosition(position); + node->_setDerivedOrientation(rotation); + node->attachObject(ent); + ent->setRenderingDistance(60.0f); + addStaticBodyMesh(e, mesh, position, rotation); + ECS::get().entity().child_of(e).set( + { node }); + } + DistrictContext parseDistrictContext(const nlohmann::json &jp, + Ogre::SceneNode *sceneNode) + { + DistrictContext ctx; + + // Parse base position and rotation + Ogre::Vector3 localPosition(0, 0, 0); + Ogre::Quaternion localRotation = Ogre::Quaternion::IDENTITY; + from_json(jp["position"], localPosition); + from_json(jp["rotation"], localRotation); + + // Parse elevation and radius + float elevation = jp.value("elevation", 0.0f); + ctx.radius = jp.value("radius", 50.0f); + + // Calculate world transforms for district center + ctx.centerPosition = sceneNode->_getDerivedPosition() + + localPosition + + Ogre::Vector3(0, elevation, 0); + ctx.centerOrientation = + sceneNode->_getDerivedOrientation() * localRotation; + + return ctx; + } + LotContext createLotContext(const DistrictContext &districtCtx, + const nlohmann::json &jlot) + { + LotContext lot; + + // Parse lot properties + float angle = jlot.value("angle", 0.0f); + lot.elevation = jlot.value("elevation", 0.0f); + + // Calculate lot rotation and position (offset from district center) + Ogre::Quaternion lotRotation(Ogre::Degree(angle), + Ogre::Vector3::UNIT_Y); + Ogre::Vector3 offset = + districtCtx.centerOrientation * lotRotation * + (Ogre::Vector3::UNIT_Z * districtCtx.radius); + + // Lot world position = district center + offset + lot elevation + lot.worldPosition = districtCtx.centerPosition + offset + + Ogre::Vector3(0, lot.elevation, 0); + + // Lot orientation = district orientation * lot rotation + lot.worldOrientation = + districtCtx.centerOrientation * lotRotation; + + return lot; + } + CellPlacement createCellPlacement(const nlohmann::json &jfcell, + const LotContext &lotCtx) + { + CellPlacement cell; + cell.valid = false; + + // Check if cell has furniture with mesh + if (!jfcell.is_object() || !jfcell.contains("furniture")) { + return cell; + } + + const nlohmann::json &furniture = jfcell["furniture"]; + if (!furniture.is_object() || !furniture.contains("mesh")) { + return cell; + } + + cell.x = jfcell.value("x", 0); + cell.y = jfcell.value("y", 0); + cell.z = jfcell.value("z", 0); + cell.rotation = jfcell.value("rotation", 2); + cell.meshName = furniture["mesh"].get(); + + // Calculate cell offsets within the lot + Ogre::Vector3 offsetX = lotCtx.worldOrientation * + Ogre::Vector3::UNIT_X * + static_cast(cell.x) * 2.0f; + Ogre::Vector3 offsetZ = lotCtx.worldOrientation * + Ogre::Vector3::UNIT_Z * + static_cast(cell.z) * 2.0f; + Ogre::Vector3 offsetY(0, cell.y * 4.0f, 0); + + // Cell position = lot position + cell offsets + cell.position = + lotCtx.worldPosition + offsetX + offsetZ + offsetY; + + // Cell orientation = lot orientation * rotation + cell.orientation = + lotCtx.worldOrientation * + Ogre::Quaternion(Ogre::Degree(90.0f * cell.rotation), + Ogre::Vector3::UNIT_Y); + + cell.valid = true; + return cell; + } + + void processLot(flecs::entity e, const nlohmann::json &jlot, + const DistrictContext &districtCtx) + { + // Validate lot dimensions + int depth = jlot.value("depth", 10); + int width = jlot.value("width", 10); + OgreAssert(width > 1 && depth > 1, "Invalid lot dimensions"); + + // Create lot context with position offset from district center + LotContext lotCtx = createLotContext(districtCtx, jlot); + + // Process furniture cells + if (jlot.contains("furniture_cells") && + jlot["furniture_cells"].is_array()) { + const nlohmann::json &cells = jlot["furniture_cells"]; + for (nlohmann::json::const_iterator it = cells.begin(); + it != cells.end(); ++it) { + const nlohmann::json &jfcell = *it; + + CellPlacement cell = + createCellPlacement(jfcell, lotCtx); + if (!cell.valid) + continue; + + Ogre::MeshPtr mesh = + getFurnitureMesh(cell.meshName); + if (mesh) { + placeFurnitureMesh(e, mesh, + cell.position, + cell.orientation); + } + } + } + } + +public: void createDecorateFurniture(flecs::entity e, - const nlohmann::json &jdistrict, int index, - Ogre::SceneNode *sceneNode, - Ogre::StaticGeometry *geo) + const nlohmann::json &jdistrict, + int /*index*/, Ogre::SceneNode *sceneNode, + Ogre::StaticGeometry * /*geo*/) { ZoneScoped; // FIXME: make furniture placement a background task. - Ogre::MaterialPtr townMaterial = createTownMaterial(e); - const nlohmann::json &jp = jdistrict; - nlohmann::json jlots = nlohmann::json::array(); - float baseHeight = 4.0f; - Ogre::Vector3 localPosition(0, 0, 0); - Ogre::Quaternion localRotation = Ogre::Quaternion::IDENTITY; - Ogre::Vector3 centerPosition = sceneNode->_getDerivedPosition(); - Ogre::Quaternion centerOrientation = - sceneNode->_getDerivedOrientation(); - float delevation = 0.0f; - float radius = 50.0f; - if (jp.find("elevation") != jp.end()) - delevation = jp["elevation"].get(); - if (jp.find("radius") != jp.end()) - radius = jp["radius"].get(); - from_json(jp["position"], localPosition); - from_json(jp["rotation"], localRotation); - centerPosition = centerPosition + localPosition + - Ogre::Vector3(0, delevation, 0); - centerOrientation = centerOrientation * localRotation; - if (jdistrict.find("lots") != jdistrict.end()) - jlots = jdistrict["lots"]; - for (const auto &jb : jlots) { - float angle = 0.0f; - int depth = 10; - int width = 10; - float distance = radius; - float elevation = 0.0f; - std::cout << jb.dump() << std::endl; - if (jb.find("angle") != jb.end()) - angle = jb["angle"].get(); - if (jb.find("depth") != jb.end()) - depth = jb["depth"].get(); - if (jb.find("width") != jb.end()) - width = jb["width"].get(); - if (jb.find("elevation") != jb.end()) - elevation = jb["elevation"].get(); - OgreAssert(width > 1 && depth > 1 && baseHeight > 1, - "Bad stuff happen"); + // Create town material (required for furniture) + createTownMaterial(e); - Ogre::Quaternion rotation = Ogre::Quaternion( - Ogre::Degree(angle), Ogre::Vector3::UNIT_Y); - Ogre::Vector3 offset = - centerOrientation * rotation * - (Ogre::Vector3::UNIT_Z * distance); - Ogre::Vector3 worldCenterPosition = - centerPosition + offset + - Ogre::Vector3(0, elevation, 0); - Ogre::Quaternion worldCenterOrientation = - centerOrientation * rotation; - float outOffset = 1.05f; - if (jb.find("furniture_cells") != jb.end()) { - for (auto &jfcell : jb["furniture_cells"]) { - int x = jfcell["x"].get(); - int y = jfcell["y"].get(); - int z = jfcell["z"].get(); - nlohmann::json furniture = - jfcell["furniture"]; - Ogre::Vector3 cellOffset( - x * 2.0f, y * 4.0f, z * 2.0f); - Ogre::Vector3 offsetX = - worldCenterOrientation * - Ogre::Vector3::UNIT_X * - (float)x * 2.0f; - Ogre::Vector3 offsetZ = - worldCenterOrientation * - Ogre::Vector3::UNIT_Z * - (float)z * 2.0f; - Ogre::Vector3 offsetY(0, y * 4.0f, 0); - static Ogre::String materialName1 = ""; - static Ogre::String materialName2 = ""; - auto getFurnitureMesh = - [](const Ogre::String &meshName) - -> Ogre::MeshPtr { - Ogre::MeshPtr mesh = - Ogre::MeshManager::getSingleton() - .getByName( - meshName); - if (!mesh) { - if (Ogre::ResourceGroupManager::getSingleton() - .resourceExists( - "General", - meshName)) { - mesh = Ogre::MeshManager::getSingleton() - .load(meshName, - "General"); - Ogre::LodConfig meshconf( - mesh); - setupLods( - meshconf); - } - } - return mesh; - }; - auto placeFurnitureMesh = [](flecs::entity - e, - Ogre::MeshPtr - mesh, - const Ogre::Vector3 - &position, - const Ogre::Quaternion - &rotation) { - Ogre::Entity *ent = - ECS::get() - .mScnMgr - ->createEntity( - mesh->getName()); - Ogre::String tmpMatName = - mesh->getSubMesh(0) - ->getMaterialName(); - if (tmpMatName.substr(0, 14) == - "furniture-sofa") { - if (materialName2 == "") - materialName2 = - tmpMatName; - else - ent->setMaterialName( + // Parse district context (center point) + DistrictContext districtCtx = + parseDistrictContext(jdistrict, sceneNode); - materialName2); - } else { - if (materialName1 == "") - materialName1 = - tmpMatName; - else - ent->setMaterialName( - materialName1); - } - Ogre::SceneNode *node = - ECS::get() - .mScnMgr - ->getRootSceneNode() - ->createChildSceneNode(); - node->_setDerivedPosition( - position); - node->_setDerivedOrientation( - rotation); - node->attachObject(ent); - ent->setRenderingDistance( - 60.0f); - addStaticBodyMesh(e, mesh, - position, - rotation); - ECS::get() - .entity() - .child_of(e) - .set( - { node }); - }; - if (furniture.find("mesh") != - furniture.end()) { - Ogre::String meshName = - furniture["mesh"] - .get(); - Ogre::MeshPtr mesh = - getFurnitureMesh( - meshName); - if (mesh) { - int rotation = 2; - if (jfcell.find( - "rotation") != - jfcell.end()) - rotation = - jfcell["rotation"] - .get(); - - Ogre::Vector3 offset = - worldCenterOrientation * - Ogre::Vector3:: - UNIT_Z * - 0.0f; - Ogre::Vector3 furniturePosition = - worldCenterPosition + - offsetX + - offsetZ + - offsetY + - offset; - Ogre::Quaternion furnitureOrientation = - worldCenterOrientation * - Ogre::Quaternion( - Ogre::Degree( - 90.0f * - (float)rotation), - Ogre::Vector3:: - UNIT_Y); - placeFurnitureMesh( - e, mesh, - furniturePosition, - furnitureOrientation); -#if 0 - Ogre::Entity *ent = - ECS::get< - EngineData>() - .mScnMgr - ->createEntity( - meshName); - Ogre::SceneNode *node = - ECS::get< - EngineData>() - .mScnMgr - ->getRootSceneNode() - ->createChildSceneNode(); - node->_setDerivedPosition( - worldCenterPosition + - offsetX + - offsetZ + - offsetY + - offset); - node->_setDerivedOrientation( - worldCenterOrientation * - Ogre::Quaternion( - Ogre::Degree( - 90.0f * - (float)rotation), - Ogre::Vector3:: - UNIT_Y)); - node->attachObject(ent); - ent->setRenderingDistance( - 60.0f); - addStaticBodyMesh( - e, mesh, - worldCenterPosition + - offsetX + - offsetZ + - offsetY + - offset, - worldCenterOrientation * - Ogre::Quaternion( - Ogre::Degree( - 90.0f * - (float)rotation), - Ogre::Vector3:: - UNIT_Y)); - ECS::get() - .entity() - .child_of(e) - .set( - { node }); -#endif -#if 0 - if (furniture.find( - "sensors") != - furniture.end()) { - for (const auto & - sensor : - furniture["sensors"]) { - std::cout - << "SENSOR: " - << sensor.dump() - << std::endl; - std::cout - << furniture - .dump() - << std::endl; - Ogre::Vector3 - sensorPosition; - sensorPosition - .x = - sensor["position_x"] - .get(); - sensorPosition - .y = - sensor["position_y"] - .get(); - sensorPosition - .z = - sensor["position_z"] - .get(); - Ogre::Quaternion worldSensorRotation = - worldCenterOrientation * - Ogre::Quaternion( - Ogre::Degree( - 90.0f * - (float)rotation), - Ogre::Vector3:: - UNIT_Y); - Ogre::Vector3 worldSensorPosition = - worldCenterPosition + - offsetX + - offsetZ + - offsetY + - offset + - worldSensorRotation * - sensorPosition; - float height = - sensor["height"] - .get(); - float radius = - sensor["radius"] - .get(); - std::cout - << "sensor: " - << height - << " " - << radius - << std::endl; - if (ECS::get() - .has()) { - ActionNodeList::ActionNode - anode; - anode.action = - sensor["action"] - .get(); - anode.action_text = - sensor["action_text"] - .get(); - anode.props = - sensor; - anode.position = - worldSensorPosition; - anode.rotation = - worldSensorRotation; - ECS::get_mut< - ActionNodeList>() - .addNode( - anode); - } - - JPH::ShapeRefC shape2 = - JoltPhysicsWrapper::getSingleton() - .createCylinderShape( - height / - 2.0f, - radius); - JPH::ShapeRefC shape = - JoltPhysicsWrapper::getSingleton() - .createRotatedTranslatedShape( - Ogre::Vector3( - 0, - height / - 2.0f, - 0), - Ogre::Quaternion:: - IDENTITY, - shape2); - - JPH::BodyID id = - JoltPhysicsWrapper::getSingleton() - .createSensor( - shape, - worldSensorPosition, - worldSensorRotation, - JPH::EMotionType:: - Static, - Layers::SENSORS); - JoltPhysicsWrapper::getSingleton() - .addBody( - id, - JPH::EActivation:: - Activate); - JoltPhysicsWrapper::getSingleton() - .addContactListener( - id, - [sensor]( - const JoltPhysics:: - ContactListener::ContactReport - &report) - -> void { - std::cout - << sensor - << std::endl; -#if 0 - OgreAssert( - false, - "contact!"); -#endif - }); - // body for sensor - ECS::get() - .entity() - .child_of( - e) - .set( - id); -#if 0 - OgreAssert( - false, - "sensor"); -#endif - } - } -#endif - if (furniture.find( - "actions") != - furniture.end()) { - for (const auto & - action : - furniture["actions"]) { - std::cout - << "SENSOR: " - << action.dump() - << std::endl; - std::cout - << furniture - .dump() - << std::endl; - Ogre::Vector3 - actionPosition; - actionPosition - .x = - action["position_x"] - .get(); - actionPosition - .y = - action["position_y"] - .get(); - actionPosition - .z = - action["position_z"] - .get(); - Ogre::Quaternion worldSensorRotation = - worldCenterOrientation * - Ogre::Quaternion( - Ogre::Degree( - 90.0f * - (float)rotation), - Ogre::Vector3:: - UNIT_Y); - Ogre::Vector3 worldSensorPosition = - worldCenterPosition + - offsetX + - offsetZ + - offsetY + - offset + - worldSensorRotation * - actionPosition; - float height = - action["height"] - .get(); - float radius = - action["radius"] - .get(); -#if 0 - if (ECS::get() - .has()) { - ActionNodeList::ActionNode - anode; - anode.action = - action["action"] - .get(); - anode.action_text = - action["action_text"] - .get(); - anode.radius = - action["radius"] - .get(); - anode.height = - action["height"] - .get(); - anode.props = - action; - anode.props - ["town"] = - e.id(); - anode.props - ["index"] = - -1; - anode.position = - worldSensorPosition; - anode.rotation = - worldSensorRotation; - anode.dynamic = - false; - ECS::get_mut< - ActionNodeList>() - .addNode( - anode); - std::cout - << "action: " - << action.dump( - 4) - << std::endl; - ECS::modified< - ActionNodeList>(); - } -#endif - } - } - } - } - } + // Process lots if they exist + if (jdistrict.contains("lots") && + jdistrict["lots"].is_array()) { + const nlohmann::json &lots = jdistrict["lots"]; + for (nlohmann::json::const_iterator it = lots.begin(); + it != lots.end(); ++it) { + const nlohmann::json &jlot = *it; + std::cout << jlot.dump() << std::endl; + processLot(e, jlot, districtCtx); } } } @@ -6367,8 +7172,8 @@ void createTownActionNodes(flecs::entity e) Ogre::Vector3::UNIT_Z * (float)z * 2.0f; Ogre::Vector3 offsetY(0, y * 4.0f, 0); - static Ogre::String materialName1 = ""; - static Ogre::String materialName2 = ""; + // static Ogre::String materialName1 = ""; + // static Ogre::String materialName2 = ""; int rotation = 2; if (jfcell.find("rotation") != jfcell.end()) @@ -6378,18 +7183,6 @@ void createTownActionNodes(flecs::entity e) Ogre::Vector3 offset = worldCenterOrientation * Ogre::Vector3::UNIT_Z * 0.0f; -#if 0 - Ogre::Vector3 furniturePosition = - worldCenterPosition + offsetX + - offsetZ + offsetY + offset; - Ogre::Quaternion furnitureOrientation = - worldCenterOrientation * - Ogre::Quaternion( - Ogre::Degree( - 90.0f * - (float)rotation), - Ogre::Vector3::UNIT_Y); -#endif if (furniture.find("actions") != furniture.end()) {