Furniture enabled

This commit is contained in:
2026-04-14 18:54:28 +03:00
parent 611dcd0d46
commit febeb8ff8d
10 changed files with 878 additions and 24 deletions

View File

@@ -24,6 +24,7 @@ set(EDITSCENE_SOURCES
systems/ProceduralMeshSystem.cpp
systems/CellGridSystem.cpp
systems/RoomLayoutSystem.cpp
systems/FurnitureLibrary.cpp
ui/TransformEditor.cpp
ui/RenderableEditor.cpp
ui/PhysicsColliderEditor.cpp
@@ -87,6 +88,7 @@ set(EDITSCENE_HEADERS
systems/EditorUISystem.hpp
systems/CellGridSystem.hpp
systems/RoomLayoutSystem.hpp
systems/FurnitureLibrary.hpp
systems/ProceduralMaterialSystem.hpp
systems/ProceduralMeshSystem.hpp
systems/ProceduralTextureSystem.hpp

View File

@@ -1,9 +1,11 @@
#include "CellGridSystem.hpp"
#include "FurnitureLibrary.hpp"
#include "../components/CellGrid.hpp"
#include "../components/TriangleBuffer.hpp"
#include "../components/Transform.hpp"
#include "../components/StaticGeometry.hpp"
#include "../components/StaticGeometryMember.hpp"
#include <OgreStaticGeometry.h>
#include "../components/ProceduralMaterial.hpp"
#include "../components/ProceduralTexture.hpp"
#include <OgreSceneManager.h>
@@ -485,6 +487,18 @@ void CellGridSystem::buildCellGrid(flecs::entity entity,
" roofTop=" + std::to_string(roofTopTb.getVertices().size()) +
" roofSide=" + std::to_string(roofSideTb.getVertices().size()));
// Build furniture
try {
buildFurniture(entity, grid);
} catch (const std::exception &e) {
Ogre::LogManager::getSingleton().logMessage(
"CellGrid: Error building furniture: " +
std::string(e.what()));
} catch (...) {
Ogre::LogManager::getSingleton().logMessage(
"CellGrid: Unknown error building furniture");
}
// Build window and door frames
try {
buildFrames(entity, grid, materialName);
@@ -2251,6 +2265,108 @@ Ogre::MeshPtr CellGridSystem::convertToMesh(const std::string &name,
}
}
void CellGridSystem::buildFurniture(flecs::entity entity,
const CellGridComponent &grid)
{
destroyFurniture(entity);
if (!entity.has<TransformComponent>())
return;
auto &transform = entity.get_mut<TransformComponent>();
if (!transform.node)
return;
FurnitureLibrary::getInstance().loadAll();
auto &meshData = m_entityMeshes[entity.id()];
for (const auto &fcell : grid.furnitureCells) {
const FurnitureDefinition *def =
FurnitureLibrary::getInstance().findByName(
fcell.furnitureType);
if (!def) {
// Fallback: try to find by exact mesh name
for (const auto &d :
FurnitureLibrary::getInstance().getDefinitions()) {
if (d.meshName == fcell.furnitureType ||
d.name == fcell.furnitureType) {
def = &d;
break;
}
}
}
if (!def)
continue;
Ogre::MeshPtr mesh =
Ogre::MeshManager::getSingleton().getByName(
def->meshName);
if (!mesh) {
Ogre::String group =
Ogre::ResourceGroupManager::getSingleton()
.findGroupContainingResource(
def->meshName);
if (!group.empty()) {
mesh = Ogre::MeshManager::getSingleton().load(
def->meshName, group);
}
}
if (!mesh) {
Ogre::LogManager::getSingleton().logMessage(
"CellGrid: Could not load furniture mesh '" +
def->meshName + "'");
continue;
}
Ogre::Entity *ent =
m_sceneMgr->createEntity(def->meshName);
if (!ent)
continue;
Ogre::SceneNode *fnode =
transform.node->createChildSceneNode();
Ogre::Vector3 pos = grid.cellToWorld(fcell.x, fcell.y,
fcell.z);
// Apply small Y offset so furniture sits on floor
pos.y += 0.05f;
fnode->setPosition(pos);
fnode->setOrientation(Ogre::Quaternion(
Ogre::Degree(90.0f * fcell.rotation),
Ogre::Vector3::UNIT_Y));
fnode->attachObject(ent);
ent->setRenderingDistance(100.0f);
// Try to set material if mesh has submesh material
if (!mesh->getSubMesh(0)->getMaterialName().empty()) {
ent->setMaterialName(
mesh->getSubMesh(0)->getMaterialName());
}
meshData.furnitureEntities.push_back(ent);
}
}
void CellGridSystem::destroyFurniture(flecs::entity entity)
{
auto it = m_entityMeshes.find(entity.id());
if (it == m_entityMeshes.end())
return;
for (auto *ent : it->second.furnitureEntities) {
if (!ent)
continue;
Ogre::SceneNode *node = ent->getParentSceneNode();
if (node) {
node->detachObject(ent);
m_sceneMgr->destroySceneNode(node);
}
try {
m_sceneMgr->destroyEntity(ent);
} catch (...) {
}
}
it->second.furnitureEntities.clear();
}
void CellGridSystem::destroyCellGridMeshes(CellGridComponent &grid)
{
// This is called from buildCellGrid which uses the entity ID as key in m_entityMeshes
@@ -2305,6 +2421,9 @@ void CellGridSystem::destroyCellGridMeshes(flecs::entity entity)
removeMesh(name);
}
// Destroy furniture
destroyFurniture(entity);
// Destroy frames
destroyFrames(entity);
@@ -3100,7 +3219,7 @@ void CellGridSystem::buildFrames(flecs::entity entity,
parent = parent.parent();
}
// Get the transform node to attach frames to
// Get the transform for frame positioning
Ogre::SceneNode *transformNode = nullptr;
if (entity.has<TransformComponent>()) {
auto &transform = entity.get<TransformComponent>();
@@ -3111,32 +3230,51 @@ void CellGridSystem::buildFrames(flecs::entity entity,
"CellGrid: No transform node for frames!");
return;
}
// Get parent transform for world position calculation
Ogre::Vector3 parentPos = transformNode->_getDerivedPosition();
Ogre::Quaternion parentRot = transformNode->_getDerivedOrientation();
Ogre::Vector3 parentScale = transformNode->_getDerivedScale();
// Create frame meshes with unique names per entity
std::string meshPrefix =
"CellGrid_" + std::to_string(entity.id()) + "_";
createWindowFrameMeshes(grid, materialName, meshPrefix, materialEntity);
createDoorFrameMeshes(grid, materialName, meshPrefix, materialEntity);
// Place frames as child entities of the transform node
// This way they move automatically when the parent transform changes
std::vector<Ogre::Entity *> frameEntities;
placeWindowFrames(entity, grid, transformNode, frameEntities);
placeDoorFrames(entity, grid, transformNode, frameEntities);
// Create or get StaticGeometry region for frames
std::string regionName = "FrameRegion_" + std::to_string(entity.id());
Ogre::StaticGeometry* staticGeom = nullptr;
auto it = m_entityMeshes.find(entity.id());
if (it != m_entityMeshes.end() && it->second.frameStaticGeometry) {
// Reuse existing static geometry - destroy it first
m_sceneMgr->destroyStaticGeometry(it->second.frameStaticGeometry);
it->second.frameStaticGeometry = nullptr;
}
staticGeom = m_sceneMgr->createStaticGeometry(regionName);
staticGeom->setCastShadows(true);
staticGeom->setRegionDimensions(Ogre::Vector3(1000.0f)); // Large region for all frames
// Place frames into StaticGeometry (batched into single draw call)
int frameCount = 0;
placeWindowFramesInStaticGeometry(entity, grid, parentPos, parentRot, parentScale, staticGeom, frameCount);
placeDoorFramesInStaticGeometry(entity, grid, parentPos, parentRot, parentScale, staticGeom, frameCount);
// Build the static geometry
staticGeom->build();
Ogre::LogManager::getSingleton().logMessage(
"CellGrid: Placed " + std::to_string(frameEntities.size()) +
" frame entities");
"CellGrid: Batched " + std::to_string(frameCount) +
" frames into StaticGeometry");
// Store reference
auto it = m_entityMeshes.find(entity.id());
if (it != m_entityMeshes.end()) {
it->second.frameEntities = std::move(frameEntities);
it->second.frameStaticGeometry = staticGeom;
} else {
// Clean up if we can't store them
for (auto *ent : frameEntities) {
m_sceneMgr->destroyEntity(ent);
}
// Clean up if we can't store it
m_sceneMgr->destroyStaticGeometry(staticGeom);
}
}
@@ -3704,7 +3842,16 @@ void CellGridSystem::destroyFrames(flecs::entity entity)
{
auto it = m_entityMeshes.find(entity.id());
if (it != m_entityMeshes.end()) {
// Destroy frame entities (and their child nodes)
// Destroy StaticGeometry if present (replaces individual entities)
if (it->second.frameStaticGeometry) {
try {
m_sceneMgr->destroyStaticGeometry(it->second.frameStaticGeometry);
} catch (...) {
}
it->second.frameStaticGeometry = nullptr;
}
// Destroy old frame entities (legacy cleanup)
for (auto *ent : it->second.frameEntities) {
if (ent) {
// Get the node that owns this entity
@@ -3748,3 +3895,189 @@ void CellGridSystem::destroyFrames(flecs::entity entity)
}
}
}
// ============================================================================
// StaticGeometry Frame Placement (Batched for better performance)
// ============================================================================
void CellGridSystem::placeWindowFramesInStaticGeometry(
flecs::entity entity, const CellGridComponent &grid,
const Ogre::Vector3 &parentPos, const Ogre::Quaternion &parentRot,
const Ogre::Vector3 &parentScale, Ogre::StaticGeometry *staticGeom,
int &frameCount)
{
std::string meshPrefix =
"CellGrid_" + std::to_string(entity.id()) + "_";
Ogre::MeshPtr extMesh = Ogre::MeshManager::getSingleton().getByName(
meshPrefix + "window_external");
Ogre::MeshPtr intMesh = Ogre::MeshManager::getSingleton().getByName(
meshPrefix + "window_internal");
if (!extMesh && !intMesh)
return;
float halfCell = grid.cellSize / 2.0f;
float frameOffset = 0.0f;
float windowHeight = 2.0f * (grid.cellHeight / 4.0f);
float windowBottomOffset = 0.8f * (grid.cellHeight / 4.0f);
float windowY = windowBottomOffset;
for (const auto &cell : grid.cells) {
Ogre::Vector3 origin = grid.cellToWorld(cell.x, cell.y, cell.z);
auto addFrame = [&](Ogre::MeshPtr mesh, const Ogre::Vector3 &localPos,
const Ogre::Quaternion &localRot) {
if (!mesh)
return;
// Calculate world position relative to parent
Ogre::Vector3 worldPos = parentPos + (parentRot * (localPos * parentScale));
Ogre::Quaternion worldRot = parentRot * localRot;
// Create temporary entity to add to StaticGeometry
Ogre::Entity* tempEnt = m_sceneMgr->createEntity(mesh->getName());
staticGeom->addEntity(tempEnt, worldPos, worldRot, parentScale);
m_sceneMgr->destroyEntity(tempEnt); // StaticGeometry copies the data
frameCount++;
};
if (extMesh) {
frameOffset = 0.1f;
if (cell.hasFlag(CellFlags::WindowXNeg)) {
Ogre::Vector3 pos = origin + Ogre::Vector3(-halfCell - frameOffset, windowY, 0);
Ogre::Quaternion rot(Ogre::Degree(-90), Ogre::Vector3::UNIT_Y);
addFrame(extMesh, pos, rot);
}
if (cell.hasFlag(CellFlags::WindowXPos)) {
Ogre::Vector3 pos = origin + Ogre::Vector3(halfCell + frameOffset, windowY, 0);
Ogre::Quaternion rot(Ogre::Degree(90), Ogre::Vector3::UNIT_Y);
addFrame(extMesh, pos, rot);
}
if (cell.hasFlag(CellFlags::WindowZNeg)) {
Ogre::Vector3 pos = origin + Ogre::Vector3(0, windowY, -halfCell - frameOffset);
Ogre::Quaternion rot(Ogre::Degree(180), Ogre::Vector3::UNIT_Y);
addFrame(extMesh, pos, rot);
}
if (cell.hasFlag(CellFlags::WindowZPos)) {
Ogre::Vector3 pos = origin + Ogre::Vector3(0, windowY, halfCell + frameOffset);
Ogre::Quaternion rot(Ogre::Degree(0), Ogre::Vector3::UNIT_Y);
addFrame(extMesh, pos, rot);
}
}
if (intMesh) {
frameOffset = 0.1f;
if (cell.hasFlag(CellFlags::IntWindowXNeg)) {
Ogre::Vector3 pos = origin + Ogre::Vector3(-halfCell + frameOffset, windowY, 0);
Ogre::Quaternion rot(Ogre::Degree(-90), Ogre::Vector3::UNIT_Y);
addFrame(intMesh, pos, rot);
}
if (cell.hasFlag(CellFlags::IntWindowXPos)) {
Ogre::Vector3 pos = origin + Ogre::Vector3(halfCell - frameOffset, windowY, 0);
Ogre::Quaternion rot(Ogre::Degree(90), Ogre::Vector3::UNIT_Y);
addFrame(intMesh, pos, rot);
}
if (cell.hasFlag(CellFlags::IntWindowZNeg)) {
Ogre::Vector3 pos = origin + Ogre::Vector3(0, windowY, -halfCell + frameOffset);
Ogre::Quaternion rot(Ogre::Degree(180), Ogre::Vector3::UNIT_Y);
addFrame(intMesh, pos, rot);
}
if (cell.hasFlag(CellFlags::IntWindowZPos)) {
Ogre::Vector3 pos = origin + Ogre::Vector3(0, windowY, halfCell - frameOffset);
Ogre::Quaternion rot(Ogre::Degree(0), Ogre::Vector3::UNIT_Y);
addFrame(intMesh, pos, rot);
}
}
}
}
void CellGridSystem::placeDoorFramesInStaticGeometry(
flecs::entity entity, const CellGridComponent &grid,
const Ogre::Vector3 &parentPos, const Ogre::Quaternion &parentRot,
const Ogre::Vector3 &parentScale, Ogre::StaticGeometry *staticGeom,
int &frameCount)
{
std::string meshPrefix =
"CellGrid_" + std::to_string(entity.id()) + "_";
Ogre::MeshPtr extMesh = Ogre::MeshManager::getSingleton().getByName(
meshPrefix + "door_external");
Ogre::MeshPtr intMesh = Ogre::MeshManager::getSingleton().getByName(
meshPrefix + "door_internal");
if (!extMesh && !intMesh)
return;
float halfCell = grid.cellSize / 2.0f;
float frameOffset = 0.1f;
float solidExtOffset = 0.0f;
float solidIntOffset = 0.1f;
float doorHeightBase = 3.0f * (grid.cellHeight / 4.0f);
float extDoorFrameY = solidExtOffset;
float intDoorFrameY = solidIntOffset;
auto addFrame = [&](Ogre::MeshPtr mesh, const Ogre::Vector3 &localPos,
const Ogre::Quaternion &localRot) {
if (!mesh)
return;
// Calculate world position relative to parent
Ogre::Vector3 worldPos = parentPos + (parentRot * (localPos * parentScale));
Ogre::Quaternion worldRot = parentRot * localRot;
// Create temporary entity to add to StaticGeometry
Ogre::Entity* tempEnt = m_sceneMgr->createEntity(mesh->getName());
staticGeom->addEntity(tempEnt, worldPos, worldRot, parentScale);
m_sceneMgr->destroyEntity(tempEnt); // StaticGeometry copies the data
frameCount++;
};
for (const auto &cell : grid.cells) {
Ogre::Vector3 origin = grid.cellToWorld(cell.x, cell.y, cell.z);
if (extMesh) {
if (cell.hasFlag(CellFlags::DoorXNeg)) {
Ogre::Vector3 pos = origin + Ogre::Vector3(-halfCell - frameOffset, extDoorFrameY, 0);
Ogre::Quaternion rot(Ogre::Degree(-90), Ogre::Vector3::UNIT_Y);
addFrame(extMesh, pos, rot);
}
if (cell.hasFlag(CellFlags::DoorXPos)) {
Ogre::Vector3 pos = origin + Ogre::Vector3(halfCell + frameOffset, extDoorFrameY, 0);
Ogre::Quaternion rot(Ogre::Degree(90), Ogre::Vector3::UNIT_Y);
addFrame(extMesh, pos, rot);
}
if (cell.hasFlag(CellFlags::DoorZNeg)) {
Ogre::Vector3 pos = origin + Ogre::Vector3(0, extDoorFrameY, -halfCell - frameOffset);
Ogre::Quaternion rot(Ogre::Degree(180), Ogre::Vector3::UNIT_Y);
addFrame(extMesh, pos, rot);
}
if (cell.hasFlag(CellFlags::DoorZPos)) {
Ogre::Vector3 pos = origin + Ogre::Vector3(0, extDoorFrameY, halfCell + frameOffset);
Ogre::Quaternion rot(Ogre::Degree(0), Ogre::Vector3::UNIT_Y);
addFrame(extMesh, pos, rot);
}
}
if (intMesh) {
if (cell.hasFlag(CellFlags::IntDoorXNeg)) {
Ogre::Vector3 pos = origin + Ogre::Vector3(-halfCell + frameOffset, intDoorFrameY, 0);
Ogre::Quaternion rot(Ogre::Degree(-90), Ogre::Vector3::UNIT_Y);
addFrame(intMesh, pos, rot);
}
if (cell.hasFlag(CellFlags::IntDoorXPos)) {
Ogre::Vector3 pos = origin + Ogre::Vector3(halfCell - frameOffset, intDoorFrameY, 0);
Ogre::Quaternion rot(Ogre::Degree(90), Ogre::Vector3::UNIT_Y);
addFrame(intMesh, pos, rot);
}
if (cell.hasFlag(CellFlags::IntDoorZNeg)) {
Ogre::Vector3 pos = origin + Ogre::Vector3(0, intDoorFrameY, -halfCell + frameOffset);
Ogre::Quaternion rot(Ogre::Degree(180), Ogre::Vector3::UNIT_Y);
addFrame(intMesh, pos, rot);
}
if (cell.hasFlag(CellFlags::IntDoorZPos)) {
Ogre::Vector3 pos = origin + Ogre::Vector3(0, intDoorFrameY, halfCell - frameOffset);
Ogre::Quaternion rot(Ogre::Degree(0), Ogre::Vector3::UNIT_Y);
addFrame(intMesh, pos, rot);
}
}
}
}

View File

@@ -8,6 +8,10 @@ namespace Procedural {
class TriangleBuffer;
}
namespace Ogre {
class StaticGeometry;
}
class CellGridSystem {
public:
CellGridSystem(flecs::world& world, Ogre::SceneManager* sceneMgr);
@@ -58,6 +62,10 @@ private:
// Build lot base geometry
void buildLotBase(flecs::entity entity, struct LotComponent& lot);
// Build furniture meshes
void buildFurniture(flecs::entity entity, const struct CellGridComponent& grid);
void destroyFurniture(flecs::entity entity);
// Build window and door frames (3D frame meshes)
void buildFrames(flecs::entity entity, const struct CellGridComponent& grid, const std::string& materialName);
void createWindowFrameMeshes(const struct CellGridComponent& grid, const std::string& materialName,
@@ -68,6 +76,14 @@ private:
void placeDoorFrames(flecs::entity entity, const struct CellGridComponent& grid, Ogre::SceneNode* parentNode, std::vector<Ogre::Entity*>& frameEntities);
void destroyFrames(flecs::entity entity);
// StaticGeometry versions (batched for better performance)
void placeWindowFramesInStaticGeometry(flecs::entity entity, const struct CellGridComponent& grid,
const Ogre::Vector3& parentPos, const Ogre::Quaternion& parentRot, const Ogre::Vector3& parentScale,
Ogre::StaticGeometry* staticGeom, int& frameCount);
void placeDoorFramesInStaticGeometry(flecs::entity entity, const struct CellGridComponent& grid,
const Ogre::Vector3& parentPos, const Ogre::Quaternion& parentRot, const Ogre::Vector3& parentScale,
Ogre::StaticGeometry* staticGeom, int& frameCount);
// Convert triangle buffer to mesh
Ogre::MeshPtr convertToMesh(const std::string& name, Procedural::TriangleBuffer& tb, const std::string& materialName);
@@ -106,6 +122,12 @@ private:
// Frame entities attached to the SceneNode (so they move with transform)
std::vector<Ogre::Entity*> frameEntities;
// StaticGeometry for frames (batches all frames into single draw call)
Ogre::StaticGeometry* frameStaticGeometry = nullptr;
// Furniture entities
std::vector<Ogre::Entity*> furnitureEntities;
};
std::unordered_map<uint64_t, MeshData> m_entityMeshes;

View File

@@ -0,0 +1,126 @@
#include "FurnitureLibrary.hpp"
#include <OgreResourceGroupManager.h>
#include <OgreDataStream.h>
#include <OgreLogManager.h>
#include <algorithm>
#include <random>
FurnitureLibrary &FurnitureLibrary::getInstance()
{
static FurnitureLibrary instance;
return instance;
}
void FurnitureLibrary::loadAll()
{
if (m_loaded)
return;
m_loaded = true;
m_definitions.clear();
std::vector<Ogre::String> names;
const std::vector<Ogre::String> &groups =
Ogre::ResourceGroupManager::getSingleton()
.getResourceGroups();
for (const auto &group : groups) {
std::vector<Ogre::String> groupNames =
*Ogre::ResourceGroupManager::getSingleton()
.findResourceNames(group, "furniture-*.json");
names.insert(names.end(), groupNames.begin(),
groupNames.end());
}
for (const auto &name : names) {
Ogre::String group =
Ogre::ResourceGroupManager::getSingleton()
.findGroupContainingResource(name);
Ogre::DataStreamPtr stream =
Ogre::ResourceGroupManager::getSingleton()
.openResource(name, group);
Ogre::String jsonStr = stream->getAsString();
try {
nlohmann::json jdata = nlohmann::json::parse(jsonStr);
FurnitureDefinition def;
def.name = jdata.value("name", name);
def.meshName = jdata.value("mesh", "");
def.jsonData = jdata;
if (jdata.contains("tags") && jdata["tags"].is_array()) {
for (auto &tag : jdata["tags"]) {
def.tags.push_back(tag.get<std::string>());
}
}
if (!def.meshName.empty()) {
m_definitions.push_back(std::move(def));
}
} catch (const std::exception &e) {
Ogre::LogManager::getSingleton().logMessage(
"FurnitureLibrary: Failed to parse " + name +
": " + e.what());
}
}
Ogre::LogManager::getSingleton().logMessage(
"FurnitureLibrary: Loaded " +
std::to_string(m_definitions.size()) +
" furniture definitions");
}
const std::vector<FurnitureDefinition> &FurnitureLibrary::getDefinitions()
const
{
return m_definitions;
}
const FurnitureDefinition *FurnitureLibrary::findByName(
const std::string &name) const
{
for (const auto &def : m_definitions) {
if (def.name == name)
return &def;
}
return nullptr;
}
std::vector<const FurnitureDefinition *> FurnitureLibrary::findByTags(
const std::vector<std::string> &requiredTags,
const std::vector<std::string> &forbiddenTags) const
{
std::vector<const FurnitureDefinition *> results;
for (const auto &def : m_definitions) {
bool match = true;
for (const auto &req : requiredTags) {
if (std::find(def.tags.begin(), def.tags.end(), req) ==
def.tags.end()) {
match = false;
break;
}
}
if (!match)
continue;
for (const auto &forb : forbiddenTags) {
if (std::find(def.tags.begin(), def.tags.end(), forb) !=
def.tags.end()) {
match = false;
break;
}
}
if (match)
results.push_back(&def);
}
return results;
}
const FurnitureDefinition *FurnitureLibrary::selectByTags(
const std::vector<std::string> &requiredTags,
const std::vector<std::string> &forbiddenTags) const
{
auto candidates = findByTags(requiredTags, forbiddenTags);
if (candidates.empty())
return nullptr;
static std::mt19937 rng((unsigned)std::chrono::steady_clock::now()
.time_since_epoch()
.count());
std::uniform_int_distribution<size_t> dist(0, candidates.size() - 1);
return candidates[dist(rng)];
}

View File

@@ -0,0 +1,39 @@
#pragma once
#include <Ogre.h>
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
struct FurnitureDefinition {
std::string name;
std::string meshName;
std::vector<std::string> tags;
nlohmann::json jsonData;
};
class FurnitureLibrary {
public:
static FurnitureLibrary &getInstance();
void loadAll();
bool isLoaded() const { return m_loaded; }
void reload() { m_loaded = false; loadAll(); }
const std::vector<FurnitureDefinition> &getDefinitions() const;
const FurnitureDefinition *findByName(const std::string &name) const;
std::vector<const FurnitureDefinition *> findByTags(
const std::vector<std::string> &requiredTags,
const std::vector<std::string> &forbiddenTags = {}) const;
const FurnitureDefinition *selectByTags(
const std::vector<std::string> &requiredTags,
const std::vector<std::string> &forbiddenTags = {}) const;
private:
FurnitureLibrary() = default;
std::vector<FurnitureDefinition> m_definitions;
bool m_loaded = false;
};

View File

@@ -1,9 +1,11 @@
#include "RoomLayoutSystem.hpp"
#include "FurnitureLibrary.hpp"
#include "../components/CellGrid.hpp"
#include <OgreLogManager.h>
#include <climits>
#include <set>
#include <unordered_map>
#include <random>
RoomLayoutSystem::RoomLayoutSystem(flecs::world& world, Ogre::SceneManager* sceneMgr)
: m_world(world)
@@ -131,6 +133,15 @@ void RoomLayoutSystem::reprocessAll()
processExteriorGeneration(*grid);
}
});
// STEP 7: Generate furniture for all rooms
FurnitureLibrary::getInstance().loadAll();
m_roomQuery.each([&](flecs::entity roomEntity, RoomComponent& room) {
CellGridComponent* grid = findParentGrid(roomEntity);
if (grid) {
generateRoomFurniture(room, *grid);
}
});
Ogre::LogManager::getSingleton().logMessage("RoomLayoutSystem: Full reprocess complete.");
}
@@ -842,3 +853,237 @@ void RoomLayoutSystem::clearWallFlags(CellGridComponent& grid, int x, int y, int
cell->flags &= ~flagsToClear;
}
void RoomLayoutSystem::generateRoomFurniture(RoomComponent& room,
CellGridComponent& grid)
{
if (room.tags.empty())
return;
// Clear existing furniture in room bounds
for (auto it = grid.furnitureCells.begin();
it != grid.furnitureCells.end();) {
if (it->x >= room.minX && it->x < room.maxX &&
it->y >= room.minY && it->y < room.maxY &&
it->z >= room.minZ && it->z < room.maxZ) {
it = grid.furnitureCells.erase(it);
} else {
++it;
}
}
int midX = (room.minX + room.maxX - 1) / 2;
int midZ = (room.minZ + room.maxZ - 1) / 2;
// Side: 0=Z- (minZ), 1=Z+ (maxZ), 2=X- (minX), 3=X+ (maxX)
const int ROTATION_BY_SIDE[4] = { 2, 0, 3, 1 };
const uint64_t WALL_FLAGS_BY_SIDE[4] = {
CellFlags::IntWallZNeg | CellFlags::WallZNeg,
CellFlags::IntWallZPos | CellFlags::WallZPos,
CellFlags::IntWallXNeg | CellFlags::WallXNeg,
CellFlags::IntWallXPos | CellFlags::WallXPos
};
const uint64_t WINDOW_FLAGS_BY_SIDE[4] = {
CellFlags::IntWindowZNeg | CellFlags::WindowZNeg,
CellFlags::IntWindowZPos | CellFlags::WindowZPos,
CellFlags::IntWindowXNeg | CellFlags::WindowXNeg,
CellFlags::IntWindowXPos | CellFlags::WindowXPos
};
const uint64_t DOOR_FLAGS_BY_SIDE[4] = {
CellFlags::IntDoorZNeg | CellFlags::DoorZNeg,
CellFlags::IntDoorZPos | CellFlags::DoorZPos,
CellFlags::IntDoorXNeg | CellFlags::DoorXNeg,
CellFlags::IntDoorXPos | CellFlags::DoorXPos
};
auto cellHasAnyDoor = [&](int x, int y, int z) -> bool {
const Cell *cell = grid.findCell(x, y, z);
if (!cell)
return false;
return (cell->flags & CellFlags::AllDoors) != 0;
};
auto cellHasAnyWindow = [&](int x, int y, int z) -> bool {
const Cell *cell = grid.findCell(x, y, z);
if (!cell)
return false;
return (cell->flags & CellFlags::AllWindows) != 0;
};
auto cellHasAnyWall = [&](int x, int y, int z) -> bool {
const Cell *cell = grid.findCell(x, y, z);
if (!cell)
return false;
return (cell->flags & CellFlags::AllWalls) != 0;
};
auto canPlaceFurniture = [&](const FurnitureDefinition *def,
int x, int y, int z) -> bool {
if (!def)
return false;
// If cell has any door, furniture must have "door" tag
if (cellHasAnyDoor(x, y, z)) {
if (std::find(def->tags.begin(), def->tags.end(),
"door") == def->tags.end())
return false;
}
// If cell has any window, furniture must NOT have "nowindow" tag
if (cellHasAnyWindow(x, y, z)) {
if (std::find(def->tags.begin(), def->tags.end(),
"nowindow") != def->tags.end())
return false;
}
// If cell has no walls, furniture must have "nowall" tag
if (!cellHasAnyWall(x, y, z)) {
if (std::find(def->tags.begin(), def->tags.end(),
"nowall") == def->tags.end())
return false;
}
return true;
};
auto placeOne = [&](int cx, int cy, int cz, int side,
const std::vector<std::string> &tags,
const std::vector<std::string> &notags,
bool onWindow) -> bool {
if (grid.findFurnitureCell(cx, cy, cz))
return false;
const Cell *cell = grid.findCell(cx, cy, cz);
if (!cell)
return false;
uint64_t sideFlags = onWindow ? WINDOW_FLAGS_BY_SIDE[side] :
WALL_FLAGS_BY_SIDE[side];
if ((cell->flags & sideFlags) == 0)
return false;
auto *def = FurnitureLibrary::getInstance().selectByTags(
tags, notags);
if (!def)
return false;
if (!canPlaceFurniture(def, cx, cy, cz))
return false;
auto &fcell = grid.getOrCreateFurnitureCell(cx, cy, cz);
fcell.furnitureType = def->name;
fcell.rotation = ROTATION_BY_SIDE[side];
fcell.tags = tags;
return true;
};
std::vector<std::string> baseTags = room.tags;
std::vector<std::string> etags = baseTags;
std::vector<std::string> itags = baseTags;
std::vector<std::string> otags = baseTags;
std::vector<std::string> ftags = baseTags;
etags.push_back("essential");
itags.push_back("important");
otags.push_back("optional");
ftags.push_back("filler");
struct TagData {
std::vector<std::string> tags;
std::vector<std::string> notagsWalls;
std::vector<std::string> notagsWindows;
bool essential;
bool filler;
};
TagData taglist[] = {
{ etags, { "nowall", "nowindow" }, { "nowindow" }, true,
false },
{ itags, { "nowall", "nowindow" }, { "nowindow" }, false,
false },
{ otags, { "nowall", "nowindow" }, { "nowindow" }, false,
false },
{ ftags, { "nowall", "nowindow" }, { "nowindow" }, false,
true },
};
// Midpoints for each side
std::tuple<int, int, int> mpoints[4] = {
{ midX, room.minY, room.minZ }, // side 0 (Z-)
{ midX, room.minY, room.maxZ - 1 }, // side 1 (Z+)
{ room.minX, room.minY, midZ }, // side 2 (X-)
{ room.maxX - 1, room.minY, midZ }, // side 3 (X+)
};
for (const auto &mtags : taglist) {
bool placed = false;
// Try wall midpoints
for (int side = 0; side < 4 && !placed; ++side) {
int cx = std::get<0>(mpoints[side]);
int cy = std::get<1>(mpoints[side]);
int cz = std::get<2>(mpoints[side]);
placed = placeOne(cx, cy, cz, side, mtags.tags,
mtags.notagsWalls, false);
if (placed && !mtags.filler)
break;
if (mtags.filler)
placed = false;
}
// Try wall ranges (all edge cells on each side)
if (!placed) {
for (int side = 0; side < 4 && !placed; ++side) {
auto edgeCells = getRoomEdgeCells(room, side);
for (const auto &cell : edgeCells) {
int cx = cell.first;
int cz = cell.second;
int cy = room.minY;
placed = placeOne(cx, cy, cz, side,
mtags.tags,
mtags.notagsWalls,
false);
if (placed && !mtags.filler)
break;
}
if (placed && !mtags.filler)
break;
}
}
if (mtags.filler)
placed = false;
// Try window midpoints
if (!placed) {
for (int side = 0; side < 4 && !placed; ++side) {
int cx = std::get<0>(mpoints[side]);
int cy = std::get<1>(mpoints[side]);
int cz = std::get<2>(mpoints[side]);
placed = placeOne(cx, cy, cz, side, mtags.tags,
mtags.notagsWindows, true);
if (placed && !mtags.filler)
break;
if (mtags.filler)
placed = false;
}
}
// Try window ranges
if (!placed) {
for (int side = 0; side < 4 && !placed; ++side) {
auto edgeCells = getRoomEdgeCells(room, side);
for (const auto &cell : edgeCells) {
int cx = cell.first;
int cz = cell.second;
int cy = room.minY;
placed = placeOne(cx, cy, cz, side,
mtags.tags,
mtags.notagsWindows,
true);
if (placed && !mtags.filler)
break;
}
if (placed && !mtags.filler)
break;
}
}
if (!placed && !mtags.filler) {
break; // do not place anything if essentials were not placed
}
}
grid.markDirty();
}

