diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index f6fa7e5..19365e1 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -23,6 +23,7 @@ set(EDITSCENE_SOURCES systems/ProceduralMaterialSystem.cpp systems/ProceduralMeshSystem.cpp systems/CellGridSystem.cpp + systems/RoomLayoutSystem.cpp ui/TransformEditor.cpp ui/RenderableEditor.cpp ui/PhysicsColliderEditor.cpp @@ -43,6 +44,9 @@ set(EDITSCENE_SOURCES ui/TownEditor.cpp ui/RoofEditor.cpp ui/RoomEditor.cpp + + + ui/ClearAreaEditor.cpp ui/FurnitureTemplateEditor.cpp ui/ComponentRegistration.cpp components/LightModule.cpp @@ -82,6 +86,7 @@ set(EDITSCENE_HEADERS components/CellGrid.hpp systems/EditorUISystem.hpp systems/CellGridSystem.hpp + systems/RoomLayoutSystem.hpp systems/ProceduralMaterialSystem.hpp systems/ProceduralMeshSystem.hpp systems/ProceduralTextureSystem.hpp @@ -114,6 +119,9 @@ set(EDITSCENE_HEADERS ui/TownEditor.hpp ui/RoofEditor.hpp ui/RoomEditor.hpp + + + ui/ClearAreaEditor.hpp ui/FurnitureTemplateEditor.hpp camera/EditorCamera.hpp gizmo/Gizmo.hpp diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 02474f7..f7ec8ef 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -10,6 +10,7 @@ #include "systems/ProceduralMaterialSystem.hpp" #include "systems/ProceduralMeshSystem.hpp" #include "systems/CellGridSystem.hpp" +#include "systems/RoomLayoutSystem.hpp" #include "camera/EditorCamera.hpp" #include "components/EntityName.hpp" #include "components/Transform.hpp" @@ -194,6 +195,10 @@ void EditorApp::setup() // Setup CellGrid system m_cellGridSystem = std::make_unique(m_world, m_sceneMgr); m_cellGridSystem->initialize(); + + // Setup RoomLayout system + m_roomLayoutSystem = std::make_unique(m_world, m_sceneMgr); + m_roomLayoutSystem->initialize(); // Add default entities to UI cache for (auto &e : m_defaultEntities) { @@ -414,7 +419,12 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) m_proceduralMeshSystem->update(); } - // Update CellGrid system + // Update RoomLayout system FIRST (generates cells for CellGrid) + if (m_roomLayoutSystem) { + m_roomLayoutSystem->update(); + } + + // Update CellGrid system (builds mesh from cells) if (m_cellGridSystem) { m_cellGridSystem->update(); } diff --git a/src/features/editScene/EditorApp.hpp b/src/features/editScene/EditorApp.hpp index e957765..e78087b 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -22,6 +22,7 @@ class ProceduralTextureSystem; class ProceduralMaterialSystem; class ProceduralMeshSystem; class CellGridSystem; +class RoomLayoutSystem; /** * RenderTargetListener for ImGui frame management @@ -97,6 +98,7 @@ private: std::unique_ptr m_proceduralMaterialSystem; std::unique_ptr m_proceduralMeshSystem; std::unique_ptr m_cellGridSystem; + std::unique_ptr m_roomLayoutSystem; // State uint16_t m_currentModifiers; diff --git a/src/features/editScene/components/CellGrid.hpp b/src/features/editScene/components/CellGrid.hpp index 363500e..55ca829 100644 --- a/src/features/editScene/components/CellGrid.hpp +++ b/src/features/editScene/components/CellGrid.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -188,22 +189,165 @@ struct CellGridComponent { /** * @brief Room definition within a cell grid * - * Rooms are rectangular areas that can be connected by doors + * Rooms are rectangular areas that can be connected by doors. + * This is the ECS version of the Lua room() function. */ struct RoomComponent { - // Room bounds (in cell coordinates, inclusive) + // Room bounds (in cell coordinates) + // A room from (minX, minZ) to (maxX-1, maxZ-1) inclusive int minX = 0, minY = 0, minZ = 0; - int maxX = 0, maxY = 0, maxZ = 0; + int maxX = 1, maxY = 1, maxZ = 1; // exclusive max (size = max - min) // Room name/tag for furniture placement rules std::string roomType; // e.g., "bedroom", "kitchen", "hallway" std::vector tags; - // Connected room indices - std::vector connectedRooms; + // Generation flags + bool createFloor = true; // Create floor cells + bool createCeiling = true; // Create ceiling cells + bool createInteriorWalls = true; // Create iwallx-/+, iwallz-/+ around the room + bool createWindows = false; // Convert exterior-facing walls to windows - // Exit directions (0-3 = Z-, Z+, X-, X+) - std::vector exits; + // Dirty flag - triggers regeneration of cell grid + bool dirty = true; + + void markDirty() { dirty = true; } + + // Helper to get size + int getSizeX() const { return maxX - minX; } + int getSizeY() const { return maxY - minY; } + int getSizeZ() const { return maxZ - minZ; } + + // Check if a cell position is inside this room + bool contains(int x, int y, int z) const { + return x >= minX && x < maxX && + y >= minY && y < maxY && + z >= minZ && z < maxZ; + } + + // Check if a cell is on the room edge (for wall placement) + bool isOnEdge(int x, int z) const { + return x == minX || x == maxX - 1 || z == minZ || z == maxZ - 1; + } + + // Get the side of the room this cell is on (0=Z-, 1=Z+, 2=X-, 3=X+, -1=not on edge) + int getEdgeSide(int x, int z) const { + if (!isOnEdge(x, z)) return -1; + if (z == minZ) return 0; // Z- (north) + if (z == maxZ - 1) return 1; // Z+ (south) + if (x == minX) return 2; // X- (west) + if (x == maxX - 1) return 3; // X+ (east) + return -1; + } + + // Persistent unique ID for this room (survives save/load) + // If empty, will be auto-generated during serialization + std::string persistentId; + + // Connected room persistent IDs (bidirectional connections) + // Using persistent IDs instead of entity IDs because entity IDs change on save/load + std::vector connectedRoomIds; + + // Exit doors for outside-facing walls (0=Z-, 1=Z+, 2=X-, 3=X+) + // An exit is always a door placed on a wall that faces outside (not connected to another room) + bool exits[4] = { false, false, false, false }; // Z-, Z+, X-, X+ + + // Generate a persistent ID if one doesn't exist + void ensurePersistentId(flecs::entity_t entityId = 0) { + if (persistentId.empty()) { + // Generate unique ID based on timestamp + random + optional entity ID + auto now = std::chrono::high_resolution_clock::now(); + auto nanos = std::chrono::duration_cast( + now.time_since_epoch()).count(); + persistentId = "room_" + std::to_string(nanos); + if (entityId != 0) { + persistentId += "_" + std::to_string(entityId); + } + } + } + + // Add a connection to another room (bidirectional, by persistent ID) + void addConnection(const std::string& otherRoomId) { + // Check if not already connected + for (const auto& id : connectedRoomIds) { + if (id == otherRoomId) return; + } + connectedRoomIds.push_back(otherRoomId); + } + + // Remove a connection to another room + void removeConnection(const std::string& otherRoomId) { + connectedRoomIds.erase( + std::remove(connectedRoomIds.begin(), connectedRoomIds.end(), otherRoomId), + connectedRoomIds.end() + ); + } + + // Check if connected to a room (by persistent ID) + bool isConnectedTo(const std::string& otherRoomId) const { + for (const auto& id : connectedRoomIds) { + if (id == otherRoomId) return true; + } + return false; + } + + // Helper to set all exits at once + void setAllExits(bool value) { + for (int i = 0; i < 4; i++) { + exits[i] = value; + } + } + + // Helper to set a specific exit + void setExit(int side, bool value) { + if (side >= 0 && side < 4) { + exits[side] = value; + } + } +}; + +/** + * @brief Room exits - defines external exits from a room + * + * This replaces the Lua create_exit0/1/2/3() functions. + * Creates external walls/doors/windows on the room edges that face outside. + */ +/** + * @brief Clear Area - clears cells in a region before room generation + * + * This replaces the Lua clear_area() and clear_furniture_area() functions. + * It clears all cells within the specified bounds before any room generation happens. + * This component is processed first in the RoomLayoutSystem. + */ +struct ClearAreaComponent { + // Area bounds (in cell coordinates, inclusive min, exclusive max) + int minX = 0, minY = 0, minZ = 0; + int maxX = 1, maxY = 1, maxZ = 1; + + // What to clear + bool clearCells = true; // Clear cell flags (walls, floors, etc.) + bool clearFurniture = true; // Clear furniture placements + bool clearRoofs = false; // Clear roof definitions (children with RoofComponent) + bool clearRooms = false; // Remove existing room entities (children with RoomComponent) + + // Processed flag - cleared each time dirty is set + bool processed = false; + + // Dirty flag - clear again when true + bool dirty = true; + + void markDirty() { dirty = true; } + + // Helper to set bounds + void setBounds(int x1, int z1, int x2, int z2, int y = 0) { + minX = x1; minZ = z1; minY = y; + maxX = x2; maxZ = z2; maxY = y + 1; + } + + // Helper to get size + int getSizeX() const { return maxX - minX; } + int getSizeY() const { return maxY - minY; } + int getSizeZ() const { return maxZ - minZ; } }; /** diff --git a/src/features/editScene/components/CellGridEditorsModule.cpp b/src/features/editScene/components/CellGridEditorsModule.cpp index ff1d684..f5cee3c 100644 --- a/src/features/editScene/components/CellGridEditorsModule.cpp +++ b/src/features/editScene/components/CellGridEditorsModule.cpp @@ -6,6 +6,8 @@ #include "../ui/TownEditor.hpp" #include "../ui/RoofEditor.hpp" #include "../ui/RoomEditor.hpp" + +#include "../ui/ClearAreaEditor.hpp" #include "../ui/FurnitureTemplateEditor.hpp" // Register CellGrid component @@ -103,11 +105,11 @@ REGISTER_COMPONENT_GROUP("Roof", "Cell Grid", RoofComponent, RoofEditor) ); } -// Register Room component -REGISTER_COMPONENT_GROUP("Room", "Cell Grid", RoomComponent, RoomEditor) +// Register Room component (in Room Layout group) +REGISTER_COMPONENT_GROUP("Room", "Room Layout", RoomComponent, RoomEditor) { registry.registerComponent( - "Room", "Cell Grid", + "Room", "Room Layout", std::make_unique(), [sceneMgr](flecs::entity e) { if (!e.has()) { @@ -140,3 +142,26 @@ REGISTER_COMPONENT_GROUP("Furniture Template", "Cell Grid", FurnitureTemplateCom } ); } + +// Register ClearArea component (in Room Layout group) +REGISTER_COMPONENT_GROUP("Clear Area", "Room Layout", ClearAreaComponent, ClearAreaEditor) +{ + registry.registerComponent( + "Clear Area", "Room Layout", + std::make_unique(), + [sceneMgr](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + [sceneMgr](flecs::entity e) { + if (e.has()) { + e.remove(); + } + } + ); +} + +// Note: ExteriorGenerationComponent has been removed. +// Exterior generation now automatically runs at the end of the RoomLayoutSystem pipeline. +// This mirrors the original Lua behavior where create_exterior() was called after all rooms were defined. diff --git a/src/features/editScene/resources.cfg b/src/features/editScene/resources.cfg index 160de71..03be0fe 100644 --- a/src/features/editScene/resources.cfg +++ b/src/features/editScene/resources.cfg @@ -7,6 +7,8 @@ FileSystem=resources/materials/scripts FileSystem=resources/meshes FileSystem=resources/textures FileSystem=resources/buildings +FileSystem=resources/buildings/parts/pier +FileSystem=resources/buildings/parts/furniture FileSystem=resources/vehicles [Popular] diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index ad5bce0..c45cb9b 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -655,7 +655,22 @@ void EditorUISystem::renderComponentList(flecs::entity entity) // Render Room if present if (entity.has()) { auto &room = entity.get_mut(); - m_componentRegistry.render(entity, room); + if (m_componentRegistry.render(entity, room)) { + room.markDirty(); + } + componentCount++; + } + + + + + + // Render ClearArea if present + if (entity.has()) { + auto &clearArea = entity.get_mut(); + if (m_componentRegistry.render(entity, clearArea)) { + clearArea.markDirty(); + } componentCount++; } diff --git a/src/features/editScene/systems/RoomLayoutSystem.cpp b/src/features/editScene/systems/RoomLayoutSystem.cpp new file mode 100644 index 0000000..d2f19e1 --- /dev/null +++ b/src/features/editScene/systems/RoomLayoutSystem.cpp @@ -0,0 +1,767 @@ +#include "RoomLayoutSystem.hpp" +#include "../components/CellGrid.hpp" +#include +#include +#include +#include + +RoomLayoutSystem::RoomLayoutSystem(flecs::world& world, Ogre::SceneManager* sceneMgr) + : m_world(world) + , m_sceneMgr(sceneMgr) + , m_roomQuery(world.query()) + , m_clearAreaQuery(world.query()) +{ +} + +RoomLayoutSystem::~RoomLayoutSystem() = default; + +void RoomLayoutSystem::initialize() +{ + if (m_initialized) + return; + m_initialized = true; + Ogre::LogManager::getSingleton().logMessage("RoomLayoutSystem initialized"); +} + +void RoomLayoutSystem::update() +{ + if (!m_initialized) + return; + + // Check if any component is dirty - if so, we need to reprocess everything + // because room layout is interdependent + bool anyDirty = false; + + m_clearAreaQuery.each([&](flecs::entity e, ClearAreaComponent& c) { + if (c.dirty || !c.processed) anyDirty = true; + }); + m_roomQuery.each([&](flecs::entity e, RoomComponent& r) { + if (r.dirty) anyDirty = true; + }); + + // If anything changed, reprocess everything + if (anyDirty || m_needsFullReprocess) { + reprocessAll(); + m_needsFullReprocess = false; + } +} + +void RoomLayoutSystem::reprocessAll() +{ + Ogre::LogManager::getSingleton().logMessage("RoomLayoutSystem: Full reprocess starting..."); + + // STEP 1: Clear areas FIRST + m_clearAreaQuery.each([&](flecs::entity clearEntity, ClearAreaComponent& clearArea) { + CellGridComponent* grid = findParentGrid(clearEntity); + if (grid) { + processClearArea(clearEntity, clearArea, *grid); + clearArea.dirty = false; + clearArea.processed = true; + } + }); + + // STEP 2: Process all rooms (creates floor, ceiling, interior walls) + m_roomQuery.each([&](flecs::entity roomEntity, RoomComponent& room) { + CellGridComponent* grid = findParentGrid(roomEntity); + if (grid) { + processRoom(roomEntity, room, *grid); + room.dirty = false; + } + }); + + // STEP 3: Process connections for all rooms (creates doors between rooms) + // Build a map of persistent ID -> room entity for connection lookup + std::unordered_map roomIdMap; + m_roomQuery.each([&](flecs::entity roomEntity, RoomComponent& room) { + room.ensurePersistentId(roomEntity.id()); + roomIdMap[room.persistentId] = roomEntity; + }); + + // Track which room pairs we've processed to avoid duplicates + std::set> processedPairs; + m_roomQuery.each([&](flecs::entity roomEntity, RoomComponent& room) { + CellGridComponent* grid = findParentGrid(roomEntity); + if (grid) { + // Only process connections where this room's ID is "less than" the other + // to avoid creating duplicate doors + for (const std::string& otherRoomId : room.connectedRoomIds) { + std::string id1 = room.persistentId; + std::string id2 = otherRoomId; + if (id1 > id2) std::swap(id1, id2); + + auto pair = std::make_pair(id1, id2); + if (processedPairs.insert(pair).second) { + // New pair - process it + auto it = roomIdMap.find(otherRoomId); + if (it != roomIdMap.end()) { + flecs::entity otherRoom = it->second; + if (otherRoom.is_alive() && otherRoom.has()) { + processRoomConnection(roomEntity, room, otherRoom, otherRoom.get(), *grid); + } + } + } + } + } + }); + + // STEP 4: Process exits for all rooms (creates external walls/doors/windows) + m_roomQuery.each([&](flecs::entity roomEntity, RoomComponent& room) { + CellGridComponent* grid = findParentGrid(roomEntity); + if (grid) { + processRoomExits(roomEntity, room, *grid); + } + }); + + // STEP 5: Process windows for rooms that have createWindows=true + // This runs after all doors/exits are created + m_roomQuery.each([&](flecs::entity roomEntity, RoomComponent& room) { + if (room.createWindows) { + CellGridComponent* grid = findParentGrid(roomEntity); + if (grid) { + processRoomWindows(roomEntity, room, *grid); + } + } + }); + + // STEP 6: Process exterior generation LAST (always runs for all rooms) + // This mirrors the original Lua create_exterior() function + m_roomQuery.each([&](flecs::entity roomEntity, RoomComponent& room) { + CellGridComponent* grid = findParentGrid(roomEntity); + if (grid) { + processExteriorGeneration(*grid); + } + }); + + Ogre::LogManager::getSingleton().logMessage("RoomLayoutSystem: Full reprocess complete."); +} + +void RoomLayoutSystem::processClearArea(flecs::entity clearEntity, ClearAreaComponent& clearArea, + CellGridComponent& grid) +{ + Ogre::LogManager::getSingleton().logMessage( + "RoomLayoutSystem: Clearing area [" + + std::to_string(clearArea.minX) + "," + std::to_string(clearArea.minZ) + "] to [" + + std::to_string(clearArea.maxX) + "," + std::to_string(clearArea.maxZ) + "]"); + + // Clear cells in the area + if (clearArea.clearCells) { + for (int y = clearArea.minY; y < clearArea.maxY; y++) { + for (int z = clearArea.minZ; z < clearArea.maxZ; z++) { + for (int x = clearArea.minX; x < clearArea.maxX; x++) { + grid.removeCell(x, y, z); + } + } + } + } + + // Clear furniture cells in the area + if (clearArea.clearFurniture) { + for (int y = clearArea.minY; y < clearArea.maxY; y++) { + for (int z = clearArea.minZ; z < clearArea.maxZ; z++) { + for (int x = clearArea.minX; x < clearArea.maxX; x++) { + grid.removeFurnitureCell(x, y, z); + } + } + } + } + + // Clear roof entities (children of the grid entity) + if (clearArea.clearRoofs) { + flecs::entity gridEntity = clearEntity.parent(); + if (gridEntity.is_alive()) { + std::vector toRemove; + gridEntity.children([&](flecs::entity child) { + if (child.has()) { + const auto& roof = child.get(); + if (roof.posX >= clearArea.minX && roof.posX < clearArea.maxX && + roof.posZ >= clearArea.minZ && roof.posZ < clearArea.maxZ) { + toRemove.push_back(child); + } + } + }); + for (auto e : toRemove) { + e.destruct(); + } + } + } + + // Clear room entities (children of the grid entity) + if (clearArea.clearRooms) { + flecs::entity gridEntity = clearEntity.parent(); + if (gridEntity.is_alive()) { + std::vector toRemove; + gridEntity.children([&](flecs::entity child) { + if (child.has()) { + const auto& room = child.get(); + bool overlaps = !(room.maxX <= clearArea.minX || room.minX >= clearArea.maxX || + room.maxZ <= clearArea.minZ || room.minZ >= clearArea.maxZ); + if (overlaps) { + toRemove.push_back(child); + } + } + }); + for (auto e : toRemove) { + e.destruct(); + } + } + } + + grid.markDirty(); +} + +void RoomLayoutSystem::processExteriorGeneration(CellGridComponent& grid) +{ + // This mirrors the original Lua create_exterior() function + // It processes ALL rooms and adds exterior flags where interior walls face empty space + // Note: This runs AFTER all rooms are created, so it can properly detect shared walls + + flecs::entity gridEntity = flecs::entity::null(); + + // Find the grid entity from the grid component + m_world.each([&](flecs::entity e, CellGridComponent& cg) { + if (&cg == &grid) { + gridEntity = e; + } + }); + + if (!gridEntity.is_alive()) return; + + // Iterate through all rooms in the grid + gridEntity.children([&](flecs::entity roomEntity) { + if (!roomEntity.has()) return true; + + const auto& room = roomEntity.get(); + int y = room.minY; + + // Process Z- side (north) + for (int x = room.minX; x < room.maxX; x++) { + // Check if there's a cell OUTSIDE the room (at z-1) + // If there IS a cell there, skip - this is a shared wall + if (grid.findCell(x, y, room.minZ - 1)) continue; + + Cell* cellPtr = grid.findCell(x, y, room.minZ); + if (!cellPtr) continue; + + // Add exterior flags for interior walls/doors/windows (without clearing interior) + if (cellPtr->hasFlag(CellFlags::IntWallZNeg)) + cellPtr->setFlag(CellFlags::WallZNeg); + if (cellPtr->hasFlag(CellFlags::IntDoorZNeg)) + cellPtr->setFlag(CellFlags::DoorZNeg); + if (cellPtr->hasFlag(CellFlags::IntWindowZNeg)) + cellPtr->setFlag(CellFlags::WindowZNeg); + } + + // Process Z+ side (south) + for (int x = room.minX; x < room.maxX; x++) { + if (grid.findCell(x, y, room.maxZ)) continue; + + Cell* cellPtr = grid.findCell(x, y, room.maxZ - 1); + if (!cellPtr) continue; + + if (cellPtr->hasFlag(CellFlags::IntWallZPos)) + cellPtr->setFlag(CellFlags::WallZPos); + if (cellPtr->hasFlag(CellFlags::IntDoorZPos)) + cellPtr->setFlag(CellFlags::DoorZPos); + if (cellPtr->hasFlag(CellFlags::IntWindowZPos)) + cellPtr->setFlag(CellFlags::WindowZPos); + } + + // Process X+ side (east) + for (int z = room.minZ; z < room.maxZ; z++) { + if (grid.findCell(room.maxX, y, z)) continue; + + Cell* cellPtr = grid.findCell(room.maxX - 1, y, z); + if (!cellPtr) continue; + + if (cellPtr->hasFlag(CellFlags::IntWallXPos)) + cellPtr->setFlag(CellFlags::WallXPos); + if (cellPtr->hasFlag(CellFlags::IntDoorXPos)) + cellPtr->setFlag(CellFlags::DoorXPos); + if (cellPtr->hasFlag(CellFlags::IntWindowXPos)) + cellPtr->setFlag(CellFlags::WindowXPos); + } + + // Process X- side (west) + for (int z = room.minZ; z < room.maxZ; z++) { + if (grid.findCell(room.minX - 1, y, z)) continue; + + Cell* cellPtr = grid.findCell(room.minX, y, z); + if (!cellPtr) continue; + + if (cellPtr->hasFlag(CellFlags::IntWallXNeg)) + cellPtr->setFlag(CellFlags::WallXNeg); + if (cellPtr->hasFlag(CellFlags::IntDoorXNeg)) + cellPtr->setFlag(CellFlags::DoorXNeg); + if (cellPtr->hasFlag(CellFlags::IntWindowXNeg)) + cellPtr->setFlag(CellFlags::WindowXNeg); + } + + return true; + }); + + grid.markDirty(); +} + +void RoomLayoutSystem::processRoom(flecs::entity roomEntity, RoomComponent& room, + CellGridComponent& grid) +{ + Ogre::LogManager::getSingleton().logMessage( + "RoomLayoutSystem: Processing room " + std::to_string(roomEntity.id()) + + " at [" + std::to_string(room.minX) + "," + std::to_string(room.minZ) + + "] size [" + std::to_string(room.getSizeX()) + "," + std::to_string(room.getSizeZ()) + "]"); + + int y = room.minY; + + // Create floor and ceiling for the room area + for (int z = room.minZ; z < room.maxZ; z++) { + for (int x = room.minX; x < room.maxX; x++) { + Cell& cell = grid.getOrCreateCell(x, y, z); + + if (room.createFloor) + cell.setFlag(CellFlags::Floor); + if (room.createCeiling) + cell.setFlag(CellFlags::Ceiling); + } + } + + // Create interior walls around the room perimeter + if (room.createInteriorWalls) { + // Z- side (north) + for (int x = room.minX; x < room.maxX; x++) { + Cell& cell = grid.getOrCreateCell(x, y, room.minZ); + cell.setFlag(CellFlags::IntWallZNeg); + } + // Z+ side (south) + for (int x = room.minX; x < room.maxX; x++) { + Cell& cell = grid.getOrCreateCell(x, y, room.maxZ - 1); + cell.setFlag(CellFlags::IntWallZPos); + } + // X- side (west) + for (int z = room.minZ; z < room.maxZ; z++) { + Cell& cell = grid.getOrCreateCell(room.minX, y, z); + cell.setFlag(CellFlags::IntWallXNeg); + } + // X+ side (east) + for (int z = room.minZ; z < room.maxZ; z++) { + Cell& cell = grid.getOrCreateCell(room.maxX - 1, y, z); + cell.setFlag(CellFlags::IntWallXPos); + } + } + + grid.markDirty(); +} + +void RoomLayoutSystem::processRoomConnections(flecs::entity roomEntity, RoomComponent& room, + CellGridComponent& grid) +{ + // This is now handled in reprocessAll() to avoid duplicates +} + +void RoomLayoutSystem::processRoomConnection(flecs::entity roomAEntity, const RoomComponent& roomA, + flecs::entity roomBEntity, const RoomComponent& roomB, + CellGridComponent& grid) +{ + Ogre::LogManager::getSingleton().logMessage( + "RoomLayoutSystem: Processing connection between rooms " + + std::to_string(roomAEntity.id()) + " and " + std::to_string(roomBEntity.id())); + + int y = roomA.minY; + + // Find adjacent cells between the two rooms + std::vector> cellsA, cellsB; + findAdjacentCells(roomA, roomB, cellsA, cellsB); + + if (cellsA.empty()) { + Ogre::LogManager::getSingleton().logMessage( + "RoomLayoutSystem: No adjacent cells found between rooms"); + return; + } + + // Find the cell closest to the geometric center of all adjacent cells + // This mirrors the original Lua connectRooms() logic + size_t index = 0; + if (cellsA.size() > 1) { + // Calculate average (center) point + int sumX = 0, sumZ = 0; + for (const auto& cell : cellsA) { + sumX += cell.first; + sumZ += cell.second; + } + int avgX = sumX / cellsA.size(); + int avgZ = sumZ / cellsA.size(); + + // Find closest point to average + int minDistance = -1; + for (size_t i = 0; i < cellsA.size(); i++) { + int dx = cellsA[i].first - avgX; + int dz = cellsA[i].second - avgZ; + int dist = dx * dx + dz * dz; + if (minDistance == -1 || dist < minDistance) { + minDistance = dist; + index = i; + } + } + } + + // Door position in roomA (closest to center of shared wall) + int xA = cellsA[index].first; + int zA = cellsA[index].second; + + // Door position in roomB (adjacent to roomA's door) + int xB = cellsB[index].first; + int zB = cellsB[index].second; + + // Determine which side of roomA faces roomB + int sideA = -1; + if (zA == roomA.minZ) sideA = 0; // Z- + else if (zA == roomA.maxZ - 1) sideA = 1; // Z+ + else if (xA == roomA.minX) sideA = 2; // X- + else if (xA == roomA.maxX - 1) sideA = 3; // X+ + + // Determine which side of roomB faces roomA + int sideB = -1; + if (zB == roomB.minZ) sideB = 0; // Z- + else if (zB == roomB.maxZ - 1) sideB = 1; // Z+ + else if (xB == roomB.minX) sideB = 2; // X- + else if (xB == roomB.maxX - 1) sideB = 3; // X+ + + // Create door in roomA + if (sideA >= 0) { + clearWallFlags(grid, xA, y, zA, sideA); + setWallFlags(grid, xA, y, zA, sideA, false, true, false); // Interior door + } + + // Create door in roomB + if (sideB >= 0) { + clearWallFlags(grid, xB, y, zB, sideB); + setWallFlags(grid, xB, y, zB, sideB, false, true, false); // Interior door + } + + grid.markDirty(); +} + +void RoomLayoutSystem::processRoomExits(flecs::entity roomEntity, RoomComponent& room, + CellGridComponent& grid) +{ + Ogre::LogManager::getSingleton().logMessage( + "RoomLayoutSystem: Processing exits for room " + std::to_string(roomEntity.id())); + + flecs::entity gridEntity = roomEntity.parent(); + if (!gridEntity.is_alive()) return; + + int y = room.minY; + + // Process each exit side + // This mirrors the original Lua createExit0-3 functions: + // - Collect all outside-facing wall positions + // - Average them to find the middle + // - Create a SINGLE door at that position + for (int side = 0; side < 4; side++) { + if (!room.exits[side]) continue; + + auto cells = getRoomEdgeCells(room, side); + + // Collect positions of all cells that face outside and have interior walls + int sumPos = -1; // Running sum of positions (X or Z depending on side) + int count = 0; // Number of valid positions found + + for (const auto& cell : cells) { + int x = cell.first; + int z = cell.second; + + // Check if this cell faces outside (no cell at adjacent position) + bool facesOutside = false; + switch (side) { + case 0: facesOutside = !grid.findCell(x, y, z - 1); break; + case 1: facesOutside = !grid.findCell(x, y, z + 1); break; + case 2: facesOutside = !grid.findCell(x - 1, y, z); break; + case 3: facesOutside = !grid.findCell(x + 1, y, z); break; + } + + if (!facesOutside) continue; + + // Check if this cell has an interior wall on this side + Cell* cellPtr = grid.findCell(x, y, z); + if (!cellPtr) continue; + + bool hasIntWall = false; + switch (side) { + case 0: hasIntWall = cellPtr->hasFlag(CellFlags::IntWallZNeg); break; + case 1: hasIntWall = cellPtr->hasFlag(CellFlags::IntWallZPos); break; + case 2: hasIntWall = cellPtr->hasFlag(CellFlags::IntWallXNeg); break; + case 3: hasIntWall = cellPtr->hasFlag(CellFlags::IntWallXPos); break; + } + + if (hasIntWall) { + // Add position to sum (use X for Z- and Z+ sides, Z for X- and X+ sides) + int pos = (side < 2) ? x : z; + if (sumPos == -1) + sumPos = pos; + else + sumPos += pos; + count++; + } + } + + // Create a single door at the averaged position + if (count > 0) { + int midPos = sumPos / count; + + // Calculate the actual cell coordinates + int doorX, doorZ; + switch (side) { + case 0: // Z- side: X varies, Z is minZ + doorX = midPos; + doorZ = room.minZ; + break; + case 1: // Z+ side: X varies, Z is maxZ-1 + doorX = midPos; + doorZ = room.maxZ - 1; + break; + case 2: // X- side: X is minX, Z varies + doorX = room.minX; + doorZ = midPos; + break; + case 3: // X+ side: X is maxX-1, Z varies + doorX = room.maxX - 1; + doorZ = midPos; + break; + default: + continue; + } + + // Convert interior wall to interior door at the middle position + clearWallFlags(grid, doorX, y, doorZ, side); + setWallFlags(grid, doorX, y, doorZ, side, false, true, false); // Interior door + } + } + + grid.markDirty(); +} + +void RoomLayoutSystem::processRoomWindows(flecs::entity roomEntity, RoomComponent& room, + CellGridComponent& grid) +{ + Ogre::LogManager::getSingleton().logMessage( + "RoomLayoutSystem: Processing windows for room " + std::to_string(roomEntity.id())); + + flecs::entity gridEntity = roomEntity.parent(); + if (!gridEntity.is_alive()) return; + + int y = room.minY; + + // Process each side of the room + for (int side = 0; side < 4; side++) { + auto cells = getRoomEdgeCells(room, side); + + for (const auto& cell : cells) { + int x = cell.first; + int z = cell.second; + + // Check if this cell faces outside (not adjacent to another room) + bool facesOutside = false; + switch (side) { + case 0: facesOutside = !grid.findCell(x, y, z - 1); break; + case 1: facesOutside = !grid.findCell(x, y, z + 1); break; + case 2: facesOutside = !grid.findCell(x - 1, y, z); break; + case 3: facesOutside = !grid.findCell(x + 1, y, z); break; + } + + if (facesOutside) { + Cell* cellPtr = grid.findCell(x, y, z); + if (!cellPtr) continue; + + // Convert interior wall to interior window + // (exterior generation will convert to exterior window later) + switch (side) { + case 0: // Z- + if (cellPtr->hasFlag(CellFlags::IntWallZNeg)) { + cellPtr->clearFlag(CellFlags::IntWallZNeg); + cellPtr->setFlag(CellFlags::IntWindowZNeg); + } + break; + case 1: // Z+ + if (cellPtr->hasFlag(CellFlags::IntWallZPos)) { + cellPtr->clearFlag(CellFlags::IntWallZPos); + cellPtr->setFlag(CellFlags::IntWindowZPos); + } + break; + case 2: // X- + if (cellPtr->hasFlag(CellFlags::IntWallXNeg)) { + cellPtr->clearFlag(CellFlags::IntWallXNeg); + cellPtr->setFlag(CellFlags::IntWindowXNeg); + } + break; + case 3: // X+ + if (cellPtr->hasFlag(CellFlags::IntWallXPos)) { + cellPtr->clearFlag(CellFlags::IntWallXPos); + cellPtr->setFlag(CellFlags::IntWindowXPos); + } + break; + } + } + } + } + + grid.markDirty(); +} + +CellGridComponent* RoomLayoutSystem::findParentGrid(flecs::entity entity) +{ + flecs::entity current = entity; + int safety = 0; + + while (current.is_alive() && safety < 100) { + if (current.has()) { + // get_mut returns a reference, convert to pointer + return ¤t.get_mut(); + } + current = current.parent(); + safety++; + } + + return nullptr; +} + +flecs::entity RoomLayoutSystem::findRoomAt(flecs::entity gridEntity, int x, int y, int z) +{ + flecs::entity result = flecs::entity::null(); + gridEntity.children([&](flecs::entity child) { + if (child.has()) { + const auto& room = child.get(); + if (room.contains(x, y, z)) { + result = child; + return false; + } + } + return true; + }); + return result; +} + +std::vector> RoomLayoutSystem::getRoomEdgeCells(const RoomComponent& room, int side) +{ + std::vector> cells; + + switch (side) { + case 0: + for (int x = room.minX; x < room.maxX; x++) { + cells.push_back({ x, room.minZ }); + } + break; + case 1: + for (int x = room.minX; x < room.maxX; x++) { + cells.push_back({ x, room.maxZ - 1 }); + } + break; + case 2: + for (int z = room.minZ; z < room.maxZ; z++) { + cells.push_back({ room.minX, z }); + } + break; + case 3: + for (int z = room.minZ; z < room.maxZ; z++) { + cells.push_back({ room.maxX - 1, z }); + } + break; + } + + return cells; +} + +bool RoomLayoutSystem::areCellsAdjacent(int x1, int z1, int x2, int z2) +{ + int dx = std::abs(x1 - x2); + int dz = std::abs(z1 - z2); + return (dx == 0 && dz == 1) || (dx == 1 && dz == 0); +} + +void RoomLayoutSystem::findAdjacentCells(const RoomComponent& roomA, const RoomComponent& roomB, + std::vector>& outCellsA, + std::vector>& outCellsB) +{ + const int sides[] = { 0, 1, 2, 3 }; + + for (int sideA : sides) { + auto edgeA = getRoomEdgeCells(roomA, sideA); + + for (int sideB : sides) { + if (sideA == sideB) continue; + + auto edgeB = getRoomEdgeCells(roomB, sideB); + + for (const auto& cellA : edgeA) { + for (const auto& cellB : edgeB) { + if (areCellsAdjacent(cellA.first, cellA.second, cellB.first, cellB.second)) { + outCellsA.push_back(cellA); + outCellsB.push_back(cellB); + } + } + } + } + } +} + +void RoomLayoutSystem::setWallFlags(CellGridComponent& grid, int x, int y, int z, + int side, bool isExternal, bool isDoor, bool isWindow) +{ + Cell* cell = grid.findCell(x, y, z); + if (!cell) return; + + uint64_t wallFlag = 0; + uint64_t doorFlag = 0; + uint64_t windowFlag = 0; + + if (isExternal) { + switch (side) { + case 0: wallFlag = CellFlags::WallZNeg; doorFlag = CellFlags::DoorZNeg; windowFlag = CellFlags::WindowZNeg; break; + case 1: wallFlag = CellFlags::WallZPos; doorFlag = CellFlags::DoorZPos; windowFlag = CellFlags::WindowZPos; break; + case 2: wallFlag = CellFlags::WallXNeg; doorFlag = CellFlags::DoorXNeg; windowFlag = CellFlags::WindowXNeg; break; + case 3: wallFlag = CellFlags::WallXPos; doorFlag = CellFlags::DoorXPos; windowFlag = CellFlags::WindowXPos; break; + } + } else { + switch (side) { + case 0: wallFlag = CellFlags::IntWallZNeg; doorFlag = CellFlags::IntDoorZNeg; windowFlag = CellFlags::IntWindowZNeg; break; + case 1: wallFlag = CellFlags::IntWallZPos; doorFlag = CellFlags::IntDoorZPos; windowFlag = CellFlags::IntWindowZPos; break; + case 2: wallFlag = CellFlags::IntWallXNeg; doorFlag = CellFlags::IntDoorXNeg; windowFlag = CellFlags::IntWindowXNeg; break; + case 3: wallFlag = CellFlags::IntWallXPos; doorFlag = CellFlags::IntDoorXPos; windowFlag = CellFlags::IntWindowXPos; break; + } + } + + if (isDoor) { + cell->setFlag(doorFlag); + } else if (isWindow) { + cell->setFlag(windowFlag); + } else { + cell->setFlag(wallFlag); + } +} + +void RoomLayoutSystem::clearWallFlags(CellGridComponent& grid, int x, int y, int z, int side) +{ + Cell* cell = grid.findCell(x, y, z); + if (!cell) return; + + uint64_t flagsToClear = 0; + + switch (side) { + case 0: + flagsToClear = CellFlags::WallZNeg | CellFlags::DoorZNeg | CellFlags::WindowZNeg | + CellFlags::IntWallZNeg | CellFlags::IntDoorZNeg | CellFlags::IntWindowZNeg; + break; + case 1: + flagsToClear = CellFlags::WallZPos | CellFlags::DoorZPos | CellFlags::WindowZPos | + CellFlags::IntWallZPos | CellFlags::IntDoorZPos | CellFlags::IntWindowZPos; + break; + case 2: + flagsToClear = CellFlags::WallXNeg | CellFlags::DoorXNeg | CellFlags::WindowXNeg | + CellFlags::IntWallXNeg | CellFlags::IntDoorXNeg | CellFlags::IntWindowXNeg; + break; + case 3: + flagsToClear = CellFlags::WallXPos | CellFlags::DoorXPos | CellFlags::WindowXPos | + CellFlags::IntWallXPos | CellFlags::IntDoorXPos | CellFlags::IntWindowXPos; + break; + } + + cell->flags &= ~flagsToClear; +} diff --git a/src/features/editScene/systems/RoomLayoutSystem.hpp b/src/features/editScene/systems/RoomLayoutSystem.hpp new file mode 100644 index 0000000..bd432ea --- /dev/null +++ b/src/features/editScene/systems/RoomLayoutSystem.hpp @@ -0,0 +1,106 @@ +#pragma once + +#include +#include + +/** + * @brief Room Layout System - processes Room components and generates cell grid + * + * This system replaces the Lua-based room generation from town.cpp. + * It processes in order: + * 1. ClearAreaComponent: Clears cells before generation + * 2. RoomComponent: Creates floor/ceiling cells and interior walls + * 3. Room connections: Creates doors between connected rooms + * 4. Room exits: Creates external walls/doors/windows based on RoomComponent::exits + * 5. Room windows: Creates windows for rooms with createWindows=true + * 6. Exterior generation: Converts interior to exterior on outer edges (always runs last) + * + * When ANY component is modified, ALL components are reprocessed to ensure + * the cell grid is consistent. + * + * The system operates on entities with CellGridComponent (children of Lot/District/Town). + * Rooms should be children of the CellGrid entity. + */ +class RoomLayoutSystem { +public: + RoomLayoutSystem(flecs::world& world, Ogre::SceneManager* sceneMgr); + ~RoomLayoutSystem(); + + // Initialize the system + void initialize(); + + // Update - process dirty rooms and connections + void update(); + +private: + // Process a single room - creates floor, ceiling, interior walls + void processRoom(flecs::entity roomEntity, class RoomComponent& room, + class CellGridComponent& grid); + + // Process connections for a room - creates doors to connected rooms + void processRoomConnections(flecs::entity roomEntity, class RoomComponent& room, + class CellGridComponent& grid); + + // Process a single room connection + void processRoomConnection(flecs::entity roomAEntity, const class RoomComponent& roomA, + flecs::entity roomBEntity, const class RoomComponent& roomB, + class CellGridComponent& grid); + + // Process room exits - creates external walls/doors/windows + void processRoomExits(flecs::entity roomEntity, class RoomComponent& room, + class CellGridComponent& grid); + + // Process room windows for a specific room (called after all doors are created) + void processRoomWindows(flecs::entity roomEntity, class RoomComponent& room, + class CellGridComponent& grid); + + // Process clear area - clears cells before room generation + void processClearArea(flecs::entity clearEntity, class ClearAreaComponent& clearArea, + class CellGridComponent& grid); + + // Process exterior generation - converts interior walls to exterior on outer edges + // This always runs at the end of the pipeline for all rooms + void processExteriorGeneration(class CellGridComponent& grid); + + // Helper: Get the parent CellGridComponent for an entity + class CellGridComponent* findParentGrid(flecs::entity entity); + + // Helper: Find the room entity that contains a given cell position + flecs::entity findRoomAt(flecs::entity gridEntity, int x, int y, int z); + + // Helper: Check if a cell at (x,z) has ANY cell in the grid at the adjacent position + // side: 0=Z-, 1=Z+, 2=X-, 3=X+ + bool hasAdjacentCell(class CellGridComponent& grid, int x, int y, int z, int side); + + // Helper: Get all cells on the edge of a room on a specific side + std::vector> getRoomEdgeCells(const class RoomComponent& room, int side); + + // Helper: Check if two cells are adjacent + bool areCellsAdjacent(int x1, int z1, int x2, int z2); + + // Helper: Find adjacent cells between two rooms + void findAdjacentCells(const class RoomComponent& roomA, const class RoomComponent& roomB, + std::vector>& outCellsA, + std::vector>& outCellsB); + + // Helper: Set cell flags for a wall/door/window + void setWallFlags(class CellGridComponent& grid, int x, int y, int z, + int side, bool isExternal, bool isDoor, bool isWindow); + + // Helper: Clear wall flags (for creating openings) + void clearWallFlags(class CellGridComponent& grid, int x, int y, int z, int side); + + flecs::world& m_world; + Ogre::SceneManager* m_sceneMgr; + bool m_initialized = false; + + // Queries + flecs::query m_roomQuery; + flecs::query m_clearAreaQuery; + + // Track if any component was modified - triggers full reprocess + bool m_needsFullReprocess = false; + + // Reprocess all components (called when anything changes) + void reprocessAll(); +}; diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index d6379cf..5c5e9e1 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -199,6 +199,9 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) if (entity.has()) { json["furnitureTemplate"] = serializeFurnitureTemplate(entity); } + if (entity.has()) { + json["clearArea"] = serializeClearArea(entity); + } // Serialize children json["children"] = nlohmann::json::array(); @@ -315,6 +318,9 @@ void SceneSerializer::deserializeEntity(const nlohmann::json& json, flecs::entit if (json.contains("furnitureTemplate")) { deserializeFurnitureTemplate(entity, json["furnitureTemplate"]); } + if (json.contains("clearArea")) { + deserializeClearArea(entity, json["clearArea"]); + } // Add to UI system if provided if (uiSystem) { @@ -1377,9 +1383,14 @@ nlohmann::json SceneSerializer::serializeLot(flecs::entity entity) nlohmann::json SceneSerializer::serializeRoom(flecs::entity entity) { - auto& room = entity.get(); + auto& room = entity.get_mut(); + + // Ensure room has a persistent ID + room.ensurePersistentId(entity.id()); + nlohmann::json json; + json["persistentId"] = room.persistentId; json["minX"] = room.minX; json["minY"] = room.minY; json["minZ"] = room.minZ; @@ -1388,8 +1399,24 @@ nlohmann::json SceneSerializer::serializeRoom(flecs::entity entity) json["maxZ"] = room.maxZ; json["roomType"] = room.roomType; json["tags"] = room.tags; - json["connectedRooms"] = room.connectedRooms; - json["exits"] = room.exits; + json["createFloor"] = room.createFloor; + json["createCeiling"] = room.createCeiling; + json["createInteriorWalls"] = room.createInteriorWalls; + json["createWindows"] = room.createWindows; + + // Serialize exits (bool array for Z-, Z+, X-, X+) + nlohmann::json exits = nlohmann::json::array(); + for (int i = 0; i < 4; i++) { + exits.push_back(room.exits[i]); + } + json["exits"] = exits; + + // Serialize connected room persistent IDs (not entity IDs, as they change on save/load) + nlohmann::json connections = nlohmann::json::array(); + for (const std::string& id : room.connectedRoomIds) { + connections.push_back(id); + } + json["connectedRoomIds"] = connections; return json; } @@ -1610,6 +1637,12 @@ void SceneSerializer::deserializeRoom(flecs::entity entity, const nlohmann::json { RoomComponent room; + // Load persistent ID (auto-generate if not present for backward compatibility) + room.persistentId = json.value("persistentId", ""); + if (room.persistentId.empty()) { + room.ensurePersistentId(entity.id()); + } + room.minX = json.value("minX", 0); room.minY = json.value("minY", 0); room.minZ = json.value("minZ", 0); @@ -1617,6 +1650,22 @@ void SceneSerializer::deserializeRoom(flecs::entity entity, const nlohmann::json room.maxY = json.value("maxY", 0); room.maxZ = json.value("maxZ", 0); room.roomType = json.value("roomType", ""); + room.createFloor = json.value("createFloor", true); + room.createCeiling = json.value("createCeiling", true); + room.createInteriorWalls = json.value("createInteriorWalls", true); + room.createWindows = json.value("createWindows", false); + + // Load exits (bool array for Z-, Z+, X-, X+) + if (json.contains("exits") && json["exits"].is_array()) { + int i = 0; + for (const auto& exitVal : json["exits"]) { + if (i >= 4) break; + if (exitVal.is_boolean()) { + room.exits[i] = exitVal.get(); + } + i++; + } + } if (json.contains("tags") && json["tags"].is_array()) { for (const auto& tag : json["tags"]) { @@ -1626,22 +1675,19 @@ void SceneSerializer::deserializeRoom(flecs::entity entity, const nlohmann::json } } - if (json.contains("connectedRooms") && json["connectedRooms"].is_array()) { - for (const auto& idx : json["connectedRooms"]) { - if (idx.is_number()) { - room.connectedRooms.push_back(idx); - } - } - } - - if (json.contains("exits") && json["exits"].is_array()) { - for (const auto& exit : json["exits"]) { - if (exit.is_number()) { - room.exits.push_back(exit); + // Load connected rooms using persistent IDs + if (json.contains("connectedRoomIds") && json["connectedRoomIds"].is_array()) { + // New format: persistent IDs + for (const auto& id : json["connectedRoomIds"]) { + if (id.is_string()) { + room.connectedRoomIds.push_back(id.get()); } } } + // Backward compatibility: old format used entity IDs which are invalid after reload + // We skip loading these as they would be garbage + room.markDirty(); entity.set(room); } @@ -1689,3 +1735,41 @@ void SceneSerializer::deserializeFurnitureTemplate(flecs::entity entity, const n entity.set(furn); } + +nlohmann::json SceneSerializer::serializeClearArea(flecs::entity entity) +{ + auto& clearArea = entity.get(); + nlohmann::json json; + + json["minX"] = clearArea.minX; + json["minY"] = clearArea.minY; + json["minZ"] = clearArea.minZ; + json["maxX"] = clearArea.maxX; + json["maxY"] = clearArea.maxY; + json["maxZ"] = clearArea.maxZ; + json["clearCells"] = clearArea.clearCells; + json["clearFurniture"] = clearArea.clearFurniture; + json["clearRoofs"] = clearArea.clearRoofs; + json["clearRooms"] = clearArea.clearRooms; + + return json; +} + +void SceneSerializer::deserializeClearArea(flecs::entity entity, const nlohmann::json& json) +{ + ClearAreaComponent clearArea; + + clearArea.minX = json.value("minX", 0); + clearArea.minY = json.value("minY", 0); + clearArea.minZ = json.value("minZ", 0); + clearArea.maxX = json.value("maxX", 1); + clearArea.maxY = json.value("maxY", 1); + clearArea.maxZ = json.value("maxZ", 1); + clearArea.clearCells = json.value("clearCells", true); + clearArea.clearFurniture = json.value("clearFurniture", true); + clearArea.clearRoofs = json.value("clearRoofs", false); + clearArea.clearRooms = json.value("clearRooms", false); + + clearArea.markDirty(); + entity.set(clearArea); +} diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index b9314b6..a493f77 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -63,6 +63,7 @@ private: nlohmann::json serializeRoom(flecs::entity entity); nlohmann::json serializeRoof(flecs::entity entity); nlohmann::json serializeFurnitureTemplate(flecs::entity entity); + nlohmann::json serializeClearArea(flecs::entity entity); // Component deserialization void deserializeTransform(flecs::entity entity, const nlohmann::json& json, flecs::entity parentEntity); @@ -89,6 +90,7 @@ private: void deserializeRoom(flecs::entity entity, const nlohmann::json& json); void deserializeRoof(flecs::entity entity, const nlohmann::json& json); void deserializeFurnitureTemplate(flecs::entity entity, const nlohmann::json& json); + void deserializeClearArea(flecs::entity entity, const nlohmann::json& json); flecs::world& m_world; Ogre::SceneManager* m_sceneMgr; diff --git a/src/features/editScene/ui/ClearAreaEditor.cpp b/src/features/editScene/ui/ClearAreaEditor.cpp new file mode 100644 index 0000000..c86014d --- /dev/null +++ b/src/features/editScene/ui/ClearAreaEditor.cpp @@ -0,0 +1,56 @@ +#include "ClearAreaEditor.hpp" +#include "../components/CellGrid.hpp" +#include + +bool ClearAreaEditor::renderComponent(flecs::entity entity, ClearAreaComponent& clearArea) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("Clear Area", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + ImGui::Text("Area Size: %dx%dx%d", clearArea.getSizeX(), clearArea.getSizeY(), clearArea.getSizeZ()); + + int minPos[3] = { clearArea.minX, clearArea.minY, clearArea.minZ }; + if (ImGui::InputInt3("Min (X/Y/Z)", minPos)) { + clearArea.minX = minPos[0]; + clearArea.minY = minPos[1]; + clearArea.minZ = minPos[2]; + modified = true; + } + + int maxPos[3] = { clearArea.maxX, clearArea.maxY, clearArea.maxZ }; + if (ImGui::InputInt3("Max (X/Y/Z)", maxPos)) { + clearArea.maxX = maxPos[0]; + clearArea.maxY = maxPos[1]; + clearArea.maxZ = maxPos[2]; + modified = true; + } + + ImGui::Separator(); + ImGui::Text("Clear Options:"); + + if (ImGui::Checkbox("Clear Cells (Walls/Floors)", &clearArea.clearCells)) modified = true; + if (ImGui::Checkbox("Clear Furniture", &clearArea.clearFurniture)) modified = true; + if (ImGui::Checkbox("Clear Roofs", &clearArea.clearRoofs)) modified = true; + if (ImGui::Checkbox("Clear Rooms", &clearArea.clearRooms)) modified = true; + + ImGui::Separator(); + + // Status + if (clearArea.processed) { + ImGui::TextColored(ImVec4(0, 1, 0, 1), "Status: Processed"); + } else { + ImGui::TextColored(ImVec4(1, 1, 0, 1), "Status: Pending"); + } + + if (ImGui::Button("Clear Area Now")) { + clearArea.markDirty(); + modified = true; + } + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/ClearAreaEditor.hpp b/src/features/editScene/ui/ClearAreaEditor.hpp new file mode 100644 index 0000000..c822260 --- /dev/null +++ b/src/features/editScene/ui/ClearAreaEditor.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "ComponentEditor.hpp" + +struct ClearAreaComponent; + +class ClearAreaEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, ClearAreaComponent& clearArea) override; + const char* getName() const override { return "Clear Area"; } +}; diff --git a/src/features/editScene/ui/ExteriorGenerationEditor.cpp b/src/features/editScene/ui/ExteriorGenerationEditor.cpp new file mode 100644 index 0000000..bb35f36 --- /dev/null +++ b/src/features/editScene/ui/ExteriorGenerationEditor.cpp @@ -0,0 +1,58 @@ +#include "ExteriorGenerationEditor.hpp" +#include "../components/CellGrid.hpp" +#include + +bool ExteriorGenerationEditor::renderComponent(flecs::entity entity, ExteriorGenerationComponent& exterior) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("Exterior Generation", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + ImGui::Text("Converts interior walls to exterior on building outer edges"); + ImGui::Text("This runs AFTER all room processing is complete."); + + ImGui::Separator(); + + // Floor Y + if (ImGui::InputInt("Floor Y", &exterior.floorY)) { + modified = true; + } + + ImGui::Separator(); + ImGui::Text("Generate:"); + + if (ImGui::Checkbox("Walls", &exterior.generateWalls)) modified = true; + if (ImGui::Checkbox("Doors", &exterior.generateDoors)) modified = true; + if (ImGui::Checkbox("Windows", &exterior.generateWindows)) modified = true; + + ImGui::Separator(); + + // Target rooms + ImGui::Text("Target Rooms: %zu", exterior.targetRooms.size()); + ImGui::TextDisabled("(Empty = all rooms)"); + if (!exterior.targetRooms.empty()) { + for (size_t i = 0; i < exterior.targetRooms.size(); i++) { + ImGui::Text(" Room %zu: ID %lu", i, exterior.targetRooms[i]); + } + } + + ImGui::Separator(); + + // Status + if (exterior.processed) { + ImGui::TextColored(ImVec4(0, 1, 0, 1), "Status: Processed"); + } else { + ImGui::TextColored(ImVec4(1, 1, 0, 1), "Status: Pending"); + } + + if (ImGui::Button("Generate Exterior Now")) { + exterior.markDirty(); + modified = true; + } + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/ExteriorGenerationEditor.hpp b/src/features/editScene/ui/ExteriorGenerationEditor.hpp new file mode 100644 index 0000000..c46394e --- /dev/null +++ b/src/features/editScene/ui/ExteriorGenerationEditor.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "ComponentEditor.hpp" + +struct ExteriorGenerationComponent; + +class ExteriorGenerationEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, ExteriorGenerationComponent& exterior) override; + const char* getName() const override { return "Exterior Generation"; } +}; diff --git a/src/features/editScene/ui/RoomEditor.cpp b/src/features/editScene/ui/RoomEditor.cpp index d9cea84..5f240e4 100644 --- a/src/features/editScene/ui/RoomEditor.cpp +++ b/src/features/editScene/ui/RoomEditor.cpp @@ -1,14 +1,26 @@ #include "RoomEditor.hpp" #include "../components/CellGrid.hpp" +#include "../components/EntityName.hpp" #include +#include bool RoomEditor::renderComponent(flecs::entity entity, RoomComponent& room) { bool modified = false; + // Ensure room has a persistent ID + room.ensurePersistentId(entity.id()); + if (ImGui::CollapsingHeader("Room", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Indent(); + // Display room identification info + ImGui::TextDisabled("Entity ID: %lu", entity.id()); + ImGui::TextDisabled("Persistent ID: %s", room.persistentId.c_str()); + ImGui::Separator(); + + ImGui::Text("Room Size: %dx%dx%d", room.getSizeX(), room.getSizeY(), room.getSizeZ()); + int minPos[3] = { room.minX, room.minY, room.minZ }; if (ImGui::InputInt3("Min (X/Y/Z)", minPos)) { room.minX = minPos[0]; @@ -25,6 +37,15 @@ bool RoomEditor::renderComponent(flecs::entity entity, RoomComponent& room) modified = true; } + // Creation options + if (ImGui::Checkbox("Create Floor", &room.createFloor)) modified = true; + if (ImGui::Checkbox("Create Ceiling", &room.createCeiling)) modified = true; + if (ImGui::Checkbox("Create Interior Walls", &room.createInteriorWalls)) modified = true; + if (ImGui::Checkbox("Create Windows", &room.createWindows)) modified = true; + ImGui::TextDisabled("(Windows are created after all doors)"); + + ImGui::Separator(); + char roomType[128] = {0}; strncpy(roomType, room.roomType.c_str(), sizeof(roomType) - 1); if (ImGui::InputText("Room Type", roomType, sizeof(roomType))) { @@ -60,14 +81,185 @@ bool RoomEditor::renderComponent(flecs::entity entity, RoomComponent& room) modified = true; } - // Connected rooms - ImGui::Text("Connected Rooms: %zu", room.connectedRooms.size()); - for (size_t i = 0; i < room.connectedRooms.size(); ++i) { - ImGui::Text(" Room %d", room.connectedRooms[i]); + ImGui::Unindent(); + } + + // Exits Section + if (ImGui::CollapsingHeader("Exits (To Outside)", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + ImGui::Text("Create exit doors to outside:"); + ImGui::TextDisabled("(Internal doors on outside-facing walls)"); + ImGui::TextDisabled("(Converted to exterior by Exterior Generation)"); + + const char* sideNames[] = { "Z- (North)", "Z+ (South)", "X- (West)", "X+ (East)" }; + + for (int i = 0; i < 4; i++) { + ImGui::PushID(i); + if (ImGui::Checkbox(sideNames[i], &room.exits[i])) { + modified = true; + } + ImGui::PopID(); + } + + // Quick presets + ImGui::Separator(); + if (ImGui::Button("All Exits")) { + room.setAllExits(true); + modified = true; + } + ImGui::SameLine(); + if (ImGui::Button("No Exits")) { + room.setAllExits(false); + modified = true; } ImGui::Unindent(); } + // Connections Section + if (ImGui::CollapsingHeader("Connections", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + ImGui::Text("Connected to %zu room(s):", room.connectedRoomIds.size()); + + // Build a map of persistent ID -> room info for displaying connections + std::unordered_map roomMap; + std::unordered_map roomLabels; + flecs::entity parent = entity.parent(); + if (parent.is_alive()) { + parent.children([&](flecs::entity child) { + if (child.has()) { + const auto& r = child.get(); + std::string label = formatRoomLabel(child, r); + roomMap[r.persistentId] = child; + roomLabels[r.persistentId] = label; + } + }); + } + + // List current connections with remove buttons + int removeIndex = -1; + for (size_t i = 0; i < room.connectedRoomIds.size(); i++) { + const std::string& connectedId = room.connectedRoomIds[i]; + ImGui::PushID((int)i); + + auto it = roomLabels.find(connectedId); + if (it != roomLabels.end()) { + ImGui::Text(" %zu: %s", i + 1, it->second.c_str()); + } else { + ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), + " %zu: (Invalid room ID: %s)", i + 1, connectedId.c_str()); + } + + ImGui::SameLine(); + if (ImGui::SmallButton("Remove")) { + removeIndex = (int)i; + modified = true; + } + ImGui::PopID(); + } + + if (removeIndex >= 0) { + // Remove from this room + std::string removedId = room.connectedRoomIds[removeIndex]; + room.connectedRoomIds.erase(room.connectedRoomIds.begin() + removeIndex); + + // Also remove this room from the other room's list (bidirectional) + auto it = roomMap.find(removedId); + if (it != roomMap.end() && it->second.is_alive() && it->second.has()) { + auto& otherRoom = it->second.get_mut(); + otherRoom.removeConnection(room.persistentId); + } + } + + ImGui::Separator(); + + // Add connection section + ImGui::Text("Add Connection:"); + + // Find all available rooms in the same parent + std::vector availableRooms; + std::vector availableRoomLabels; + + if (parent.is_alive()) { + parent.children([&](flecs::entity child) { + if (child.has() && child != entity) { + const auto& r = child.get(); + // Check if not already connected + if (!room.isConnectedTo(r.persistentId)) { + availableRooms.push_back(child); + availableRoomLabels.push_back(formatRoomLabel(child, r)); + } + } + }); + } + + if (availableRooms.empty()) { + ImGui::TextDisabled(" No available rooms to connect"); + } else { + static int selectedRoomIdx = 0; + if (selectedRoomIdx >= (int)availableRooms.size()) selectedRoomIdx = 0; + + // Build combo items + std::string comboItems; + for (size_t i = 0; i < availableRoomLabels.size(); i++) { + if (i > 0) comboItems += "\0"; + comboItems += availableRoomLabels[i]; + } + comboItems += "\0"; + + ImGui::PushID("add_conn"); + if (ImGui::Combo("Room", &selectedRoomIdx, comboItems.c_str())) { + // Selection changed + } + + ImGui::SameLine(); + if (ImGui::Button("Connect")) { + flecs::entity selectedRoom = availableRooms[selectedRoomIdx]; + const auto& selectedRoomComp = selectedRoom.get(); + + // Add bidirectional connection using persistent IDs + room.addConnection(selectedRoomComp.persistentId); + auto& otherRoom = selectedRoom.get_mut(); + otherRoom.addConnection(room.persistentId); + + modified = true; + } + ImGui::PopID(); + } + + ImGui::Unindent(); + } + + if (modified) { + room.markDirty(); + } + return modified; } + +std::string RoomEditor::formatRoomLabel(flecs::entity entity, const RoomComponent& room) +{ + // Format: "Name [E:entityId P:persistentId] (Type)" + std::string label; + + // Try to get entity name if available + if (entity.has()) { + label = entity.get().name; + } + + if (label.empty()) { + label = room.roomType.empty() ? "Unnamed" : room.roomType; + } + + // Add entity ID and persistent ID + label += " [E:" + std::to_string(entity.id()) + " P:" + room.persistentId.substr(0, 8) + "]"; + + // Add room type if different from name + if (!room.roomType.empty() && label.find(room.roomType) == std::string::npos) { + label += " (" + room.roomType + ")"; + } + + return label; +} diff --git a/src/features/editScene/ui/RoomEditor.hpp b/src/features/editScene/ui/RoomEditor.hpp index efda3c7..339c2fe 100644 --- a/src/features/editScene/ui/RoomEditor.hpp +++ b/src/features/editScene/ui/RoomEditor.hpp @@ -1,6 +1,8 @@ #pragma once #include "ComponentEditor.hpp" +#include +#include struct RoomComponent; @@ -8,4 +10,9 @@ class RoomEditor : public ComponentEditor { public: bool renderComponent(flecs::entity entity, RoomComponent& room) override; const char* getName() const override { return "Room"; } + +private: + // Format a room label for display in connection lists + // Format: "Name [E:entityId P:persistentId] (Type)" + std::string formatRoomLabel(flecs::entity entity, const RoomComponent& room); }; diff --git a/src/features/editScene/ui/RoomWindowsEditor.cpp b/src/features/editScene/ui/RoomWindowsEditor.cpp new file mode 100644 index 0000000..97e3f42 --- /dev/null +++ b/src/features/editScene/ui/RoomWindowsEditor.cpp @@ -0,0 +1,71 @@ +#include "RoomWindowsEditor.hpp" +#include "../components/CellGrid.hpp" +#include + +bool RoomWindowsEditor::renderComponent(flecs::entity entity, RoomWindowsComponent& windows) +{ + bool modified = false; + + if (ImGui::CollapsingHeader("Room Windows", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + // Linked room + ImGui::Text("Linked Room:"); + if (windows.room.is_alive()) { + ImGui::Text(" Entity ID: %llu", windows.room.id()); + if (windows.room.has()) { + const auto& room = windows.room.get(); + ImGui::Text(" Type: %s", room.roomType.c_str()); + } + } else { + ImGui::TextDisabled(" (Not set)"); + } + + ImGui::Separator(); + + // Floor Y + if (ImGui::InputInt("Floor Y", &windows.floorY)) { + modified = true; + } + + ImGui::Separator(); + + // Window sides + ImGui::Text("Create windows on sides:"); + if (ImGui::Checkbox("Z- (North)", &windows.windowZNeg)) modified = true; + if (ImGui::Checkbox("Z+ (South)", &windows.windowZPos)) modified = true; + if (ImGui::Checkbox("X- (West)", &windows.windowXNeg)) modified = true; + if (ImGui::Checkbox("X+ (East)", &windows.windowXPos)) modified = true; + + // Quick presets + ImGui::Separator(); + ImGui::Text("Quick Presets:"); + if (ImGui::Button("All Sides")) { + windows.setAllSides(true); + modified = true; + } + ImGui::SameLine(); + if (ImGui::Button("None")) { + windows.setAllSides(false); + modified = true; + } + + ImGui::Separator(); + + // Status + if (windows.processed) { + ImGui::TextColored(ImVec4(0, 1, 0, 1), "Status: Processed"); + } else { + ImGui::TextColored(ImVec4(1, 1, 0, 1), "Status: Pending"); + } + + if (ImGui::Button("Reprocess Windows")) { + windows.markDirty(); + modified = true; + } + + ImGui::Unindent(); + } + + return modified; +} diff --git a/src/features/editScene/ui/RoomWindowsEditor.hpp b/src/features/editScene/ui/RoomWindowsEditor.hpp new file mode 100644 index 0000000..c447b3b --- /dev/null +++ b/src/features/editScene/ui/RoomWindowsEditor.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "ComponentEditor.hpp" + +struct RoomWindowsComponent; + +class RoomWindowsEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, RoomWindowsComponent& windows) override; + const char* getName() const override { return "Room Windows"; } +};