Almost good generation of room layout

This commit is contained in:
2026-04-13 04:03:48 +03:00
parent da4a1a6722
commit a955f0b218
19 changed files with 1613 additions and 31 deletions

View File

@@ -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

View File

@@ -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<CellGridSystem>(m_world, m_sceneMgr);
m_cellGridSystem->initialize();
// Setup RoomLayout system
m_roomLayoutSystem = std::make_unique<RoomLayoutSystem>(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();
}

View File

@@ -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<ProceduralMaterialSystem> m_proceduralMaterialSystem;
std::unique_ptr<ProceduralMeshSystem> m_proceduralMeshSystem;
std::unique_ptr<CellGridSystem> m_cellGridSystem;
std::unique_ptr<RoomLayoutSystem> m_roomLayoutSystem;
// State
uint16_t m_currentModifiers;

View File

@@ -4,6 +4,7 @@
#include <vector>
#include <string>
#include <unordered_map>
#include <chrono>
#include <flecs.h>
#include <Ogre.h>
@@ -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<std::string> tags;
// Connected room indices
std::vector<int> 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<int> 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<std::string> connectedRoomIds;
// Exit doors for outside-facing walls (0=Z-, 1=Z+, 2=X-, 3=X+)
// An exit is always a door placed on a wall that faces outside (not connected to another room)
bool exits[4] = { false, false, false, false }; // Z-, Z+, X-, X+
// Generate a persistent ID if one doesn't exist
void ensurePersistentId(flecs::entity_t entityId = 0) {
if (persistentId.empty()) {
// Generate unique ID based on timestamp + random + optional entity ID
auto now = std::chrono::high_resolution_clock::now();
auto nanos = std::chrono::duration_cast<std::chrono::nanoseconds>(
now.time_since_epoch()).count();
persistentId = "room_" + std::to_string(nanos);
if (entityId != 0) {
persistentId += "_" + std::to_string(entityId);
}
}
}
// Add a connection to another room (bidirectional, by persistent ID)
void addConnection(const std::string& otherRoomId) {
// Check if not already connected
for (const auto& id : connectedRoomIds) {
if (id == otherRoomId) return;
}
connectedRoomIds.push_back(otherRoomId);
}
// Remove a connection to another room
void removeConnection(const std::string& otherRoomId) {
connectedRoomIds.erase(
std::remove(connectedRoomIds.begin(), connectedRoomIds.end(), otherRoomId),
connectedRoomIds.end()
);
}
// Check if connected to a room (by persistent ID)
bool isConnectedTo(const std::string& otherRoomId) const {
for (const auto& id : connectedRoomIds) {
if (id == otherRoomId) return true;
}
return false;
}
// Helper to set all exits at once
void setAllExits(bool value) {
for (int i = 0; i < 4; i++) {
exits[i] = value;
}
}
// Helper to set a specific exit
void setExit(int side, bool value) {
if (side >= 0 && side < 4) {
exits[side] = value;
}
}
};
/**
* @brief Room exits - defines external exits from a room
*
* This replaces the Lua create_exit0/1/2/3() functions.
* Creates external walls/doors/windows on the room edges that face outside.
*/
/**
* @brief Clear Area - clears cells in a region before room generation
*
* This replaces the Lua clear_area() and clear_furniture_area() functions.
* It clears all cells within the specified bounds before any room generation happens.
* This component is processed first in the RoomLayoutSystem.
*/
struct ClearAreaComponent {
// Area bounds (in cell coordinates, inclusive min, exclusive max)
int minX = 0, minY = 0, minZ = 0;
int maxX = 1, maxY = 1, maxZ = 1;
// What to clear
bool clearCells = true; // Clear cell flags (walls, floors, etc.)
bool clearFurniture = true; // Clear furniture placements
bool clearRoofs = false; // Clear roof definitions (children with RoofComponent)
bool clearRooms = false; // Remove existing room entities (children with RoomComponent)
// Processed flag - cleared each time dirty is set
bool processed = false;
// Dirty flag - clear again when true
bool dirty = true;
void markDirty() { dirty = true; }
// Helper to set bounds
void setBounds(int x1, int z1, int x2, int z2, int y = 0) {
minX = x1; minZ = z1; minY = y;
maxX = x2; maxZ = z2; maxY = y + 1;
}
// Helper to get size
int getSizeX() const { return maxX - minX; }
int getSizeY() const { return maxY - minY; }
int getSizeZ() const { return maxZ - minZ; }
};
/**

View File

@@ -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<RoomComponent>(
"Room", "Cell Grid",
"Room", "Room Layout",
std::make_unique<RoomEditor>(),
[sceneMgr](flecs::entity e) {
if (!e.has<RoomComponent>()) {
@@ -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<ClearAreaComponent>(
"Clear Area", "Room Layout",
std::make_unique<ClearAreaEditor>(),
[sceneMgr](flecs::entity e) {
if (!e.has<ClearAreaComponent>()) {
e.set<ClearAreaComponent>({});
}
},
[sceneMgr](flecs::entity e) {
if (e.has<ClearAreaComponent>()) {
e.remove<ClearAreaComponent>();
}
}
);
}
// Note: ExteriorGenerationComponent has been removed.
// Exterior generation now automatically runs at the end of the RoomLayoutSystem pipeline.
// This mirrors the original Lua behavior where create_exterior() was called after all rooms were defined.

View File

@@ -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]

View File

@@ -655,7 +655,22 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
// Render Room if present
if (entity.has<RoomComponent>()) {
auto &room = entity.get_mut<RoomComponent>();
m_componentRegistry.render<RoomComponent>(entity, room);
if (m_componentRegistry.render<RoomComponent>(entity, room)) {
room.markDirty();
}
componentCount++;
}
// Render ClearArea if present
if (entity.has<ClearAreaComponent>()) {
auto &clearArea = entity.get_mut<ClearAreaComponent>();
if (m_componentRegistry.render<ClearAreaComponent>(entity, clearArea)) {
clearArea.markDirty();
}
componentCount++;
}

View File

@@ -0,0 +1,767 @@
#include "RoomLayoutSystem.hpp"
#include "../components/CellGrid.hpp"
#include <OgreLogManager.h>
#include <climits>
#include <set>
#include <unordered_map>
RoomLayoutSystem::RoomLayoutSystem(flecs::world& world, Ogre::SceneManager* sceneMgr)
: m_world(world)
, m_sceneMgr(sceneMgr)
, m_roomQuery(world.query<RoomComponent>())
, m_clearAreaQuery(world.query<ClearAreaComponent>())
{
}
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<std::string, flecs::entity> 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<std::pair<std::string, std::string>> 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<RoomComponent>()) {
processRoomConnection(roomEntity, room, otherRoom, otherRoom.get<RoomComponent>(), *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<flecs::entity> toRemove;
gridEntity.children([&](flecs::entity child) {
if (child.has<RoofComponent>()) {
const auto& roof = child.get<RoofComponent>();
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<flecs::entity> toRemove;
gridEntity.children([&](flecs::entity child) {
if (child.has<RoomComponent>()) {
const auto& room = child.get<RoomComponent>();
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<RoomComponent>()) return true;
const auto& room = roomEntity.get<RoomComponent>();
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<std::pair<int, int>> 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<CellGridComponent>()) {
// get_mut returns a reference, convert to pointer
return &current.get_mut<CellGridComponent>();
}
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<RoomComponent>()) {
const auto& room = child.get<RoomComponent>();
if (room.contains(x, y, z)) {
result = child;
return false;
}
}
return true;
});
return result;
}
std::vector<std::pair<int, int>> RoomLayoutSystem::getRoomEdgeCells(const RoomComponent& room, int side)
{
std::vector<std::pair<int, int>> 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<std::pair<int, int>>& outCellsA,
std::vector<std::pair<int, int>>& 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;
}

View File

@@ -0,0 +1,106 @@
#pragma once
#include <flecs.h>
#include <Ogre.h>
/**
* @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<std::pair<int, int>> 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<std::pair<int, int>>& outCellsA,
std::vector<std::pair<int, int>>& 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<class RoomComponent> m_roomQuery;
flecs::query<class ClearAreaComponent> m_clearAreaQuery;
// Track if any component was modified - triggers full reprocess
bool m_needsFullReprocess = false;
// Reprocess all components (called when anything changes)
void reprocessAll();
};

View File

@@ -199,6 +199,9 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
if (entity.has<FurnitureTemplateComponent>()) {
json["furnitureTemplate"] = serializeFurnitureTemplate(entity);
}
if (entity.has<ClearAreaComponent>()) {
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<RoomComponent>();
auto& room = entity.get_mut<RoomComponent>();
// 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<bool>();
}
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<std::string>());
}
}
}
// 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<RoomComponent>(room);
}
@@ -1689,3 +1735,41 @@ void SceneSerializer::deserializeFurnitureTemplate(flecs::entity entity, const n
entity.set<FurnitureTemplateComponent>(furn);
}
nlohmann::json SceneSerializer::serializeClearArea(flecs::entity entity)
{
auto& clearArea = entity.get<ClearAreaComponent>();
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<ClearAreaComponent>(clearArea);
}

View File

@@ -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;

View File

@@ -0,0 +1,56 @@
#include "ClearAreaEditor.hpp"
#include "../components/CellGrid.hpp"
#include <imgui.h>
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;
}

View File

@@ -0,0 +1,11 @@
#pragma once
#include "ComponentEditor.hpp"
struct ClearAreaComponent;
class ClearAreaEditor : public ComponentEditor<ClearAreaComponent> {
public:
bool renderComponent(flecs::entity entity, ClearAreaComponent& clearArea) override;
const char* getName() const override { return "Clear Area"; }
};

View File

@@ -0,0 +1,58 @@
#include "ExteriorGenerationEditor.hpp"
#include "../components/CellGrid.hpp"
#include <imgui.h>
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;
}

View File

@@ -0,0 +1,11 @@
#pragma once
#include "ComponentEditor.hpp"
struct ExteriorGenerationComponent;
class ExteriorGenerationEditor : public ComponentEditor<ExteriorGenerationComponent> {
public:
bool renderComponent(flecs::entity entity, ExteriorGenerationComponent& exterior) override;
const char* getName() const override { return "Exterior Generation"; }
};

View File

@@ -1,14 +1,26 @@
#include "RoomEditor.hpp"
#include "../components/CellGrid.hpp"
#include "../components/EntityName.hpp"
#include <imgui.h>
#include <algorithm>
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<std::string, flecs::entity> roomMap;
std::unordered_map<std::string, std::string> roomLabels;
flecs::entity parent = entity.parent();
if (parent.is_alive()) {
parent.children([&](flecs::entity child) {
if (child.has<RoomComponent>()) {
const auto& r = child.get<RoomComponent>();
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<RoomComponent>()) {
auto& otherRoom = it->second.get_mut<RoomComponent>();
otherRoom.removeConnection(room.persistentId);
}
}
ImGui::Separator();
// Add connection section
ImGui::Text("Add Connection:");
// Find all available rooms in the same parent
std::vector<flecs::entity> availableRooms;
std::vector<std::string> availableRoomLabels;
if (parent.is_alive()) {
parent.children([&](flecs::entity child) {
if (child.has<RoomComponent>() && child != entity) {
const auto& r = child.get<RoomComponent>();
// 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<RoomComponent>();
// Add bidirectional connection using persistent IDs
room.addConnection(selectedRoomComp.persistentId);
auto& otherRoom = selectedRoom.get_mut<RoomComponent>();
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<EntityNameComponent>()) {
label = entity.get<EntityNameComponent>().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;
}

View File

@@ -1,6 +1,8 @@
#pragma once
#include "ComponentEditor.hpp"
#include <string>
#include <unordered_map>
struct RoomComponent;
@@ -8,4 +10,9 @@ class RoomEditor : public ComponentEditor<RoomComponent> {
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);
};

View File

@@ -0,0 +1,71 @@
#include "RoomWindowsEditor.hpp"
#include "../components/CellGrid.hpp"
#include <imgui.h>
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<RoomComponent>()) {
const auto& room = windows.room.get<RoomComponent>();
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;
}

View File

@@ -0,0 +1,11 @@
#pragma once
#include "ComponentEditor.hpp"
struct RoomWindowsComponent;
class RoomWindowsEditor : public ComponentEditor<RoomWindowsComponent> {
public:
bool renderComponent(flecs::entity entity, RoomWindowsComponent& windows) override;
const char* getName() const override { return "Room Windows"; }
};