View File

@@ -62,6 +62,9 @@ private:
// This always runs at the end of the pipeline for all rooms
void processExteriorGeneration(class CellGridComponent& grid);
// Generate furniture for a room based on its tags
void generateRoomFurniture(class RoomComponent& room, class CellGridComponent& grid);
// Helper: Get the parent CellGridComponent for an entity
class CellGridComponent* findParentGrid(flecs::entity entity);

View File

@@ -2,10 +2,13 @@
#include "../components/CellGrid.hpp"
#include "../components/ProceduralMaterial.hpp"
#include "../components/ProceduralTexture.hpp"
#include "../systems/FurnitureLibrary.hpp"
#include <OgreStringConverter.h>
bool CellGridEditor::renderComponent(flecs::entity entity, CellGridComponent& grid)
{
m_currentEntity = entity;
bool modified = false;
if (ImGui::CollapsingHeader("Cell Grid", ImGuiTreeNodeFlags_DefaultOpen)) {
@@ -145,17 +148,38 @@ void CellGridEditor::renderFurnitureEditor(CellGridComponent& grid)
{
static int fx = 0, fy = 0, fz = 0;
static char tagsBuffer[256] = {0};
static char furnitureType[128] = {0};
static int selectedFurnitureIndex = -1;
static int rotation = 0;
FurnitureLibrary::getInstance().loadAll();
const auto& defs = FurnitureLibrary::getInstance().getDefinitions();
ImGui::InputInt3("Furniture Pos", &fx);
ImGui::InputText("Tags (comma separated)", tagsBuffer, sizeof(tagsBuffer));
ImGui::InputText("Furniture Type", furnitureType, sizeof(furnitureType));
// Furniture type dropdown
std::string preview = selectedFurnitureIndex >= 0 && selectedFurnitureIndex < (int)defs.size()
? defs[selectedFurnitureIndex].name + " (" + defs[selectedFurnitureIndex].meshName + ")"
: "Select furniture type...";
if (ImGui::BeginCombo("Furniture Type", preview.c_str())) {
for (int i = 0; i < (int)defs.size(); ++i) {
std::string label = defs[i].name + " (" + defs[i].meshName + ")";
bool isSelected = (selectedFurnitureIndex == i);
if (ImGui::Selectable(label.c_str(), isSelected)) {
selectedFurnitureIndex = i;
}
if (isSelected)
ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
ImGui::SliderInt("Rotation", &rotation, 0, 3);
if (ImGui::Button("Add/Update Furniture")) {
if (ImGui::Button("Add/Update Furniture") && selectedFurnitureIndex >= 0) {
auto& fcell = grid.getOrCreateFurnitureCell(fx, fy, fz);
fcell.furnitureType = furnitureType;
fcell.furnitureType = defs[selectedFurnitureIndex].name;
fcell.rotation = rotation;
fcell.tags.clear();
// Parse tags
@@ -164,7 +188,6 @@ void CellGridEditor::renderFurnitureEditor(CellGridComponent& grid)
size_t end = tagsStr.find(',');
while (end != std::string::npos) {
std::string tag = tagsStr.substr(start, end - start);
// Trim whitespace
tag.erase(0, tag.find_first_not_of(" \t"));
tag.erase(tag.find_last_not_of(" \t") + 1);
if (!tag.empty()) {
@@ -173,7 +196,6 @@ void CellGridEditor::renderFurnitureEditor(CellGridComponent& grid)
start = end + 1;
end = tagsStr.find(',', start);
}
// Last tag
std::string lastTag = tagsStr.substr(start);
lastTag.erase(0, lastTag.find_first_not_of(" \t"));
lastTag.erase(lastTag.find_last_not_of(" \t") + 1);
@@ -183,6 +205,12 @@ void CellGridEditor::renderFurnitureEditor(CellGridComponent& grid)
grid.markDirty();
}
ImGui::SameLine();
if (ImGui::Button("Delete Furniture")) {
grid.removeFurnitureCell(fx, fy, fz);
grid.markDirty();
}
ImGui::Separator();
// List furniture cells
@@ -196,8 +224,14 @@ void CellGridEditor::renderFurnitureEditor(CellGridComponent& grid)
fy = fcell.y;
fz = fcell.z;
rotation = fcell.rotation;
strncpy(furnitureType, fcell.furnitureType.c_str(), sizeof(furnitureType) - 1);
// Build tags string
// Find index in defs
selectedFurnitureIndex = -1;
for (int i = 0; i < (int)defs.size(); ++i) {
if (defs[i].name == fcell.furnitureType) {
selectedFurnitureIndex = i;
break;
}
}
std::string tags;
for (size_t i = 0; i < fcell.tags.size(); ++i) {
if (i > 0) tags += ", ";

View File

@@ -20,4 +20,5 @@ private:
int newCellX = 0, newCellY = 0, newCellZ = 0;
char scriptBuffer[16384] = {0};
bool showGrid = false;
flecs::entity m_currentEntity = flecs::entity::null();
};

View File

@@ -1,8 +1,10 @@
#include "RoomEditor.hpp"
#include "../components/CellGrid.hpp"
#include "../components/EntityName.hpp"
#include "../systems/FurnitureLibrary.hpp"
#include <imgui.h>
#include <algorithm>
#include <set>
bool RoomEditor::renderComponent(flecs::entity entity, RoomComponent& room)
{
@@ -80,6 +82,53 @@ bool RoomEditor::renderComponent(flecs::entity entity, RoomComponent& room)
if (!lastTag.empty()) room.tags.push_back(lastTag);
modified = true;
}
// Validate that furniture exists for these tags
if (!room.tags.empty()) {
FurnitureLibrary::getInstance().loadAll();
auto matches = FurnitureLibrary::getInstance().findByTags(room.tags, {});
if (matches.empty()) {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"Warning: No furniture found for these tags");
} else {
ImGui::TextDisabled("%zu furniture item(s) match", matches.size());
}
}
if (ImGui::CollapsingHeader("Available Furniture", ImGuiTreeNodeFlags_None)) {
ImGui::Indent();
FurnitureLibrary::getInstance().loadAll();
const auto& defs = FurnitureLibrary::getInstance().getDefinitions();
if (defs.empty()) {
ImGui::TextDisabled("No furniture definitions loaded");
} else {
// Build a sorted set of all tags
std::set<std::string> allTags;
for (const auto& def : defs) {
for (const auto& tag : def.tags) {
allTags.insert(tag);
}
}
ImGui::TextDisabled("All tags:");
std::string tagList;
for (const auto& tag : allTags) {
if (!tagList.empty()) tagList += ", ";
tagList += tag;
}
ImGui::TextWrapped("%s", tagList.c_str());
ImGui::Separator();
for (const auto& def : defs) {
std::string tagStr;
for (size_t i = 0; i < def.tags.size(); ++i) {
if (i > 0) tagStr += ", ";
tagStr += def.tags[i];
}
ImGui::BulletText("%s [%s]", def.name.c_str(), tagStr.c_str());
}
}
ImGui::Unindent();
}
ImGui::Unindent();
}