#include <iostream>
#include <Ogre.h>
#include <OgreMeshSerializer.h>
#include <OgreMesh.h>
#include <OgreResourceGroupManager.h>
#include <OgreTerrainGroup.h>
#include <OgreImGuiOverlay.h>
#include <OgreRTShaderSystem.h>
#include <Procedural.h>
#include <nlohmann/json.hpp>
#include <FastNoiseLite.h>
#include "Components.h"
#include "GameData.h"
#include "EditorGizmoModule.h"
#include "TerrainModule.h"
#include "StaticGeometryModule.h"
#include "physics.h"
#include "PhysicsModule.h"
#include "LuaData.h"
#include "PlayerActionModule.h"
#include "CharacterModule.h"
#include "CharacterManagerModule.h"
#include "CharacterAIModule.h"
#include "items.h"
#include "town.h"
#include <tracy/Tracy.hpp>

/*
 * TODO: Create doors and handle them via script.
 * TODO: Handle doors direction.
 * TODO: Hide hero mesh if camera is too close.
 * TODO: create furniture tiles from the same Lua script. Create tile tags, Create tile templates.
*/

void serializeMesh(Ogre::MeshPtr mesh, const Ogre::String &meshName)
{
	ZoneScoped;
	Ogre::MeshSerializer meshSerializer;

	meshSerializer.exportMesh(mesh, meshName,
				  Ogre::Serializer::ENDIAN_NATIVE);
}
namespace ECS
{
namespace Items
{
int64_t makeCellKey(int kx, int ky, int kz)
{
	ZoneScoped;
	int64_t key =
		(int64_t)kx + 1024 * (int64_t)ky + 1024 * 1024 * (int64_t)kz;
	return key;
}
int64_t makeCellKey(const nlohmann::json &cell)
{
	ZoneScoped;
	int64_t kx = cell["x"].get<int>();
	int64_t ky = cell["y"].get<int>();
	int64_t kz = cell["z"].get<int>();
	int64_t key = makeCellKey(kx, ky, kz);
	return key;
};
void displayGrid(flecs::entity e, nlohmann::json &lot, int index,
		 Ogre::SceneNode *sceneNode, const Ogre::Vector3 &localPosition,
		 const Ogre::Quaternion &localRotation)
{
	ZoneScoped;
	if (lot.find("edited") == lot.end()) {
		lot["edited"] = true;
		int width = lot["width"].get<int>();
		int height = lot["height"].get<int>();
		Ogre::Vector3 worldPosition =
			sceneNode->_getDerivedPosition() + localPosition;
		Ogre::Quaternion worldRotation =
			sceneNode->_getDerivedOrientation() * localRotation;
		Ogre::Vector3 origin = worldPosition +
				       worldRotation * Ogre::Vector3::UNIT_X *
					       4.0f * (-width / 2) +
				       worldRotation * Ogre::Vector3::UNIT_Z *
					       4.0f * (-height / 2);
		Ogre::ManualObject *mobj =
			ECS::get<EngineData>().mScnMgr->createManualObject(
				"lot" +
				Ogre::StringConverter::toString(e.id()) + "_" +
				Ogre::StringConverter::toString(index));
		mobj->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
		mobj->begin(ECS::get<EditorDebugMaterial>().material,
			    Ogre::RenderOperation::OT_LINE_LIST);
		mobj->setBufferUsage(Ogre::HardwareBufferUsage::HBU_GPU_ONLY);
		int i, j;
		for (j = 0; j < width; j++) {
			Ogre::Vector3 d1 = worldRotation *
					   Ogre::Vector3::UNIT_X * 4.0f * j;
			Ogre::Vector3 d2 = worldRotation *
					   Ogre::Vector3::UNIT_Z * 4.0f *
					   height;
			mobj->position(origin + d1);
			mobj->colour(Ogre::ColourValue(0, 1, 1, 1));
			mobj->position(origin + d1 + d2);
			mobj->colour(Ogre::ColourValue(0, 1, 1, 1));
		}
		mobj->end();
		sceneNode->attachObject(mobj);
	}
}
void createTownItem()
{
	ZoneScoped;
	Ogre::Vector3 itemPosition =
		ECS::get<EditorGizmo>().sceneNode->_getDerivedPosition();
	Ogre::Quaternion itemOrientation =
		ECS::get<EditorGizmo>().sceneNode->_getDerivedOrientation();
	flecs::entity e = StaticGeometryModule::createItem(
		itemPosition, itemOrientation, "town");
	Ogre::String prop = StaticGeometryModule::getItemProperties(e);
	nlohmann::json j = nlohmann::json::parse(prop);
	j["districts"] = nlohmann::json::array();
	StaticGeometryModule::setItemProperties(e, j.dump());
	StaticGeometryModule::saveItems();
}
void createTownMenu()
{
	ZoneScoped;
	if (ImGui::MenuItem("Create"))
		createTownItem();
}
bool editCell(const Ogre::String &cellLabel, nlohmann::json &cell)
{
	ZoneScoped;
	bool changed = false;
	auto checkboxBit = [cellLabel, &changed](Ogre::String label,
						 nlohmann::json &cell,
						 uint64_t bit) {
		uint64_t flags = cell["flags"].get<uint64_t>();
		bool status = (flags & bit) == bit;
		if (ImGui::Checkbox((label + "##" + cellLabel).c_str(),
				    &status)) {
			if (status)
				flags |= bit;
			else
				flags &= ~bit;
			cell["flags"] = flags;
			changed = true;
		}
	};
	checkboxBit("Floor", cell, (1ULL << 0));
	checkboxBit("Ceiling", cell, (1ULL << 1));
	checkboxBit("Ext Wall X-", cell, (1ULL << 2));
	checkboxBit("Ext Wall X+", cell, (1ULL << 3));
	checkboxBit("Ext Wall Z+", cell, (1ULL << 4));
	checkboxBit("Ext Wall Z-", cell, (1ULL << 5));
	checkboxBit("Ext Door X-", cell, (1ULL << 6));
	checkboxBit("Ext Door X+", cell, (1ULL << 7));
	checkboxBit("Ext Door Z+", cell, (1ULL << 8));
	checkboxBit("Ext Door Z-", cell, (1ULL << 9));
	checkboxBit("Ext Window X-", cell, (1ULL << 10));
	checkboxBit("Ext Window X+", cell, (1ULL << 11));
	checkboxBit("Ext Window Z+", cell, (1ULL << 12));
	checkboxBit("Ext Window Z-", cell, (1ULL << 13));
	checkboxBit("Int Wall X-", cell, (1ULL << 14));
	checkboxBit("Int Wall X+", cell, (1ULL << 15));
	checkboxBit("Int Wall Z+", cell, (1ULL << 16));
	checkboxBit("Int Wall Z-", cell, (1ULL << 17));
	checkboxBit("Int Door X-", cell, (1ULL << 18));
	checkboxBit("Int Door X+", cell, (1ULL << 19));
	checkboxBit("Int Door Z+", cell, (1ULL << 20));
	checkboxBit("Int Door Z-", cell, (1ULL << 21));
	checkboxBit("Int Window X-", cell, (1ULL << 22));
	checkboxBit("Int Window X+", cell, (1ULL << 23));
	checkboxBit("Int Window Z+", cell, (1ULL << 24));
	checkboxBit("Int Window Z-", cell, (1ULL << 25));
	return changed;
}

struct CellsScript {
	struct room {
		int minX;
		int minZ;
		int sizeX;
		int sizeZ;
	};
	std::vector<struct room> rooms;
	lua_State *L;
	int currentX;
	int currentY;
	int currentZ;
	Ogre::String cellScript;
	nlohmann::json &lot;
	nlohmann::json &cells;
	nlohmann::json &fucells;
	FastNoiseLite noise;
	CellsScript(const Ogre::String &cellScript, nlohmann::json &lot)
		: L(luaL_newstate())
		, currentX(0)
		, currentY(0)
		, currentZ(0)
		, cellScript(cellScript)
		, lot(lot)
		, cells(lot["cells"])
		, fucells(lot["furniture_cells"])
	{
		ZoneScoped;
		noise.SetNoiseType(FastNoiseLite::NoiseType_OpenSimplex2);
		noise.SetSeed(310);
		noise.SetFrequency(0.01f);
		noise.SetFractalType(FastNoiseLite::FractalType_FBm);
		noise.SetFractalOctaves(4);
		noise.SetFractalLacunarity(2.0f);
		noise.SetFractalGain(0.5f);
		luaL_requiref(L, "table", luaopen_table, 1);
		lua_pop(L, 1);
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				int seed = lua_tointeger(L, 1);
				_this->noise.SetSeed(seed);
				return 0;
			},
			1);
		lua_setglobal(L, "seed");
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				int pX = lua_tointeger(L, 1);
				int pY = lua_tointeger(L, 2);
				int pZ = lua_tointeger(L, 3);
				_this->currentX = pX;
				_this->currentY = pY;
				_this->currentZ = pZ;
				return 0;
			},
			1);
		lua_setglobal(L, "pos");
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				int pX = lua_tointeger(L, 1);
				int pY = lua_tointeger(L, 2);
				int pZ = lua_tointeger(L, 3);
				_this->currentX += pX;
				_this->currentY += pY;
				_this->currentZ += pZ;
				return 0;
			},
			1);
		lua_setglobal(L, "go");
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				_this->cell(0);
				return 0;
			},
			1);
		lua_setglobal(L, "cell");
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				Ogre::String cmd = lua_tostring(L, 1);
				Ogre::String bit = lua_tostring(L, 2);
				_this->bitCmd(cmd, bit);
				return 0;
			},
			1);
		lua_setglobal(L, "bit");
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				int minX = lua_tointeger(L, 1);
				int minZ = lua_tointeger(L, 2);
				int sizeX = lua_tointeger(L, 3);
				int sizeZ = lua_tointeger(L, 4);
				int r = _this->room(minX, minZ, sizeX, sizeZ);
				std::cout << "room: " << r << std::endl;
				lua_pushinteger(L, r);
				return 1;
			},
			1);
		lua_setglobal(L, "room");
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				int minX = lua_tointeger(L, 1);
				int minZ = lua_tointeger(L, 2);
				int sizeX = lua_tointeger(L, 3);
				int sizeZ = lua_tointeger(L, 4);
				float height = lua_tonumber(L, 5);
				int type = lua_tointeger(L, 6);
				int r = _this->roof(minX, minZ, sizeX, sizeZ,
						    height, type);
				std::cout << "roof: " << r << std::endl;
				lua_pushinteger(L, r);
				return 1;
			},
			1);
		lua_setglobal(L, "roof");
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				int room = lua_tointeger(L, 1);
				Ogre::String tags = lua_tostring(L, 2);
				_this->place_furniture(room, tags);
				return 0;
			},
			1);
		lua_setglobal(L, "place_furniture");
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				int minX = lua_tointeger(L, 1);
				int minZ = lua_tointeger(L, 2);
				int sizeX = lua_tointeger(L, 3);
				int sizeZ = lua_tointeger(L, 4);
				_this->clear_area(minX, minZ, sizeX, sizeZ);
				return 0;
			},
			1);
		lua_setglobal(L, "clear_area");
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				int minX = lua_tointeger(L, 1);
				int minZ = lua_tointeger(L, 2);
				int sizeX = lua_tointeger(L, 3);
				int sizeZ = lua_tointeger(L, 4);
				_this->clear_furniture_area(minX, minZ, sizeX,
							    sizeZ);
				return 0;
			},
			1);
		lua_setglobal(L, "clear_furniture_area");
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				_this->clear_roofs();
				return 0;
			},
			1);
		lua_setglobal(L, "clear_roofs");
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				int r1 = lua_tointeger(L, 1);
				int r2 = lua_tointeger(L, 2);
				_this->connectRooms(r1, r2);
				std::cout << "connect rooms: " << r1 << " "
					  << r2 << std::endl;
				return 0;
			},
			1);
		lua_setglobal(L, "connect_rooms");
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				int room = lua_tointeger(L, 1);
				_this->createExit0(room);
				return 0;
			},
			1);
		lua_setglobal(L, "create_exit0");
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				int room = lua_tointeger(L, 1);
				_this->createExit1(room);
				return 0;
			},
			1);
		lua_setglobal(L, "create_exit1");
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				int room = lua_tointeger(L, 1);
				_this->createExit2(room);
				return 0;
			},
			1);
		lua_setglobal(L, "create_exit2");
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				int room = lua_tointeger(L, 1);
				_this->createExit3(room);
				return 0;
			},
			1);
		lua_setglobal(L, "create_exit3");
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				int room = lua_tointeger(L, 1);
				_this->createWindows(room);
				return 0;
			},
			1);
		lua_setglobal(L, "create_windows");
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				_this->createExterior();
				return 0;
			},
			1);
		lua_setglobal(L, "create_exterior");
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				float angle = lua_tonumber(L, 1);
				_this->lot["angle"] = angle;
				return 0;
			},
			1);
		lua_setglobal(L, "set_lot_angle_degrees");
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				float width = lua_tonumber(L, 1);
				_this->lot["width"] = width;
				return 0;
			},
			1);
		lua_setglobal(L, "set_lot_width");
		lua_pushlightuserdata(L, this);
		lua_pushcclosure(
			L,
			[](lua_State *L) -> int {
				CellsScript *_this = static_cast<CellsScript *>(
					lua_touserdata(L, lua_upvalueindex(1)));
				float depth = lua_tonumber(L, 1);
				_this->lot["depth"] = depth;
				return 0;
			},
			1);
		lua_setglobal(L, "set_lot_depth");
	}
	static std::string trim(const std::string &str)
	{
		ZoneScoped;
		// Find the first character that is not a whitespace character
		const std::string ws =
			" \t\n\r\f\v"; // Common whitespace characters
		size_t first = str.find_first_not_of(ws);

		// If all characters are whitespace (or string is empty), return an empty string
		if (std::string::npos == first) {
			return "";
		}

		// Find the last character that is not a whitespace character
		size_t last = str.find_last_not_of(ws);

		// Extract the substring between the first and last non-whitespace characters
		return str.substr(first, (last - first + 1));
	}
	static std::vector<std::string> split_and_trim(const std::string &s,
						       char delimiter)
	{
		ZoneScoped;
		std::vector<std::string> tokens;
		std::string token;
		std::stringstream ss(s); // Use stringstream to parse the string

		// Use getline to extract each token based on the delimiter
		while (std::getline(ss, token, delimiter)) {
			// Trim whitespace from the extracted token
			std::string trimmed_token = trim(token);
			// Add the trimmed token to the vector (optional: skip empty strings)
			if (!trimmed_token.empty()) {
				tokens.push_back(trimmed_token);
			}
		}
		return tokens;
	}
	void buildCellIndex()
	{
		ZoneScoped;
		for (auto &cell : cells)
			cell["id"] = makeCellKey(cell);
	}
	void buildFCellIndex()
	{
		ZoneScoped;
		for (auto &fucell : fucells)
			fucell["id"] = makeCellKey(fucell);
	}
	int findCell(int X, int Y, int Z)
	{
		ZoneScoped;
		int64_t key = makeCellKey(X, Y, Z);
		int result = -1;
		int i;
		for (i = 0; i < cells.size(); i++) {
			int64_t ckey = makeCellKey(cells[i]);
			if (key == ckey) {
				result = i;
				break;
			}
		}
		return result;
	}
	int findFCell(int X, int Y, int Z)
	{
		ZoneScoped;
		int64_t key = makeCellKey(X, Y, Z);
		int result = -1;
		int i;
		for (i = 0; i < fucells.size(); i++) {
			int64_t ckey = makeCellKey(fucells[i]);
			if (key == ckey) {
				result = i;
				break;
			}
		}
		return result;
	}
	void bitCmd(const Ogre::String &cmd, const Ogre::String &bit)
	{
		ZoneScoped;
		uint64_t flags = 0;
		std::vector<Ogre::String> bitconv = {
			"floor",     "ceiling",	 "wallx-",    "wallx+",
			"wallz+",    "wallz-",	 "doorx-",    "doorx+",
			"doorz+",    "doorz-",	 "windowx-",  "windowx+",
			"windowz+",  "windowz-", "iwallx-",   "iwallx+",
			"iwallz+",   "iwallz-",	 "idoorx-",   "idoorx+",
			"idoorz+",   "idoorz-",	 "iwindowx-", "iwindowx+",
			"iwindowz+", "iwindowz-"
		};
		std::vector<Ogre::String> bits = split_and_trim(bit, ',');
		bool cmdSet = cmd == "set";
		bool cmdClear = cmd == "clear";
		int cellIndex = findCell(currentX, currentY, currentZ);
		if (cellIndex >= 0)
			flags = cells[cellIndex]["flags"].get<uint64_t>();
		for (auto &sbit : bits) {
			auto loc =
				std::find(bitconv.begin(), bitconv.end(), sbit);
			if (loc != bitconv.end()) {
				int index = std::distance(bitconv.begin(), loc);
				if (cmdSet)
					flags |= (1 << (uint64_t)index);
				if (cmdClear)
					flags &= ~(1 << (uint64_t)index);
			}
		}
		cell(flags);
	}
	bool isBit(const Ogre::String &bit)
	{
		ZoneScoped;
		uint64_t flags = 0;
		std::vector<Ogre::String> bitconv = {
			"floor",     "ceiling",	 "wallx-",    "wallx+",
			"wallz+",    "wallz-",	 "doorx-",    "doorx+",
			"doorz+",    "doorz-",	 "windowx-",  "windowx+",
			"windowz+",  "windowz-", "iwallx-",   "iwallx+",
			"iwallz+",   "iwallz-",	 "idoorx-",   "idoorx+",
			"idoorz+",   "idoorz-",	 "iwindowx-", "iwindowx+",
			"iwindowz+", "iwindowz-"
		};
		std::vector<Ogre::String> bits = split_and_trim(bit, ',');
		int cellIndex = findCell(currentX, currentY, currentZ);
		if (cellIndex >= 0)
			flags = cells[cellIndex]["flags"].get<uint64_t>();
		uint64_t bitset = 0;
		for (auto &sbit : bits) {
			auto loc =
				std::find(bitconv.begin(), bitconv.end(), sbit);
			if (loc != bitconv.end()) {
				int index = std::distance(bitconv.begin(), loc);
				bitset |= (1ULL << (uint64_t)index);
			}
		}
		return (flags & bitset) == bitset;
	}
	static bool isBit(const nlohmann::json &cell, const Ogre::String &bit)
	{
		ZoneScoped;
		uint64_t flags = 0;
		std::vector<Ogre::String> bitconv = {
			"floor",     "ceiling",	 "wallx-",    "wallx+",
			"wallz+",    "wallz-",	 "doorx-",    "doorx+",
			"doorz+",    "doorz-",	 "windowx-",  "windowx+",
			"windowz+",  "windowz-", "iwallx-",   "iwallx+",
			"iwallz+",   "iwallz-",	 "idoorx-",   "idoorx+",
			"idoorz+",   "idoorz-",	 "iwindowx-", "iwindowx+",
			"iwindowz+", "iwindowz-"
		};
		std::vector<Ogre::String> bits = split_and_trim(bit, ',');
		flags = cell["flags"].get<uint64_t>();
		uint64_t bitset = 0;
		for (auto &sbit : bits) {
			auto loc =
				std::find(bitconv.begin(), bitconv.end(), sbit);
			if (loc != bitconv.end()) {
				int index = std::distance(bitconv.begin(), loc);
				bitset |= (1ULL << (uint64_t)index);
			}
		}
		return (flags & bitset) == bitset;
	}
	void cell(uint64_t flags)
	{
		ZoneScoped;
		if (findCell(currentX, currentY, currentZ) == -1) {
			nlohmann::json cell;
			buildCellIndex();
			cell["x"] = currentX;
			cell["y"] = currentY;
			cell["z"] = currentZ;
			cell["flags"] = flags;
			cell["id"] = makeCellKey(cell);
			cells.push_back(cell);
		} else {
			int index = findCell(currentX, currentY, currentZ);
			if (index >= 0)
				cells[index]["flags"] = flags;
		}
	}
	void clear()
	{
		ZoneScoped;
		int index = findCell(currentX, currentY, currentZ);
		if (index >= 0)
			cells.erase(index);
	}
	void clear_area(int minX, int minZ, int sizeX, int sizeZ)
	{
		ZoneScoped;
		int i, j;
		for (i = 0; i < sizeZ; i++)
			for (j = 0; j < sizeX; j++) {
				currentX = minX + j;
				currentZ = minZ + i;
				clear();
			}
	}
	void clear_furniture()
	{
		ZoneScoped;
		int index = findFCell(currentX, currentY, currentZ);
		if (index >= 0)
			fucells.erase(index);
	}
	void clear_furniture_area(int minX, int minZ, int sizeX, int sizeZ)
	{
		ZoneScoped;
		int i, j;
		for (i = 0; i < sizeZ; i++)
			for (j = 0; j < sizeX; j++) {
				currentX = minX + j;
				currentZ = minZ + i;
				clear_furniture();
			}
	}
	int room(int minX, int minZ, int sizeX, int sizeZ)
	{
		ZoneScoped;
		int i, j, ret;
		clear_area(minX, minZ, sizeX, sizeZ);
		for (i = 0; i < sizeZ; i++)
			for (j = 0; j < sizeX; j++) {
				currentX = minX + j;
				currentZ = minZ + i;
				bitCmd("set", "floor,ceiling");
			}
		for (i = 0; i < sizeX; i++) {
			currentX = minX + i;
			currentZ = minZ;
			bitCmd("set", "iwallz-");
		}
		for (i = 0; i < sizeX; i++) {
			currentX = minX + i;
			currentZ = minZ + sizeZ - 1;
			bitCmd("set", "iwallz+");
		}
		for (i = 0; i < sizeZ; i++) {
			currentX = minX;
			currentZ = minZ + i;
			bitCmd("set", "iwallx-");
		}
		for (i = 0; i < sizeZ; i++) {
			currentX = minX + sizeX - 1;
			currentZ = minZ + i;
			bitCmd("set", "iwallx+");
		}
		ret = rooms.size();
		rooms.push_back({ minX, minZ, sizeX, sizeZ });
		return ret;
	}
	int roof(int minX, int minZ, int sizeX, int sizeZ, float height,
		 int type)
	{
		ZoneScoped;
		nlohmann::json roofs = nlohmann::json::array();
		if (lot.find("roofs") != lot.end())
			roofs = lot["roofs"];
		int ret = roofs.size();
		nlohmann::json roof;
		roof["type"] = type;
		roof["position_x"] = minX;
		roof["position_y"] = currentY;
		roof["position_z"] = minZ;
		roof["size_x"] = sizeX;
		roof["size_z"] = sizeZ;
		roof["offset_x"] = 0.0f;
		roof["offset_y"] = 0.0f;
		roof["offset_z"] = 0.0f;
		roof["base_height"] = height;
		roof["max_height"] = height;
		roofs.push_back(roof);
		lot["roofs"] = roofs;
		return ret;
	}
	void clear_roofs()
	{
		ZoneScoped;
		lot["roofs"] = nlohmann::json::array();
	}
	void fcell(const std::vector<Ogre::String> &tags,
		   nlohmann::json furniture, int rotation)
	{
		ZoneScoped;
		nlohmann::json jtags = nlohmann::json::array();
		for (const auto &tag : tags)
			jtags.push_back(tag);
		if (findFCell(currentX, currentY, currentZ) == -1) {
			nlohmann::json fucell;
			buildFCellIndex();
			fucell["x"] = currentX;
			fucell["y"] = currentY;
			fucell["z"] = currentZ;
			fucell["tags"] = jtags;
			fucell["id"] = makeCellKey(fucell);
			fucell["furniture"] = furniture;
			fucell["rotation"] = rotation;
			fucells.push_back(fucell);
		} else {
			int index = findFCell(currentX, currentY, currentZ);
			if (index >= 0) {
				fucells[index]["tags"] = jtags;
				fucells[index]["furniture"] = furniture;
				fucells[index]["rotation"] = rotation;
			}
		}
	}
	nlohmann::json select_furniture(const std::vector<Ogre::String> &tags,
					const std::vector<Ogre::String> &notags)
	{
		ZoneScoped;
		nlohmann::json adata = nlohmann::json::array();
		ECS::get().query_builder<const FurnitureItem>().build().each(
			[&](flecs::entity e, const FurnitureItem &item) {
				bool apply = true;
				// if at least one tag not found, do not add
				for (auto &t : item.tags) {
					std::cout << "tag: " << t << std::endl;
					for (auto &notag : notags) {
						std::cout << "notag: " << notag
							  << std::endl;
					}
				}
				for (auto &t : tags) {
					if (std::find(item.tags.begin(),
						      item.tags.end(),
						      t) == item.tags.end()) {
						apply = false;
						break;
					}
				}
				if (apply)
					// if at least one notag found, do not add
					for (auto &t : item.tags) {
						if (std::find(notags.begin(),
							      notags.end(),
							      t) !=
						    notags.end()) {
							apply = false;
							break;
						}
					}
				if (apply) {
					nlohmann::json jdata =
						nlohmann::json::parse(
							item.properties);
					adata.push_back(jdata);
				}
			});
		if (adata.size() == 0)
			return nlohmann::json();
		float nvalue = noise.GetNoise((float)currentX, (float)currentY,
					      (float)currentZ);
		float selectionf = (nvalue + 1) / 2.0f;
		int selection = Ogre::Math::Floor(selectionf * adata.size());
		return adata[selection];
	}
	bool place_furniture(int room, Ogre::String tags)
	{
		ZoneScoped;
		bool ret = true;
		std::vector<Ogre::String> atags = split_and_trim(tags, ',');
		int i, j;
		int minX = rooms[room].minX;
		int minZ = rooms[room].minZ;
		int sizeX = rooms[room].sizeX;
		int sizeZ = rooms[room].sizeZ;
		int midX = minX + sizeX / 2;
		int midZ = minZ + sizeZ / 2;
		std::cout << "room: " << room << " " << minX << " " << minZ
			  << " " << sizeX << " " << sizeZ << std::endl;
		clear_furniture_area(minX, minZ, sizeX, sizeZ);
		auto nodoors = [&]() -> bool {
			return !isBit("idoorz+") && !isBit("idoorz-") &&
			       !isBit("idoorx+") && !isBit("idoorx-");
		};
		auto nowindows = [&]() -> bool {
			return !isBit("windowz+") && !isBit("windowz-") &&
			       !isBit("windowx+") && !isBit("windowx-");
		};
		auto placeOne = [&](const Ogre::String &bit,
				    const std::vector<Ogre::String> &tags,
				    const std::vector<Ogre::String> &notags,
				    int rotation) -> bool {
			if (findFCell(currentX, currentY, currentZ) >= 0)
				return false;
			if (isBit(bit) && nodoors()) {
				nlohmann::json furniture =
					select_furniture(tags, notags);
				if (furniture.empty()) // got nothing
					return false;
				std::cout << currentX << " " << currentY << " "
					  << currentZ << std::endl;
				fcell(tags, furniture, rotation);
				return true;
			}
			return false;
		};
		std::vector<Ogre::String> etags = atags;
		std::vector<Ogre::String> itags = atags;
		std::vector<Ogre::String> otags = atags;
		std::vector<Ogre::String> ftags = atags;
		struct midpoints {
			int X, Z;
			int rotation;
		};
		struct ranges {
			int startX, endX;
			int startZ, endZ;
			int rotation;
		};
		struct tagdata {
			std::vector<Ogre::String> tags, notagswalls,
				notagswindows;
			bool essential;
			bool filler;
		};
		etags.push_back("essential");
		itags.push_back("important");
		otags.push_back("optional");
		ftags.push_back("filler");
		struct 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 },
		};
		Ogre::String bits_walls[] = { "iwallz+", "iwallx+", "iwallz-",
					      "iwallx-" };
		Ogre::String bits_windows[] = { "windowz+", "windowx+",
						"windowz-", "windowx-" };
		struct midpoints mpoints[] = {
			{ midX, minZ, 2 }, { midX, minZ + sizeZ - 1, 0 },
			{ minX, midZ, 3 }, { minX + sizeX - 1, midZ, 1 },
			{ midX, minZ, 2 }, { midX, minZ + sizeZ - 1, 0 },
			{ minX, midZ, 3 }, { minX + sizeX - 1, midZ, 1 }
		};
		struct ranges range_points[] = {
			{ minX, sizeX - 1, minZ, minZ, 2 },
			{ minX, sizeX - 1, minZ + sizeZ - 1, minZ + sizeZ - 1,
			  0 },
			{ minX, minX, minZ, minZ + sizeZ - 1, 3 },
			{ minX + sizeX - 1, minX + sizeX - 1, minZ,
			  minZ + sizeZ - 1, 1 },
		};
		for (const auto &mtags : taglist) {
			bool placed = false;
			if (!placed)
				for (auto &p : mpoints) {
					currentX = p.X;
					currentZ = p.Z;
					if (!placed && nodoors()) {
						const Ogre::String &bit =
							bits_walls[p.rotation];
						placed = placeOne(
							bit, mtags.tags,
							mtags.notagswalls,
							p.rotation);
						if (placed && !mtags.filler)
							break;
					}
					if (mtags.filler)
						placed = false;
				}
			if (!placed)
				for (auto &p : range_points) {
					int i, j;
					for (i = p.startZ; i <= p.endZ; i++) {
						for (j = p.startX; j <= p.endX;
						     j++) {
							currentX = j;
							currentZ = i;
							const Ogre::String &bit = bits_walls
								[p.rotation];

							placed = placeOne(
								bit, mtags.tags,
								mtags.notagswalls,
								p.rotation);
							if (placed &&
							    !mtags.filler)
								break;
						}
						if (placed && !mtags.filler)
							break;
					}
					if (placed && !mtags.filler)
						break;
				}
			if (mtags.filler)
				placed = false;
			if (!placed)
				for (auto &p : mpoints) {
					if (!placed && nodoors()) {
						const Ogre::String &bit =
							bits_windows[p.rotation];
						placed = placeOne(
							bit, mtags.tags,
							mtags.notagswindows,
							p.rotation);
						if (placed && !mtags.filler)
							break;
					}
				}
			if (mtags.filler)
				placed = false;
			if (!placed)
				for (auto &p : range_points) {
					int i, j;
					for (i = p.startZ; i <= p.endZ; i++) {
						for (j = p.startX; j <= p.endX;
						     j++) {
							currentX = j;
							currentZ = i;
							const Ogre::String &bit =
								bits_windows
									[p.rotation];
							placed = placeOne(
								bit, mtags.tags,
								mtags.notagswindows,
								p.rotation);
							if (placed &&
							    !mtags.filler)
								break;
						}
						if (placed && !mtags.filler)
							break;
					}
					if (placed && !mtags.filler)
						break;
				}
			if (!placed && !mtags.filler) {
				ret = false;
				break; // do not place anything if essentials were not placed
			}
			if (!placed && !mtags.filler)
				break;
		}
		return ret;
	}

	std::vector<std::pair<int, int> > roomEdge(int room)
	{
		ZoneScoped;
		std::vector<std::pair<int, int> > seg;
		std::set<std::pair<int, int> > seen;
		int i;
		if (rooms[room].sizeX <= 0 || rooms[room].sizeZ <= 0)
			goto out;
		else if (rooms[room].sizeX == 1 && rooms[room].sizeZ == 1) {
			seg.push_back({ rooms[room].minX, rooms[room].minZ });
			goto out;
		} else if (rooms[room].sizeZ == 1) {
			for (i = 0; i < rooms[room].sizeX; i++) {
				seg.push_back({ rooms[room].minX + i,
						rooms[room].minZ });
			}
			goto out;
		} else if (rooms[room].sizeX == 1) {
			for (i = 0; i < rooms[room].sizeZ; i++) {
				seg.push_back({ rooms[room].minX,
						rooms[room].minZ + i });
			}
			goto out;
		}

		for (i = 0; i < rooms[room].sizeX; i++) {
			// filling values for minZ == 0
			// starting with X = 0
			seen.insert({ rooms[room].minX + i, rooms[room].minZ });
		}
		for (i = 0; i < rooms[room].sizeZ; i++) {
			// zero part is laready filled
			seen.insert({ rooms[room].minX, rooms[room].minZ + i });
		}
		for (i = 0; i < rooms[room].sizeX; i++) {
			seen.insert(
				{ rooms[room].minX + i,
				  rooms[room].minZ + rooms[room].sizeZ - 1 });
		}
		for (i = 0; i < rooms[room].sizeZ; i++) {
			seen.insert({ rooms[room].minX + rooms[room].sizeX - 1,
				      rooms[room].minZ + i });
		}
		for (auto &cell : seen)
			seg.push_back(cell);
out:
		dumpSeg(seg);
		OgreAssert(seg.size() <= rooms[room].sizeX * rooms[room].sizeZ,
			   "bad edge returned");
		return seg;
	}
	bool adjacent(std::pair<int, int> c1, std::pair<int, int> c2)
	{
		ZoneScoped;
		int dx = std::abs(c1.first - c2.first);
		int dz = std::abs(c1.second - c2.second);
		return (dx == 0 && dz == 1) || (dx == 1 && dz == 0);
	}
	void dumpSeg(const std::vector<std::pair<int, int> > &seg)
	{
		ZoneScoped;
		int count = 0;
		for (auto &cell : seg) {
			std::cout << count << " " << cell.first << " "
				  << cell.second << std::endl;
			count++;
		}
	}
	void adjacentCells(int room, int other,
			   std::vector<std::pair<int, int> > &ret1,
			   std::vector<std::pair<int, int> > &ret2)
	{
		ZoneScoped;
		std::vector<std::pair<int, int> > room_seg = roomEdge(room);
		std::vector<std::pair<int, int> > other_seg = roomEdge(other);
		std::vector<std::pair<int, int> > ret;
		std::cout << "main: " << std::endl;
		dumpSeg(room_seg);
		std::cout << "other: " << std::endl;
		dumpSeg(other_seg);
		for (auto &cell : room_seg)
			for (auto &ocell : other_seg) {
				if (adjacent(cell, ocell)) {
					ret1.push_back(cell);
					ret2.push_back(ocell);
				}
			}
	}
	void createExit0(int room)
	{
		ZoneScoped;
		int posX = rooms[room].minX;
		int posZ = rooms[room].minZ;
		int i;
		int sumX = -1;
		int count = 0;
		for (i = 0; i < rooms[room].sizeX; i++) {
			currentX = posX + i;
			currentZ = posZ;
			if (findCell(posX + i, currentY, posZ - 1) != -1)
				continue;
			if (isBit("iwallz-")) {
				if (sumX == -1)
					sumX = posX + i;
				else
					sumX += posX + i;
				count++;
			}
		}
		if (count > 0) {
			int rposX = sumX / count;
			currentX = rposX;
			currentZ = posZ;
			bitCmd("clear", "iwallz-");
			bitCmd("set", "idoorz-");
		}
	}
	void createExit1(int room)
	{
		ZoneScoped;
		int posX = rooms[room].minX;
		int posZ = rooms[room].minZ + rooms[room].sizeZ - 1;
		int i;
		int sumX = -1;
		int count = 0;
		for (i = 0; i < rooms[room].sizeX; i++) {
			currentX = posX + i;
			currentZ = posZ;
			if (findCell(posX + i, currentY, posZ + 1) != -1)
				continue;
			if (isBit("iwallz+")) {
				if (sumX == -1)
					sumX = posX + i;
				else
					sumX += posX + i;
				count++;
			}
		}
		if (count > 0) {
			int rposX = sumX / count;
			currentX = rposX;
			currentZ = posZ;
			bitCmd("clear", "iwallz+");
			bitCmd("set", "idoorz+");
		}
	}
	void createExit2(int room)
	{
		ZoneScoped;
		int posX = rooms[room].minX + rooms[room].sizeX - 1;
		int posZ = rooms[room].minZ;
		int i;
		int sumZ = -1;
		int count = 0;
		for (i = 0; i < rooms[room].sizeZ; i++) {
			currentX = posX;
			currentZ = posZ + i;
			if (findCell(posX + 1, currentY, posZ + i) != -1)
				continue;
			if (isBit("iwallx+")) {
				if (sumZ == -1)
					sumZ = posZ + i;
				else
					sumZ += posZ + i;
				count++;
			}
		}
		if (count > 0) {
			int rposZ = sumZ / count;
			currentX = posX;
			currentZ = rposZ;
			bitCmd("clear", "iwallx+");
			bitCmd("set", "idoorx+");
		}
	}
	void createExit3(int room)
	{
		ZoneScoped;
		int posX = rooms[room].minX;
		int posZ = rooms[room].minZ;
		int i;
		int sumZ = -1;
		int count = 0;
		for (i = 0; i < rooms[room].sizeZ; i++) {
			currentX = posX;
			currentZ = posZ + i;
			if (findCell(posX - 1, currentY, posZ + i) != -1)
				continue;
			if (isBit("iwallx-")) {
				if (sumZ == -1)
					sumZ = posZ + i;
				else
					sumZ += posZ + i;
				count++;
			}
		}
		if (count > 0) {
			int rposZ = sumZ / count;
			currentX = posX;
			currentZ = rposZ;
			bitCmd("clear", "iwallx-");
			bitCmd("set", "idoorx-");
		}
	}
	void createWindows(int room)
	{
		ZoneScoped;
		int posX = rooms[room].minX;
		int posZ = rooms[room].minZ;
		int i;
		for (i = 0; i < rooms[room].sizeX; i++) {
			currentX = posX + i;
			currentZ = posZ;
			if (findCell(posX + i, currentY, posZ - 1) != -1)
				continue;
			if (isBit("iwallz-")) {
				bitCmd("clear", "iwallz-");
				bitCmd("set", "iwindowz-");
			}
		}
		posX = rooms[room].minX;
		posZ = rooms[room].minZ + rooms[room].sizeZ - 1;
		for (i = 0; i < rooms[room].sizeX; i++) {
			currentX = posX + i;
			currentZ = posZ;
			if (findCell(posX + i, currentY, posZ + 1) != -1)
				continue;
			if (isBit("iwallz+")) {
				bitCmd("clear", "iwallz+");
				bitCmd("set", "iwindowz+");
			}
		}
		posX = rooms[room].minX + rooms[room].sizeX - 1;
		posZ = rooms[room].minZ;
		for (i = 0; i < rooms[room].sizeZ; i++) {
			currentX = posX;
			currentZ = posZ + i;
			if (findCell(posX + 1, currentY, posZ + i) != -1)
				continue;
			if (isBit("iwallx+")) {
				bitCmd("clear", "iwallx+");
				bitCmd("set", "iwindowx+");
			}
		}
		posX = rooms[room].minX;
		posZ = rooms[room].minZ;
		for (i = 0; i < rooms[room].sizeZ; i++) {
			currentX = posX;
			currentZ = posZ + i;
			if (findCell(posX - 1, currentY, posZ + i) != -1)
				continue;
			if (isBit("iwallx-")) {
				bitCmd("clear", "iwallx-");
				bitCmd("set", "iwindowx-");
			}
		}
	}
	void createExterior()
	{
		ZoneScoped;
		int room;
		for (room = 0; room < rooms.size(); room++) {
			int posX = rooms[room].minX;
			int posZ = rooms[room].minZ;
			int i;
			for (i = 0; i < rooms[room].sizeX; i++) {
				currentX = posX + i;
				currentZ = posZ;
				if (findCell(posX + i, currentY, posZ - 1) !=
				    -1)
					continue;
				if (isBit("iwallz-"))
					bitCmd("set", "wallz-");
				if (isBit("idoorz-"))
					bitCmd("set", "doorz-");
				if (isBit("iwindowz-"))
					bitCmd("set", "windowz-");
			}
			posX = rooms[room].minX;
			posZ = rooms[room].minZ + rooms[room].sizeZ - 1;
			for (i = 0; i < rooms[room].sizeX; i++) {
				currentX = posX + i;
				currentZ = posZ;
				if (findCell(posX + i, currentY, posZ + 1) !=
				    -1)
					continue;
				if (isBit("iwallz+"))
					bitCmd("set", "wallz+");
				if (isBit("idoorz+"))
					bitCmd("set", "doorz+");
				if (isBit("iwindowz+"))
					bitCmd("set", "windowz+");
			}
			posX = rooms[room].minX + rooms[room].sizeX - 1;
			posZ = rooms[room].minZ;
			for (i = 0; i < rooms[room].sizeZ; i++) {
				currentX = posX;
				currentZ = posZ + i;
				if (findCell(posX + 1, currentY, posZ + i) !=
				    -1)
					continue;
				if (isBit("iwallx+"))
					bitCmd("set", "wallx+");
				if (isBit("idoorx+"))
					bitCmd("set", "doorx+");
				if (isBit("iwindowx+"))
					bitCmd("set", "windowx+");
			}
			posX = rooms[room].minX;
			posZ = rooms[room].minZ;
			for (i = 0; i < rooms[room].sizeZ; i++) {
				currentX = posX;
				currentZ = posZ + i;
				if (findCell(posX - 1, currentY, posZ + i) !=
				    -1)
					continue;
				if (isBit("iwallx-"))
					bitCmd("set", "wallx-");
				if (isBit("idoorx-"))
					bitCmd("set", "doorx-");
				if (isBit("iwindowx-"))
					bitCmd("set", "windowx-");
			}
		}
	}
	void connectRooms(int r1, int r2)
	{
		ZoneScoped;
		std::vector<std::pair<int, int> > seg1, seg2;
		int index = 0;
		adjacentCells(r1, r2, seg1, seg2);
		OgreAssert(seg1.size() == seg2.size(), "bad edges");
		if (seg1.size() == 0 || seg2.size() == 0)
			return;
		for (auto &cell : seg1) {
			std::cout << "seg1: " << cell.first << " "
				  << cell.second << std::endl;
		}
		for (auto &cell : seg2) {
			std::cout << "seg2: " << cell.first << " "
				  << cell.second << std::endl;
		}
		if (seg1.size() > 1) {
			int sumX = 0, sumZ = 0;
			int count = 0;
			// find average point of first room cells
			for (count = 0; count < seg1.size(); count++) {
				sumX += seg1[count].first;
				sumZ += seg1[count].second;
			}
			sumX /= seg1.size();
			sumZ /= seg1.size();
			// find closest point to average;
			int distance = -1;
			for (count = 0; count < seg1.size(); count++) {
				int mdx = seg1[count].first - sumX;
				int mdz = seg1[count].second - sumZ;
				if (count == 0) {
					index = 0;
					distance = mdx * mdx + mdz * mdz;
				} else {
					if (distance > mdx * mdx + mdz * mdz) {
						distance =
							mdx * mdx + mdz * mdz;
						index = count;
					}
				}
			}
		} else
			index = 0;
		OgreAssert(adjacent(seg1[index], seg2[index]),
			   "connected points are not adjacent!");
		int dx = seg1[index].first - seg2[index].first;
		int dz = seg1[index].second - seg2[index].second;
		std::cout << dx << " " << dz << std::endl;
		if (dx == 0 && dz == 0) {
			std::cerr << "can't connect rooms" << std::endl;
			return;
		}
		OgreAssert(std::abs(dx) == 1 && dz == 0 ||
				   dx == 0 && std::abs(dz) == 1,
			   "shit happens");
		std::cout << "points: " << seg1[index].first << " "
			  << seg1[index].second << " ";
		std::cout << seg2[index].first << " " << seg2[index].second
			  << std::endl;
		if (dz < 0) {
			currentX = seg1[index].first;
			currentZ = seg1[index].second;
			OgreAssert(isBit("iwallz+"), "bad bit");
			bitCmd("clear", "iwallz+");
			bitCmd("set", "idoorz+");
			currentX = seg2[index].first;
			currentZ = seg2[index].second;
			OgreAssert(isBit("iwallz-"), "bad bit");
			bitCmd("clear", "iwallz-");
			bitCmd("set", "idoorz-");
		}
		if (dz > 0) {
			currentX = seg1[index].first;
			currentZ = seg1[index].second;
			OgreAssert(isBit("iwallz-"), "bad bit");
			bitCmd("clear", "iwallz-");
			bitCmd("set", "idoorz-");
			currentX = seg2[index].first;
			currentZ = seg2[index].second;
			OgreAssert(isBit("iwallz+"), "bad bit");
			bitCmd("clear", "iwallz+");
			bitCmd("set", "idoorz+");
		}
		if (dx < 0) {
			currentX = seg1[index].first;
			currentZ = seg1[index].second;
			OgreAssert(isBit("iwallx+"), "bad bit");
			bitCmd("clear", "iwallx+");
			bitCmd("set", "idoorx+");
			currentX = seg2[index].first;
			currentZ = seg2[index].second;
			OgreAssert(isBit("iwallx-"), "bad bit");
			bitCmd("clear", "iwallx-");
			bitCmd("set", "idoorx-");
		}
		if (dx > 0) {
			currentX = seg1[index].first;
			currentZ = seg1[index].second;
			OgreAssert(isBit("iwallx-"), "bad bit");
			bitCmd("clear", "iwallx-");
			bitCmd("set", "idoorx-");
			currentX = seg2[index].first;
			currentZ = seg2[index].second;
			OgreAssert(isBit("iwallx+"), "bad bit");
			bitCmd("clear", "iwallx+");
			bitCmd("set", "idoorx+");
		}
		/*
        int cell1 = findCell(seg1[index].first, currentY, seg1[index].second);
        int cell2 = findCell(seg2[index].first, currentY, seg2[index].second);
        std::cout << sumX << " " << sumZ << "..." << sumOX << " "
              << sumOZ << std::endl;
        std::cout << dx << " " << dz << " " << cell1 << " " << cell2
              << std::endl;
        // OgreAssert(false, "eeeeeh");
        // OgreAssert(false, "eeeeeh");
        */
	}
	virtual ~CellsScript()
	{
		ZoneScoped;
		lua_close(L);
	}
	void run()
	{
		ZoneScoped;
		int result = luaL_dostring(L, cellScript.c_str());
		if (result != LUA_OK) {
			std::cerr << "Lua script execution failed: "
				  << lua_tostring(L, -1) << std::endl;
			lua_pop(L, 1);
		} else
			std::cout << "Running script OK" << std::endl;
	}
};
bool editRoofs(const Ogre::String &lotLabel, nlohmann::json &lot)
{
	ZoneScoped;
	bool changed = false;
	static int roofPosition[3] = { 0, 0 };
	static int roofSize[2] = { 1, 1 };
	static float roofOffset[3] = { 0, 0, 0 };
	static float baseHeight = 0.5f;
	static float maxHeight = 0.5f;
	static int roofType = 0;
	const char *items[] = { "Flat", "Normal", "Normal2", "Cone",
				"Cylinder" };
	nlohmann::json roofs = nlohmann::json::array();
	if (lot.find("roofs") != lot.end())
		roofs = lot["roofs"];
	Ogre::String addNewLabel = lotLabel + "_AddNewRoof";
	if (ImGui::CollapsingHeader(("Roofs..." + lotLabel).c_str())) {
		ImGui::Text("Roofs");
		ImGui::Separator();
		ImGui::SeparatorText("in cell sizes");
		int roofCount = 0;
		for (auto &roof : roofs) {
			Ogre::String roofLabel =
				lotLabel + "_" +
				Ogre::StringConverter::toString(roofCount);
			ImGui::Text("roof - %d", roofCount);
			int mroofType = roof["type"].get<int>();
			int mroofPosition[3];
			int mroofSize[2];
			float mroofOffset[3];
			float mbaseHeight, mmaxHeight;
			mroofPosition[0] = roof["position_x"].get<int>();
			mroofPosition[1] = roof["position_y"].get<int>();
			mroofPosition[2] = roof["position_z"].get<int>();
			mroofSize[0] = roof["size_x"].get<int>();
			mroofSize[1] = roof["size_z"].get<int>();
			mroofOffset[0] = roof["offset_x"].get<float>();
			mroofOffset[1] = roof["offset_y"].get<float>();
			mroofOffset[2] = roof["offset_z"].get<float>();
			mbaseHeight = roof["base_height"].get<float>();
			mmaxHeight = roof["max_height"].get<float>();
			if (ImGui::Combo(("Roof Type##" + lotLabel).c_str(),
					 &mroofType, items,
					 IM_ARRAYSIZE(items)))
				changed = true;
			if (ImGui::SliderInt(
				    ("Roof Position X##" + lotLabel).c_str(),
				    &mroofPosition[0], -100, 100))
				changed = true;
			if (ImGui::SliderInt(
				    ("Roof Position Y##" + lotLabel).c_str(),
				    &mroofPosition[1], -100, 100))
				changed = true;
			if (ImGui::SliderInt(
				    ("Roof Position Z##" + lotLabel).c_str(),
				    &mroofPosition[2], -100, 100))
				changed = true;
			if (ImGui::SliderInt(
				    ("Roof Size X##" + lotLabel).c_str(),
				    &mroofSize[0], 1, 50))
				changed = true;
			if (ImGui::SliderInt(
				    ("Roof Size Z##" + lotLabel).c_str(),
				    &mroofSize[1], 1, 50))
				changed = true;
			if (ImGui::SliderFloat(
				    ("Roof Offset X##" + lotLabel).c_str(),
				    &mroofOffset[0], -10.0f, 10.0f))
				changed = true;
			if (ImGui::SliderFloat(
				    ("Roof Offset Y##" + lotLabel).c_str(),
				    &mroofOffset[1], -10.0f, 10.0f))
				changed = true;
			if (ImGui::SliderFloat(
				    ("Roof Offset Z##" + lotLabel).c_str(),
				    &mroofOffset[2], -10.0f, 10.0f))
				changed = true;
			if (ImGui::SliderFloat(
				    ("Roof Base Height##" + lotLabel).c_str(),
				    &mbaseHeight, 0.1f, 100.0f))
				changed = true;
			mmaxHeight = Ogre::Math::Clamp(mmaxHeight, mbaseHeight,
						       100.0f);
			if (ImGui::SliderFloat(
				    ("Roof Max Height##" + lotLabel).c_str(),
				    &mmaxHeight, 0.1f, 100.0f))
				changed = true;
			if (changed) {
				roof["type"] = mroofType;
				roof["position_x"] = mroofPosition[0];
				roof["position_y"] = mroofPosition[1];
				roof["position_z"] = mroofPosition[2];
				roof["size_x"] = mroofSize[0];
				roof["size_z"] = mroofSize[1];
				roof["offset_x"] = mroofOffset[0];
				roof["offset_y"] = mroofOffset[1];
				roof["offset_z"] = mroofOffset[2];
				roof["base_height"] = mbaseHeight;
				roof["max_height"] = mmaxHeight;
			}
			if (ImGui::SmallButton(
				    ("Delete##" + roofLabel).c_str())) {
				roofs.erase(roofCount);
				changed = true;
				break;
			}
		}
		roofCount++;
		ImGui::Combo(("Roof Type##" + addNewLabel).c_str(), &roofType,
			     items, IM_ARRAYSIZE(items));
		ImGui::SliderInt(("Roof Position X##" + addNewLabel).c_str(),
				 &roofPosition[0], -100, 100);
		ImGui::SliderInt(("Roof Position Y##" + addNewLabel).c_str(),
				 &roofPosition[1], 0, 100);
		ImGui::SliderInt(("Roof Position Z##" + addNewLabel).c_str(),
				 &roofPosition[2], -100, 100);
		ImGui::SliderInt(("Roof Size X##" + addNewLabel).c_str(),
				 &roofSize[0], 1, 50);
		ImGui::SliderInt(("Roof Size Z##" + addNewLabel).c_str(),
				 &roofSize[1], 1, 50);
		ImGui::SliderFloat(("Roof Offset X##" + addNewLabel).c_str(),
				   &roofOffset[0], -10.0f, 10.0f);
		ImGui::SliderFloat(("Roof Offset Y##" + addNewLabel).c_str(),
				   &roofOffset[1], -10.0f, 10.0f);
		ImGui::SliderFloat(("Roof Offset Z##" + addNewLabel).c_str(),
				   &roofOffset[2], -10.0f, 10.0f);
		ImGui::SliderFloat(("Roof Base Height##" + addNewLabel).c_str(),
				   &baseHeight, 0.1f, 100.0f);
		ImGui::SliderFloat(("Roof Max Height##" + addNewLabel).c_str(),
				   &maxHeight, 0.1f, 100.0f);
		if (ImGui::SmallButton(("Add##" + addNewLabel).c_str())) {
			nlohmann::json roof;
			roof["type"] = roofType;
			roof["position_x"] = roofPosition[0];
			roof["position_y"] = roofPosition[1];
			roof["position_z"] = roofPosition[2];
			roof["size_x"] = roofSize[0];
			roof["size_z"] = roofSize[1];
			roof["offset_x"] = roofOffset[0];
			roof["offset_y"] = roofOffset[1];
			roof["offset_z"] = roofOffset[2];
			roof["base_height"] = baseHeight;
			roof["max_height"] = maxHeight;
			roofs.push_back(roof);
			changed = true;
		}
		if (changed)
			lot["roofs"] = roofs;
	}
	return changed;
}
bool editLot(const Ogre::String &lotLabel, nlohmann::json &lot)
{
	ZoneScoped;
	bool changed = false;
	float angle = lot["angle"].get<float>();
	if (ImGui::SliderFloat(("Lot Angle##" + lotLabel).c_str(), &angle,
			       -180.0f, 180.0f)) {
		lot["angle"] = angle;
		changed = true;
	}
	float elevation = lot["elevation"].get<float>();
	if (ImGui::SliderFloat(("Lot Elevation##" + lotLabel).c_str(),
			       &elevation, -10.0f, 10.0f)) {
		lot["elevation"] = elevation;
		changed = true;
	}
	int width = lot["width"].get<int>();
	if (ImGui::SliderInt(("Lot Width##" + lotLabel).c_str(), &width, 4,
			     50)) {
		lot["width"] = width;
		changed = true;
	}
	int depth = lot["depth"].get<int>();
	if (ImGui::SliderInt(("Lot Depth##" + lotLabel).c_str(), &depth, 4,
			     50)) {
		lot["depth"] = depth;
		changed = true;
	}
	nlohmann::json cells = nlohmann::json::array();
	static char text_buffer[16384];
	strcpy(text_buffer, "");
	/* copy script data from json to a buffer */
	if (lot.find("cellScript") != lot.end()) {
		Ogre::String script = lot["cellScript"].get<Ogre::String>();
		strncpy(text_buffer, script.c_str(), sizeof(text_buffer));
	}
	/* edit a buffer and if changed, copy a buffer back to json */
	if (ImGui::InputTextMultiline(("Cells Script##" + lotLabel).c_str(),
				      text_buffer, IM_ARRAYSIZE(text_buffer),
				      ImVec2(1000,
					     ImGui::GetTextLineHeight() * 20),
				      ImGuiInputTextFlags_AllowTabInput)) {
		lot["cellScript"] = Ogre::String(text_buffer);
	}
	if (ImGui::IsItemDeactivatedAfterEdit()) {
		lot["cellScript"] = Ogre::String(text_buffer);
		changed = true;
	}
	if (ImGui::SmallButton(("Execute script...##" + lotLabel).c_str())) {
		CellsScript script(lot["cellScript"].get<Ogre::String>(), lot);
		script.run();
		changed = true;
	}

	for (const auto &cell : lot["cells"])
		cells.push_back(cell);
	if (ImGui::CollapsingHeader(("Cells...##" + lotLabel).c_str())) {
		if (cells.size() == 0) {
			Ogre::String createCellsLabel =
				"Create cells...##" + lotLabel;
			if (ImGui::SmallButton(createCellsLabel.c_str())) {
				nlohmann::json cell;
				cell["x"] = 0;
				cell["y"] = 0;
				cell["z"] = 0;
				cell["flags"] = 0U;
				cell["id"] = makeCellKey(cell);
				lot["cells"].push_back(cell);
				changed = true;
			}
		} else {
			int cellCount = 0;
			for (auto &cell : cells) {
				Ogre::String cellLabel =
					lotLabel + "cell" +
					Ogre::StringConverter::toString(
						cellCount);
				ImGui::Text("x = %d", cell["x"].get<int>());
				ImGui::Text("y = %d", cell["y"].get<int>());
				ImGui::Text("z = %d", cell["z"].get<int>());
				ImGui::Text("flags = %016lx",
					    cell["flags"].get<uint64_t>());
				changed = changed || editCell(cellLabel, cell);
				ImGui::Separator();
				auto makeCreateButton = [&](const Ogre::String
								    &label,
							    int offsetX,
							    int offsetY,
							    int offsetZ) {
					if (ImGui::SmallButton(label.c_str())) {
						int currentX =
							cell["x"].get<int>();
						int currentY =
							cell["y"].get<int>();
						int currentZ =
							cell["z"].get<int>();
						nlohmann::json newcell;
						newcell["x"] =
							currentX + offsetX;
						newcell["y"] =
							currentY + offsetY;
						newcell["z"] =
							currentZ + offsetZ;
						newcell["flags"] = 0U;
						bool ok = true;
						for (const auto &chk : cells) {
							int64_t main_key =
								makeCellKey(
									newcell);
							int64_t chk_key =
								makeCellKey(
									chk);
							if (main_key ==
							    chk_key) {
								ok = false;
								break;
							}
						}
						if (ok) {
							newcell["id"] =
								makeCellKey(
									newcell);
							cells.push_back(
								newcell);
							changed = true;
						}
					}
				};
				struct CreateButtons {
					Ogre::String label;
					int x, y, z;
				};
				struct CreateButtons cbuttons[] = {
					{ "NewX+##" + cellLabel, 1, 0, 0 },
					{ "NewX-##" + cellLabel, -1, 0, 0 },
					{ "NewY+##" + cellLabel, 0, 1, 0 },
					{ "NewY-##" + cellLabel, 0, -1, 0 },
					{ "NewZ+##" + cellLabel, 0, 0, 1 },
					{ "NewZ-##" + cellLabel, 0, 0, -1 },
				};
				int i;
				for (i = 0;
				     i < sizeof(cbuttons) / sizeof(cbuttons[0]);
				     i++) {
					if (i > 0)
						ImGui::SameLine();
					makeCreateButton(cbuttons[i].label,
							 cbuttons[i].x,
							 cbuttons[i].y,
							 cbuttons[i].z);
				}
				Ogre::String cellDeleteLabel =
					"Delete##" + cellLabel;

				if (ImGui::SmallButton(
					    cellDeleteLabel.c_str())) {
					cells.erase(cellCount);
					changed = true;
				}
				if (changed) {
					lot["cells"] = cells;
					break;
				}
				cellCount++;
			}
		}
	}
	changed = changed || editRoofs(lotLabel, lot);
	return changed;
}
void commandEraseLot(nlohmann::json &district, int lotIndex)
{
	ZoneScoped;
	nlohmann::json lots = nlohmann::json::array();
	for (const auto &lot : district["lots"])
		lots.push_back(lot);
	lots.erase(lotIndex);
	district["lots"] = lots;
}
void cleanupLot(nlohmann::json &lot)
{
	ZoneScoped;
	lot.erase("cells");
	lot.erase("furniture_cells");
	lot.erase("roofs");
}
void addLotTemplate(nlohmann::json &district, nlohmann::json &lotTemplates,
		    int lotIndex)
{
	ZoneScoped;
	if (district.find("lots") != district.end()) {
		nlohmann::json &lots = district["lots"];
		nlohmann::json lotTemplate = lots[lotIndex];
		cleanupLot(lotTemplate);
		lotTemplates.push_back(lotTemplate);
	}
}
void addLotFromTemplate(nlohmann::json &district,
			const nlohmann::json &lotTemplates,
			int lotTemplateIndex)
{
	ZoneScoped;
	nlohmann::json lots = nlohmann::json::array();
	OgreAssert(lotTemplateIndex < lotTemplates.size() &&
			   lotTemplates.size() > 0,
		   "bad template");
	if (lotTemplates.size() == 0 || lotTemplateIndex >= lotTemplates.size())
		return;
	for (const auto &lot : district["lots"])
		lots.push_back(lot);
	nlohmann::json t = lotTemplates[lotTemplateIndex];
	float angle = 0.0f;
	float nextAngle = angle;
	float radius = district["radius"].get<float>();
	if (lots.size() > 0) {
		angle = lots[0]["angle"].get<float>();
		nextAngle = angle;
		for (auto &lot : lots) {
			float width = lot["width"].get<float>();
			if (angle < lot["angle"].get<float>()) {
				angle = lot["angle"].get<float>();
				nextAngle = angle +
					    Ogre::Math::ASin(width /
							     (radius * 2.0f))
						    .valueDegrees();
			}
		}
	}
	t["angle"] = nextAngle;
	CellsScript script(t["cellScript"].get<Ogre::String>(), t);
	script.run();
	lots.push_back(t);
	district["lots"] = lots;
}
void addLot(nlohmann::json &district, float angle, int width, int depth,
	    float elevation, const Ogre::String &script)
{
	ZoneScoped;
	nlohmann::json lots = nlohmann::json::array();
	for (const auto &lot : district["lots"])
		lots.push_back(lot);
	nlohmann::json l;
	l["angle"] = angle;
	l["width"] = width;
	l["depth"] = depth;
	l["elevation"] = elevation;
	l["cellScript"] = script;
	l["objects"] = nlohmann::json::array();
	CellsScript cellScript(l["cellScript"].get<Ogre::String>(), l);
	cellScript.run();
	lots.push_back(l);
	district["lots"] = lots;
}
bool editDistrict(const Ogre::String &districtLabel, nlohmann::json &district,
		  nlohmann::json &lotTemplates)
{
	ZoneScoped;
	bool changed = false;
	nlohmann::json lots = nlohmann::json::array();
	for (const auto &lot : district["lots"])
		lots.push_back(lot);
	int lotCount = 0;
	for (auto &lot : lots) {
		Ogre::String lotLabel =
			districtLabel + "_lot" +
			Ogre::StringConverter::toString(lotCount);
		ImGui::Text("%s", lotLabel.c_str());
		if (ImGui::CollapsingHeader(
			    ("Edit lot... ##" + lotLabel).c_str())) {
			bool lotChanged = editLot(lotLabel, lot);
			lot["_dirty"] = lotChanged;
			changed = changed || lotChanged;
			ImGui::Separator();
			if (ImGui::SmallButton(
				    ("Add as template##" + lotLabel).c_str())) {
				addLotTemplate(district, lotTemplates,
					       lotCount);
				changed = true;
			}
			if (ImGui::SmallButton(
				    ("Lot Delete##" + lotLabel).c_str())) {
				commandEraseLot(district, lotCount);
				changed = true;
				break;
			}
		}
		lotCount++;
	}
	ImGui::Separator();
	int templateCount = 0;
	for (auto &t : lotTemplates) {
		Ogre::String templateLabel =
			districtLabel + "_template" +
			Ogre::StringConverter::toString(templateCount);
		Ogre::String templateName;
		if (t.find("templateName") != t.end())
			templateName = t["templateName"];
		char nameBuffer[64];
		strncpy(nameBuffer, templateName.c_str(), sizeof(nameBuffer));
		ImGui::Text("%d", templateCount);
		if (ImGui::InputText(("Name##" + templateLabel).c_str(),
				     nameBuffer, sizeof(nameBuffer))) {
			templateName = Ogre::String(nameBuffer);
			t["templateName"] = templateName;
			changed = true;
		}
		if (ImGui::SmallButton(
			    ("Add lot with template##" + districtLabel +
			     Ogre::StringConverter::toString(templateCount))
				    .c_str())) {
			int d = district["lots"].size();
			addLotFromTemplate(district, lotTemplates,
					   templateCount);
			int d2 = district["lots"].size();
			OgreAssert(d2 > d, "failed to add lot");
			lots = district["lots"];
			changed = true;
		}
		templateCount++;
	}
	ImGui::Separator();
	if (ImGui::SmallButton("Add lot")) {
		addLot(district, 0.0f, 10, 10, 0.0f, "-- cell creation script");
		changed = true;
	}
	district["lots"] = lots;
	float radius = 50.0f;
	bool plazza = false;
	if (district.find("plazza") != district.end())
		plazza = district["plazza"].get<bool>();
	if (district.find("radius") != district.end())
		radius = district["radius"].get<float>();
	if (ImGui::Checkbox("Plazza", &plazza)) {
		changed = true;
		district["plazza"] = plazza;
	}
	float elevation = 0.0f, height = 0.2f;
	if (district.find("elevation") != district.end())
		elevation = district["elevation"].get<float>();
	if (district.find("height") != district.end())
		height = district["height"].get<float>();
	if (ImGui::SliderFloat("Radius", &radius, 20.0f, 100.0f)) {
		district["radius"] = radius;
		changed = true;
	}
	if (ImGui::SliderFloat("Height", &height, 0.1f, 10.0f)) {
		district["height"] = height;
		changed = true;
	}
	if (ImGui::SliderFloat("Elevation", &elevation, -10.0f, 10.0f)) {
		district["elevation"] = elevation;
		changed = true;
	}
	return changed;
}
bool editColorRects(nlohmann::json &rects)
{
	ZoneScoped;
	bool changed = false;
	ImGui::Text("Color Rects");
	for (auto it = rects.begin(); it != rects.end(); it++) {
		Ogre::String key = it.key();
		ImGui::Text("%s", key.c_str());
		float left = 0.0f, top = 0.0f, right = 0.1f, bottom = 0.1f;
		nlohmann::json nrect = it.value();
		left = nrect["left"].get<float>();
		top = nrect["top"].get<float>();
		right = nrect["right"].get<float>();
		bottom = nrect["bottom"].get<float>();
		float ccolor[4] = { 0, 0, 0, 1 };
		Ogre::ColourValue colour;
		from_json(nrect["color"], colour);
		ccolor[0] = colour.r;
		ccolor[1] = colour.g;
		ccolor[2] = colour.b;
		ccolor[3] = colour.a;
		if (ImGui::InputFloat(("left##" + key).c_str(), &left))
			changed = true;
		if (ImGui::InputFloat(("top##" + key).c_str(), &top))
			changed = true;
		if (ImGui::InputFloat(("right##" + key).c_str(), &right))
			changed = true;
		if (ImGui::InputFloat(("bottom##" + key).c_str(), &bottom))
			changed = true;
		if (ImGui::ColorPicker4(("Color##" + key).c_str(), ccolor))
			changed = true;
		if (changed) {
			left = Ogre::Math::Clamp(left, 0.0f, 1.0f);
			top = Ogre::Math::Clamp(top, 0.0f, 1.0f);
			right = Ogre::Math::Clamp(right, left, 1.0f);
			bottom = Ogre::Math::Clamp(bottom, top, 1.0f);
			nrect["left"] = left;
			nrect["top"] = top;
			nrect["right"] = right;
			nrect["bottom"] = bottom;
			colour.r = ccolor[0];
			colour.g = ccolor[1];
			colour.b = ccolor[2];
			colour.a = ccolor[3];
			to_json(nrect["color"], colour);
			rects[key] = nrect;
		}
	}
	if (rects.size() > 0)
		ImGui::Separator();
	{
		static char colorName[64] = { 0 };
		static float left = 0.0f, top = 0.0f, right = 0.1f,
			     bottom = 0.1f;
		ImGui::InputText("New color rect", colorName,
				 sizeof(colorName));
		ImGui::InputFloat("left##New_Color_Rect", &left);
		ImGui::InputFloat("top##New_Color_Rect", &top);
		ImGui::InputFloat("right##New_Color_Rect", &right);
		ImGui::InputFloat("bottom##New_Color_Rect", &bottom);
		left = Ogre::Math::Clamp(left, 0.0f, 1.0f);
		top = Ogre::Math::Clamp(top, 0.0f, 1.0f);
		right = Ogre::Math::Clamp(right, left, 1.0f);
		bottom = Ogre::Math::Clamp(bottom, top, 1.0f);
		static float ccolor[4] = { 0, 0, 0, 1 };
		ImGui::ColorPicker4("Color##New_Color_Rect", ccolor);
		ImGui::Text("%f %f %f %f", ccolor[0], ccolor[1], ccolor[2],
			    ccolor[3]);
		bool valid = true;
		if (right - left <= 0.0f || bottom - top <= 0.0f)
			valid = false;
		if (strlen(colorName) == 0)
			valid = false;

		if (ImGui::SmallButton("Create##New_Color_Rect")) {
			Ogre::ColourValue newColour;
			newColour.r = ccolor[0];
			newColour.g = ccolor[1];
			newColour.b = ccolor[2];
			newColour.a = ccolor[3];
			Ogre::String sColorName(colorName);
			if (valid) {
				nlohmann::json nrect;
				nrect["left"] = left;
				nrect["top"] = top;
				nrect["right"] = right;
				nrect["bottom"] = bottom;
				to_json(nrect["color"], newColour);
				rects[sColorName] = nrect;
				changed = true;
			}
			ccolor[0] = 0;
			ccolor[1] = 0;
			ccolor[2] = 0;
			ccolor[3] = 1;
			strcpy(colorName, "");
		}
	}
	return changed;
}
void runAllScriptsForTown(flecs::entity e)
{
	ZoneScoped;
	Ogre::String prop = StaticGeometryModule::getItemProperties(e);
	nlohmann::json j = nlohmann::json::parse(prop);
	auto &districts = j["districts"];
	for (auto &district : districts) {
		if (district.find("lots") == district.end())
			continue;
		auto &lots = district["lots"];
		for (auto &lot : lots) {
			if (lot.find("cellScript") == lot.end())
				continue;
			CellsScript script(
				lot["cellScript"].get<Ogre::String>(), lot);
			script.run();
		}
		district["lots"] = lots;
	}
	j["districts"] = districts;
	StaticGeometryModule::setItemProperties(e, j.dump());
}
void runSingleLotScript(flecs::entity e, nlohmann::json &lot)
{
	if (lot.find("cellScript") == lot.end())
		return;
	CellsScript script(lot["cellScript"].get<Ogre::String>(), lot);
	script.run();
}
struct Selector {
	int selection;
	Ogre::String result;
	Ogre::String label;
	std::vector<Ogre::String> options;
	Selector(const Ogre::String &label,
		 const std::vector<Ogre::String> &options)
		: label(label)
		, options(options)
		, selection(-1)
	{
	}
	bool select()
	{
		bool changed = false;
		if (selection < 0)
			selection = 0;
		if (selection >= options.size())
			selection = (options.size() > 0) ? options.size() - 1 :
							   0;
		if (options.size() == 0) {
			ImGui::Text("None: %s", label.c_str());
			return false;
		}
		if (ImGui::BeginCombo(label.c_str(),
				      options[selection].c_str())) {
			int i;
			for (i = 0; i < options.size(); i++) {
				bool selected = selection == i;
				if (ImGui::Selectable(options[i].c_str(),
						      selected)) {
					selection = i;
					changed = true;
				}
			}
			ImGui::EndCombo();
		}
		if (changed || result.empty())
			result = options[selection];
		return changed;
	}
	void set_default(const Ogre::String &def)
	{
		int index = -1;
		auto pos = std::find(options.begin(), options.end(), def);
		if (pos == options.end())
			index = -1;
		else {
			index = std::distance(options.begin(), pos);
			selection = index;
			result = options[index];
		}
	}
};
bool editNPCs(nlohmann::json &npcs)
{
	struct slotEdit {
		const Ogre::String &label;
		const Ogre::String &slotBase;
		std::vector<Ogre::String> *options_m;
		std::vector<Ogre::String> *options_f;
		const Ogre::String &slot;
	};
	static std::vector<Ogre::String> faces_a_m, hairs_a_m, tops_a_m,
		bottoms_a_m, feet_a_m;
	static std::vector<Ogre::String> faces_a_f, hairs_a_f, tops_a_f,
		bottoms_a_f, feet_a_f;
	ZoneScoped;
	bool changed = false;
	ImGui::Text("NPC");
	int id = 0;
	struct slotEdit pslots_a[] = {
		{ "Face", "face", &faces_a_m, &faces_a_f, "slot_face" },
		{ "Hair", "hair", &hairs_a_m, &hairs_a_f, "slot_hair" },
		{ "Top", "top", &tops_a_m, &tops_a_f, "slot_top" },
		{ "Bottom", "bottom", &bottoms_a_m, &bottoms_a_f,
		  "slot_bottom" },
		{ "Feet", "feet", &feet_a_m, &feet_a_f, "slot_feet" },
	};
	for (auto g : pslots_a) {
		g.options_m->clear();
		g.options_f->clear();
		ECS::get_mut<CharacterModule>().getSlotMeshes(
			"adult", "male", g.slotBase, *g.options_m);
		ECS::get_mut<CharacterModule>().getSlotMeshes(
			"adult", "female", g.slotBase, *g.options_f);
	}
	for (auto &npc : npcs) {
		Ogre::String meid = Ogre::StringConverter::toString(id);
		static char firstName[64] = { 0 };
		static char lastName[64] = { 0 };
		static char nickName[64] = { 0 };
		static char tags[256] = { 0 };
		if (npc.find("firstName") == npc.end())
			npc["firstName"] = "";
		if (npc.find("lastName") == npc.end())
			npc["lastName"] = "";
		if (npc.find("nickName") == npc.end())
			npc["nickName"] = "";
		if (npc.find("tags") == npc.end())
			npc["tags"] = "";
		if (npc.find("sex") == npc.end())
			npc["sex"] = 1;
		if (npc["sex"].get<int>() == 0) {
			for (const auto &m : pslots_a) {
				if (npc.find(m.slot) == npc.end()) {
					npc[m.slot] = m.options_m->at(0);
					changed = true;
				}
			}
		} else if (npc["sex"].get<int>() == 1) {
			for (const auto &m : pslots_a) {
				if (npc.find(m.slot) == npc.end()) {
					npc[m.slot] = m.options_m->at(0);
					changed = true;
				}
			}
		}

		strncpy(firstName, npc["firstName"].get<Ogre::String>().c_str(),
			sizeof(firstName));
		strncpy(lastName, npc["lastName"].get<Ogre::String>().c_str(),
			sizeof(lastName));
		strncpy(nickName, npc["nickName"].get<Ogre::String>().c_str(),
			sizeof(lastName));
		strncpy(tags, npc["tags"].get<Ogre::String>().c_str(),
			sizeof(tags));

		ImGui::InputText(("Last name##" + meid).c_str(), lastName,
				 sizeof(lastName));
		if (ImGui::IsItemDeactivatedAfterEdit()) {
			npc["lastName"] = Ogre::String(lastName);
			changed = true;
		}
		ImGui::InputText(("First name##" + meid).c_str(), firstName,
				 sizeof(firstName));
		if (ImGui::IsItemDeactivatedAfterEdit()) {
			npc["firstName"] = Ogre::String(firstName);
			changed = true;
		}
		ImGui::InputText(("Nickname##" + meid).c_str(), nickName,
				 sizeof(nickName));
		if (ImGui::IsItemDeactivatedAfterEdit()) {
			npc["nickName"] = Ogre::String(nickName);
			changed = true;
		}
		Selector race(("Race##" + meid).c_str(), { "human" });
		Selector age(("Age##" + meid).c_str(), { "adult" });
		Selector sex(("Sex##" + meid).c_str(), { "male", "female" });
		if (npc.find("race") != npc.end())
			race.set_default(npc["race"].get<Ogre::String>());
		if (npc.find("age") != npc.end())
			age.set_default(npc["age"].get<Ogre::String>());
		if (npc.find("sex") != npc.end())
			sex.selection = npc["sex"].get<int>();
		if (race.select()) {
			npc["race"] = race.result;
			changed = true;
		}
		if (age.select()) {
			npc["age"] = age.result;
			changed = true;
		}
		/* sex is integer */
		if (sex.select()) {
			npc["sex"] = sex.selection;
			changed = true;
		}
		if (sex.selection == 0) {
			for (const auto &es : pslots_a) {
				Selector sel((es.label + "##" + meid).c_str(),
					     *es.options_m);
				sel.set_default(
					npc[es.slot].get<Ogre::String>());
				if (sel.select()) {
					npc[es.slot] = sel.result;
					changed = true;
				}
			}
		} else if (sex.selection == 1) {
			for (const auto &es : pslots_a) {
				Selector sel((es.label + "##" + meid).c_str(),
					     *es.options_f);
				sel.set_default(
					npc[es.slot].get<Ogre::String>());
				if (sel.select()) {
					npc[es.slot] = sel.result;
					changed = true;
				}
			}
		}

		ImGui::InputText(("Tags##" + meid).c_str(), tags, sizeof(tags));
		if (ImGui::IsItemDeactivatedAfterEdit()) {
			npc["tags"] = Ogre::String(tags);
			changed = true;
		}

		if (ImGui::SmallButton(("Spawn##" + meid).c_str())) {
			Ogre::Vector3 npcPosition;
			Ogre::Quaternion npcOrientation;
			from_json(npc["position"], npcPosition);
			from_json(npc["orientation"], npcOrientation);

			// FIXME: create TownCharacterManager and register NPCs through there
			ECS::get_mut<CharacterManagerModule>()
				.createCharacterData(
					npc["slot_face"].get<Ogre::String>(),
					npc["slot_hair"].get<Ogre::String>(),
					npc["slot_top"].get<Ogre::String>(),
					npc["slot_bottom"].get<Ogre::String>(),
					npc["slot_feet"].get<Ogre::String>(),
					npcPosition, npcOrientation);
		}
		if (ImGui::SmallButton(
			    ("Delete##" + Ogre::StringConverter::toString(id))
				    .c_str())) {
			npcs.erase(id);
			changed = true;
			break;
		}
		id++;
	}
	{
		ImGui::Text("New NPC");
		static char lastName[64] = { 0 };
		ImGui::InputText("Last name", lastName, sizeof(lastName));
		static char firstName[64] = { 0 };
		ImGui::InputText("First name", firstName, sizeof(firstName));
		static char tags[256] = { 0 };
		ImGui::InputText("Tags", tags, sizeof(tags));
		static Selector race("Race##new_npc", { "human" });
		static Selector age("Age##new_npc", { "adult" });
		static Selector sex("Sex##new_npc", { "male", "female" });
		changed = changed || race.select();
		changed = changed || age.select();
		changed = changed || sex.select();
		static Selector sel_face("Face##new_npc", {});
		static Selector sel_hair("Hair##new_npc", {});
		static Selector sel_top("Top##new_npc", {});
		static Selector sel_bottom("Bottom##new_npc", {});
		static Selector sel_feet("Feet##new_npc", {});
		if (changed || sel_face.options.size() == 0) {
			if (sex.selection == 0) {
				sel_face = Selector("Face##new_npc", faces_a_m);
				sel_hair = Selector("Hair##new_npc", hairs_a_m);
				sel_top = Selector("Top##new_npc", tops_a_m);
				sel_bottom = Selector("Bottom##new_npc",
						      bottoms_a_m);
				sel_feet = Selector("Feet##new_npc", feet_a_m);
			} else if (sex.selection == 1) {
				sel_face = Selector("Face##new_npc", faces_a_f);
				sel_hair = Selector("Hair##new_npc", hairs_a_f);
				sel_top = Selector("Top##new_npc", tops_a_f);
				sel_bottom = Selector("Bottom##new_npc",
						      bottoms_a_f);
				sel_feet = Selector("Feet##new_npc", feet_a_f);
			}
		}
		changed = changed || sel_face.select();
		changed = changed || sel_hair.select();
		changed = changed || sel_top.select();
		changed = changed || sel_bottom.select();
		changed = changed || sel_feet.select();
		if (ImGui::SmallButton("Add NPC")) {
			nlohmann::json npc;
			npc["lastName"] = Ogre::String(lastName);
			npc["firstName"] = Ogre::String(firstName);
			npc["tags"] = Ogre::String(tags);
			Ogre::Vector3 npcPosition =
				ECS::get<EditorGizmo>()
					.sceneNode->_getDerivedPosition();
			Ogre::Quaternion npcOrientation =
				ECS::get<EditorGizmo>()
					.sceneNode->_getDerivedOrientation();
			to_json(npc["position"], npcPosition);
			to_json(npc["orientation"], npcOrientation);
			npc["sex"] = sex.selection;
			npc["slot_hair"] = sel_hair.result;
			npc["slot_face"] = sel_face.result;
			npc["slot_top"] = sel_top.result;
			npc["slot_bottom"] = sel_bottom.result;
			npc["slot_feet"] = sel_feet.result;
			npc["health"] = 100;
			npc["stamina"] = 100;
			npcs.push_back(npc);
			changed = true;
		}
	}
	ImGui::Separator();
	return changed;
}
void createTownPopup(const std::pair<flecs::entity, Ogre::String> item)
{
	ZoneScoped;
	Ogre::String prop = StaticGeometryModule::getItemProperties(item.first);
	Ogre::Vector3 townPosition;
	Ogre::Quaternion townRotation;
	StaticGeometryModule::getItemPositionAndRotation(
		item.first, townPosition, townRotation);
	nlohmann::json j = nlohmann::json::parse(prop);
	bool changed = false;
	int count = 0;
	ImGui::Text("Town");
	char townNameBuf[64];
	strcpy(townNameBuf, "");
	if (j.find("name") != j.end())
		strncpy(townNameBuf, j["name"].get<Ogre::String>().c_str(),
			sizeof(townNameBuf));
	ImGui::InputText("Town Name", townNameBuf, sizeof(townNameBuf));
	if (ImGui::IsItemDeactivatedAfterEdit()) {
		j["name"] = Ogre::String(townNameBuf);
		changed = true;
	}
	if (ImGui::SmallButton("Run all town scripts")) {
		runAllScriptsForTown(item.first);
		StaticGeometryModule::saveItems();
		changed = true;
	}
	nlohmann::json colorRects = nlohmann::json::object();
	nlohmann::json lotTemplates = nlohmann::json::array();
	if (j.find("colorRects") != j.end())
		colorRects = j["colorRects"];
	if (j.find("lotTemplates") != j.end())
		lotTemplates = j["lotTemplates"];
	if (ImGui::CollapsingHeader("Edit Color Rects")) {
		changed = changed || editColorRects(colorRects);
		ImGui::Separator();
	}
	nlohmann::json npcs = nlohmann::json::array();
	if (j.find("npcs") != j.end())
		npcs = j["npcs"];

	changed = changed || editNPCs(npcs);
	nlohmann::json districts = nlohmann::json::array();
	for (auto &district : j["districts"])
		districts.push_back(district);
	count = 0;
	for (auto &district : districts) {
		ImGui::Separator();
		Ogre::String districtLabel =
			"district" + Ogre::StringConverter::toString(count);
		ImGui::Text("%s", districtLabel.c_str());
		bool districtChanged =
			editDistrict(districtLabel, district, lotTemplates);
		district["_dirty"] = districtChanged;
		changed = changed || districtChanged;
		ImGui::Separator();
		if (ImGui::SmallButton(("Delete##" + districtLabel).c_str())) {
			districts.erase(count);
			changed = true;
			break;
		}
		count++;
	}
	ImGui::Separator();
	if (ImGui::SmallButton("Add district")) {
		Ogre::Vector3 cursorPosition =
			ECS::get<EditorGizmo>().sceneNode->_getDerivedPosition();
		Ogre::Quaternion cursorOrientation =
			ECS::get<EditorGizmo>()
				.sceneNode->_getDerivedOrientation();
		Ogre::Vector3 localPosition = cursorPosition - townPosition;
		Ogre::Quaternion localRotation =
			townRotation.Inverse() * cursorOrientation;
		nlohmann::json d;
		d["radius"] = 50.0f;
		d["lots"] = nlohmann::json::array();
		to_json(d["position"], localPosition);
		to_json(d["rotation"], localRotation);
		d["elevation"] = 0.0f;
		d["plazza"] = false;
		districts.push_back(d);
		changed = true;
	}
	ImGui::Separator();
	ImGui::Text("%s", j.dump(4).c_str());
	if (changed) {
		j["npcs"] = npcs;
		j["districts"] = districts;
		j["colorRects"] = colorRects;
		j["lotTemplates"] = lotTemplates;
		StaticGeometryModule::setItemProperties(item.first, j.dump());
		StaticGeometryModule::saveItems();
		Geometry::createTownMaterial(item.first, true);
		StaticGeometryModule::updateItemGeometry(item.first);
	}
}
}
namespace Geometry
{
struct BitSet {
	uint64_t bit;
	float sizeX;
	float sizeY;
	Ogre::Vector3 normal, offset;
	Procedural::TriangleBuffer *tb;
};

struct ProcessCells {
	const float cellHeight = 4.0f;
	const float cornerWidth = 0.2f;
	Procedural::TriangleBuffer floortb;
	Procedural::TriangleBuffer ceilingtb;
	Procedural::TriangleBuffer extwalltb;
	Procedural::TriangleBuffer intwalltb;
	Procedural::TriangleBuffer intwindowstb;
	std::vector<struct BitSet> bits_ext_corners;
	std::vector<struct BitSet> bits_solid;
	std::vector<struct BitSet> bits_doors;
	std::vector<struct BitSet> bits_windows;
	std::vector<struct BitSet> bits_int_corners1;
	std::vector<struct BitSet> bits_int_corners2;
	ProcessCells()
	{
		ZoneScoped;
		bits_ext_corners = {
			{ 4 | 16, // external wall
			  4.0f,
			  0.2f,
			  Ogre::Vector3::NEGATIVE_UNIT_X,
			  { -1.0 - 0.2f, 2.0f, 1.0f + 0.1f },
			  &extwalltb },
			{ 4 | 16, // external wall
			  4.0f,
			  0.2f,
			  Ogre::Vector3::UNIT_Z,
			  { -1.0 - 0.1f, 2.0f, 1.0f + 0.2f },
			  &extwalltb },
			{ 4 | 32, // external wall
			  4.0f,
			  0.2f,
			  Ogre::Vector3::NEGATIVE_UNIT_X,
			  { -1.0 - 0.2f, 2.0f, -1.0f - 0.1f },
			  &extwalltb },
			{ 4 | 32, // external wall
			  4.0f,
			  0.2f,
			  Ogre::Vector3::NEGATIVE_UNIT_Z,
			  { -1.0 - 0.1f, 2.0f, -1.0f - 0.2f },
			  &extwalltb },
			{ 8 | 16, // external wall
			  4.0f,
			  0.2f,
			  Ogre::Vector3::UNIT_X,
			  { 1.0f + 0.2f, 2.0f, 1.0f + 0.1f },
			  &extwalltb },
			{ 8 | 16, // external wall
			  4.0f,
			  0.2f,
			  Ogre::Vector3::UNIT_Z,
			  { 1.0f + 0.1f, 2.0f, 1.0f + 0.2f },
			  &extwalltb },
			{ 8 | 32, // external wall
			  4.0f,
			  0.2f,
			  Ogre::Vector3::UNIT_X,
			  { 1.0f + 0.2f, 2.0f, -1.0f - 0.1f },
			  &extwalltb },
			{ 8 | 32, // external wall
			  4.0f,
			  0.2f,
			  Ogre::Vector3::NEGATIVE_UNIT_Z,
			  { 1.0f + 0.1f, 2.0f, -1.0f - 0.2f },
			  &extwalltb },
		};
		float solidCellHeight = 4.0f;
		float solidExtOffset = 0.0f;
		float solidExtHeight = solidCellHeight - solidExtOffset;
		float solidIntOffset = 0.1f;
		float solidIntHeight = solidCellHeight - 0.3f - solidExtOffset;

		bits_solid = {
			{ 1ULL << 0, // floor
			  2.0f,
			  2.0f,
			  Ogre::Vector3::UNIT_Y,
			  { 0, 0.1f, 0 },
			  &floortb },
			{ 1ULL << 1, // ceiling
			  2.0f,
			  2.0f,
			  Ogre::Vector3::NEGATIVE_UNIT_Y,
			  { 0, 3.7f, 0 },
			  &ceilingtb },
			{ 1ULL << 2, // external wall
			  solidExtHeight,
			  2.0f,
			  Ogre::Vector3::NEGATIVE_UNIT_X,
			  { -1.0 - 0.2f, solidExtHeight / 2.0f + solidExtOffset,
			    0 },
			  &extwalltb },
			{ 1ULL << 3, // external wall
			  solidExtHeight,
			  2.0f,
			  Ogre::Vector3::UNIT_X,
			  { 1.0 + 0.2f, solidExtHeight / 2.0f + solidExtOffset,
			    0 },
			  &extwalltb },
			{ 1ULL << 4, // external wall
			  solidExtHeight,
			  2.0f,
			  Ogre::Vector3::UNIT_Z,
			  { 0, solidExtHeight / 2.0f + solidExtOffset,
			    1.0f + 0.2f },
			  &extwalltb },
			{ 1ULL << 5, // external wall
			  solidExtHeight,
			  2.0f,
			  Ogre::Vector3::NEGATIVE_UNIT_Z,
			  { 0, solidExtHeight / 2.0f + solidExtOffset,
			    -1.0f - 0.2f },
			  &extwalltb },
			{ 1ULL << 14, // internal wall
			  solidIntHeight,
			  1.8f,
			  Ogre::Vector3::UNIT_X,
			  { -1.0 + 0.1f, solidIntHeight / 2.0f + solidIntOffset,
			    0 },
			  &intwalltb },
			{ 1ULL << 15, // internal wall
			  solidIntHeight,
			  1.8f,
			  Ogre::Vector3::NEGATIVE_UNIT_X,
			  { 1.0 - 0.1f, solidIntHeight / 2.0f + solidIntOffset,
			    0 },
			  &intwalltb },
			{ 1ULL << 16, // internal wall
			  solidIntHeight,
			  1.8f,
			  Ogre::Vector3::NEGATIVE_UNIT_Z,
			  { 0, solidIntHeight / 2.0f + solidIntOffset,
			    1.0f - 0.1f },
			  &intwalltb },
			{ 1ULL << 17, // internal wall
			  solidIntHeight,
			  1.8f,
			  Ogre::Vector3::UNIT_Z,
			  { 0, solidIntHeight / 2.0f + solidIntOffset,
			    -1.0f + 0.1f },
			  &intwalltb },
		};
		bits_int_corners1 = {
			{ 1ULL << 14, // internal wall corner
			  solidIntHeight,
			  0.1f,
			  Ogre::Vector3::UNIT_X,
			  { -1.0f + 0.1f,
			    solidIntHeight / 2.0f + solidIntOffset,
			    (1.8f + 0.1f) / 2.0f },
			  &intwalltb },
			{ 1ULL << 15, // internal wall
			  solidIntHeight,
			  0.1f,
			  Ogre::Vector3::NEGATIVE_UNIT_X,
			  { 1.0 - 0.1f, solidIntHeight / 2.0f + solidIntOffset,
			    (1.8f + 0.1f) / 2.0f },
			  &intwalltb },
			{ 1ULL << 16, // internal wall
			  solidIntHeight,
			  0.1f,
			  Ogre::Vector3::NEGATIVE_UNIT_Z,
			  { (1.8f + 0.1f) / 2.0f,
			    solidIntHeight / 2.0f + solidIntOffset,
			    1.0f - 0.1f },
			  &intwalltb },
			{ 1ULL << 17, // internal wall
			  solidIntHeight,
			  0.1f,
			  Ogre::Vector3::UNIT_Z,
			  { (1.8f + 0.1f) / 2.0f,
			    solidIntHeight / 2.0f + solidIntOffset,
			    -1.0f + 0.1f },
			  &intwalltb },
		};
		bits_int_corners2 = {
			{ 1ULL << 14, // internal wall corner
			  solidIntHeight,
			  0.1f,
			  Ogre::Vector3::UNIT_X,
			  { -1.0f + 0.1f,
			    solidIntHeight / 2.0f + solidIntOffset,
			    (-1.8f - 0.1f) / 2.0f },
			  &intwalltb },
			{ 1ULL << 15, // internal wall
			  solidIntHeight,
			  0.1f,
			  Ogre::Vector3::NEGATIVE_UNIT_X,
			  { 1.0 - 0.1f, solidIntHeight / 2.0f + solidIntOffset,
			    -(1.8f + 0.1f) / 2.0f },
			  &intwalltb },
			{ 1ULL << 16, // internal wall
			  solidIntHeight,
			  0.1f,
			  Ogre::Vector3::NEGATIVE_UNIT_Z,
			  { -(1.8f + 0.1f) / 2.0f,
			    solidIntHeight / 2.0f + solidIntOffset,
			    1.0f - 0.1f },
			  &intwalltb },
			{ 1ULL << 17, // internal wall
			  solidIntHeight,
			  0.1f,
			  Ogre::Vector3::UNIT_Z,
			  { -(1.8f + 0.1f) / 2.0f,
			    solidIntHeight / 2.0f + solidIntOffset,
			    -1.0f + 0.1f },
			  &intwalltb },
		};
		struct BitSet doorbits[] = {
			{ 1ULL << 6, // external door
			  3.0f,
			  0.4f,
			  Ogre::Vector3::NEGATIVE_UNIT_X,
			  { -1.0 - 0.2f, 1.5f, 0 },
			  &extwalltb },
			{ 1ULL << 7, // external door
			  3.0f,
			  0.4f,
			  Ogre::Vector3::UNIT_X,
			  { 1.0 + 0.2f, 1.5f, 0 },
			  &extwalltb },
			{ 1ULL << 8, // external door
			  3.0f,
			  0.4f,
			  Ogre::Vector3::UNIT_Z,
			  { 0, 1.5f, 1.0f + 0.2f },
			  &extwalltb },
			{ 1ULL << 9, // external door
			  3.0f,
			  0.4f,
			  Ogre::Vector3::NEGATIVE_UNIT_Z,
			  { 0, 1.5f, -1.0f - 0.2f },
			  &extwalltb },
			{ 1ULL << 18, // internal door
			  3.0f,
			  0.4f,
			  Ogre::Vector3::UNIT_X,
			  { -1.0 + 0.1f, 1.5f, 0 },
			  &intwalltb },
			{ 1ULL << 19, // internal door
			  3.0f,
			  0.4f,
			  Ogre::Vector3::NEGATIVE_UNIT_X,
			  { 1.0 - 0.1f, 1.5f, 0 },
			  &intwalltb },
			{ 1ULL << 20, // internal door
			  3.0f,
			  0.4f,
			  Ogre::Vector3::NEGATIVE_UNIT_Z,
			  { 0, 1.5f, 1.0f - 0.1f },
			  &intwalltb },
			{ 1ULL << 21, // internal door
			  3.0f,
			  0.4f,
			  Ogre::Vector3::UNIT_Z,
			  { 0, 1.5f, -1.0f + 0.1f },
			  &intwalltb },
		};
		// const float doorm = 0.8f;
		const float doorWidth = 1.4f;
		const float doorHeightBase = 3.0f;
		float doorHeight = doorHeightBase - solidExtOffset;
		float baseOffsetY = solidExtOffset;
		float doorTopHeight = solidExtHeight - doorHeight;
		for (const struct BitSet &bits : doorbits) {
			float sideWidth = (2.0f - doorWidth) / 2.0f;
			float moffset = (2.0f - sideWidth) / 2.0f;
			if (bits.tb == &intwalltb) {
				sideWidth = (1.8f - doorWidth) / 2.0f;
				moffset = (1.8f - sideWidth) / 2.0f;
				baseOffsetY = solidIntOffset;
				doorHeight = doorHeightBase - solidIntOffset;
				doorTopHeight = solidIntHeight - doorHeight;
			}
			if (bits.offset.x == 0.0f) { //z+ or z-
				struct BitSet pbits = bits;
				pbits.sizeY = sideWidth;
				pbits.offset.x = -moffset;
				pbits.offset.y =
					baseOffsetY + doorHeight / 2.0f;
				bits_doors.push_back(pbits);
				pbits.sizeY = sideWidth;
				pbits.offset.x = moffset;
				pbits.offset.y =
					baseOffsetY + doorHeight / 2.0f;
				bits_doors.push_back(pbits);
				pbits.sizeY = sideWidth;
				pbits.offset.x = -moffset;
				pbits.sizeX = doorTopHeight;
				pbits.offset.y = baseOffsetY + doorHeight +
						 doorTopHeight / 2.0f;
				bits_doors.push_back(pbits);
				pbits.sizeY = sideWidth;
				pbits.offset.x = moffset;
				pbits.sizeX = doorTopHeight;
				pbits.offset.y = baseOffsetY + doorHeight +
						 doorTopHeight / 2.0f;
				bits_doors.push_back(pbits);
				pbits.offset.x = 0;
				pbits.sizeX = doorTopHeight;
				pbits.sizeY = doorWidth;
				pbits.offset.y = baseOffsetY + doorHeight +
						 doorTopHeight / 2.0f;
				bits_doors.push_back(pbits);
			} else if (bits.offset.z == 0.0f) {
				struct BitSet pbits = bits;
				pbits.sizeY = sideWidth;
				pbits.offset.z = -moffset;
				pbits.offset.y =
					baseOffsetY + doorHeight / 2.0f;
				bits_doors.push_back(pbits);
				pbits.sizeY = sideWidth;
				pbits.offset.z = moffset;
				pbits.offset.y =
					baseOffsetY + doorHeight / 2.0f;
				bits_doors.push_back(pbits);
				pbits.sizeY = sideWidth;
				pbits.offset.z = -moffset;
				pbits.sizeX = doorTopHeight;
				pbits.offset.y = baseOffsetY + doorHeight +
						 doorTopHeight / 2.0f;
				bits_doors.push_back(pbits);
				pbits.sizeY = sideWidth;
				pbits.offset.z = moffset;
				pbits.sizeX = doorTopHeight;
				pbits.offset.y = baseOffsetY + doorHeight +
						 doorTopHeight / 2.0f;
				bits_doors.push_back(pbits);
				pbits.offset.z = 0;
				pbits.sizeX = doorTopHeight;
				pbits.sizeY = doorWidth;
				pbits.offset.y = baseOffsetY + doorHeight +
						 doorTopHeight / 2.0f;
				bits_doors.push_back(pbits);
			}
		}
		struct BitSet windowbits[] = {
			{ 1ULL << 10, // external window
			  2.0f,
			  0.2f,
			  Ogre::Vector3::NEGATIVE_UNIT_X,
			  { -1.0 - 0.2f, 1.5f, 0 },
			  &extwalltb },
			{ 1ULL << 11, // external window
			  2.0f,
			  0.2f,
			  Ogre::Vector3::UNIT_X,
			  { 1.0 + 0.2f, 1.5f, 0 },
			  &extwalltb },
			{ 1ULL << 12, // external window
			  2.0f,
			  0.2f,
			  Ogre::Vector3::UNIT_Z,
			  { 0, 1.5f, 1.0f + 0.2f },
			  &extwalltb },
			{ 1ULL << 13, // external window
			  2.0f,
			  0.2f,
			  Ogre::Vector3::NEGATIVE_UNIT_Z,
			  { 0, 1.5f, -1.0f - 0.2f },
			  &extwalltb },
			{ 1ULL << 22, // internal window
			  2.0f,
			  0.2f,
			  Ogre::Vector3::UNIT_X,
			  { -1.0 + 0.1f, 1.5f, 0 },
			  &intwindowstb },
			{ 1ULL << 23, // internal window
			  2.0f,
			  0.2f,
			  Ogre::Vector3::NEGATIVE_UNIT_X,
			  { 1.0 - 0.1f, 1.5f, 0 },
			  &intwindowstb },
			{ 1ULL << 24, // internal window
			  2.0f,
			  0.2f,
			  Ogre::Vector3::NEGATIVE_UNIT_Z,
			  { 0, 1.5f, 1.0f - 0.1f },
			  &intwindowstb },
			{ 1ULL << 25, // internal window
			  2.0f,
			  0.2f,
			  Ogre::Vector3::UNIT_Z,
			  { 0, 1.5f, -1.0f + 0.1f },
			  &intwindowstb },
		};
		const float windowWidth = 1.6f;
		for (const struct BitSet &bits : windowbits) {
			float windowBottomOffset = 0.8f;
			float sideWidth = (2.0f - windowWidth) / 2.0f;
			float moffset = (2.0f - sideWidth) / 2.0f;
			float midSize = 2.0f;
			float bottomSize = windowBottomOffset - solidExtOffset;
			float topOffset = 3.0f;
			float topSize = solidExtHeight - midSize - bottomSize;
			float offsetY = solidExtOffset;
			if (bits.tb == &intwindowstb) {
				bottomSize =
					windowBottomOffset - solidIntOffset;
				topSize = solidIntHeight - midSize - bottomSize;
				sideWidth = (1.8f - windowWidth) / 2.0f;
				moffset = (1.8f - sideWidth) / 2.0f;
				offsetY = solidIntOffset;
			}
			if (bits.offset.x == 0.0f) {
				struct BitSet pbits = bits;
				pbits.sizeY = sideWidth;
				pbits.sizeX = bottomSize;
				pbits.offset.x = -moffset;
				pbits.offset.y = bottomSize / 2.0f + offsetY;
				bits_windows.push_back(pbits);
				pbits.sizeY = sideWidth;
				pbits.sizeX = bottomSize;
				pbits.offset.x = moffset;
				pbits.offset.y = bottomSize / 2.0f + offsetY;
				bits_windows.push_back(pbits);
				pbits.sizeY = sideWidth;
				pbits.sizeX = midSize;
				pbits.offset.x = -moffset;
				pbits.offset.y =
					bottomSize + midSize / 2.0f + offsetY;
				bits_windows.push_back(pbits);
				pbits.sizeY = sideWidth;
				pbits.sizeX = midSize;
				pbits.offset.x = moffset;
				pbits.offset.y =
					bottomSize + midSize / 2.0f + offsetY;
				bits_windows.push_back(pbits);
				pbits.sizeY = sideWidth;
				pbits.sizeX = topSize;
				pbits.offset.x = -moffset;
				pbits.offset.y = bottomSize + midSize +
						 topSize / 2.0f + offsetY;
				bits_windows.push_back(pbits);
				pbits.sizeY = sideWidth;
				pbits.sizeX = topSize;
				pbits.offset.x = moffset;
				pbits.offset.y = bottomSize + midSize +
						 topSize / 2.0f + offsetY;
				bits_windows.push_back(pbits);

				pbits.sizeY = windowWidth;
				pbits.sizeX = bottomSize;
				pbits.offset.x = 0;
				pbits.offset.y = bottomSize / 2.0f + offsetY;
				bits_windows.push_back(pbits);

				pbits.sizeY = windowWidth;
				pbits.sizeX = topSize;
				pbits.offset.x = 0;
				pbits.offset.y = bottomSize + midSize +
						 topSize / 2.0f + offsetY;
				bits_windows.push_back(pbits);
			} else if (bits.offset.z == 0.0f) {
				struct BitSet pbits = bits;
				pbits.sizeY = sideWidth;
				pbits.sizeX = bottomSize;
				pbits.offset.z = -moffset;
				pbits.offset.y = bottomSize / 2.0f + offsetY;
				bits_windows.push_back(pbits);
				pbits.sizeY = sideWidth;
				pbits.sizeX = bottomSize;
				pbits.offset.z = moffset;
				pbits.offset.y = bottomSize / 2.0f + offsetY;
				bits_windows.push_back(pbits);
				pbits.sizeY = sideWidth;
				pbits.sizeX = midSize;
				pbits.offset.z = -moffset;
				pbits.offset.y =
					bottomSize + midSize / 2.0f + offsetY;
				bits_windows.push_back(pbits);
				pbits.sizeY = sideWidth;
				pbits.sizeX = midSize;
				pbits.offset.z = moffset;
				pbits.offset.y =
					bottomSize + midSize / 2.0f + offsetY;
				bits_windows.push_back(pbits);
				pbits.sizeY = sideWidth;
				pbits.sizeX = topSize;
				pbits.offset.z = -moffset;
				pbits.offset.y = bottomSize + midSize +
						 topSize / 2.0f + offsetY;
				bits_windows.push_back(pbits);
				pbits.sizeY = sideWidth;
				pbits.sizeX = topSize;
				pbits.offset.z = moffset;
				pbits.offset.y = bottomSize + midSize +
						 topSize / 2.0f + offsetY;
				bits_windows.push_back(pbits);

				pbits.sizeY = windowWidth;
				pbits.sizeX = bottomSize;
				pbits.offset.z = 0;
				pbits.offset.y = bottomSize / 2.0f + offsetY;
				bits_windows.push_back(pbits);

				pbits.sizeY = windowWidth;
				pbits.sizeX = topSize;
				pbits.offset.z = 0;
				pbits.offset.y = bottomSize + midSize +
						 topSize / 2.0f + offsetY;
				bits_windows.push_back(pbits);
			}
		}
	}
	void process(flecs::entity e, const Ogre::Vector3 &cellOffset,
		     uint64_t flags, Procedural::TriangleBuffer &cellTb,
		     Procedural::TriangleBuffer &intwinTb,
		     Procedural::TriangleBuffer &exteriorTb)
	{
		auto gen = [cellOffset](float sizeX, float sizeY,
					const Ogre::Vector3 &normal,
					const Ogre::Vector3 offset,
					Procedural::TriangleBuffer &tb) {
			int nsegY = 1;
			if (sizeY < 1.0f)
				nsegY = 1;
			Procedural::PlaneGenerator()
				.setNormal(normal)
				.setSizeX(sizeX)
				.setSizeY(sizeY)
				.setNumSegX(1)
				.setNumSegY(nsegY)
				.setEnableNormals(true)
				.setPosition(cellOffset + offset)
				.addToTriangleBuffer(tb);
		};
		uint64_t corners_flags = 0;
		corners_flags |= ((flags >> 0) & 0x3c);
		corners_flags |= ((flags >> 4) & 0x3c);
		corners_flags |= ((flags >> 8) & 0x3c);
		// corners_flags |= ((flags >> 12) & 0x3c);
		uint64_t corners_flags2 = 0;
		corners_flags2 |= ((flags >> 0) & 0x3c000);
		corners_flags2 |= ((flags >> 4) & 0x3c000);
		corners_flags2 |= ((flags >> 8) & 0x3c000);
		for (struct BitSet &bit : bits_ext_corners) {
			if ((corners_flags & bit.bit) == bit.bit)
				gen(bit.sizeX, bit.sizeY, bit.normal,
				    bit.offset, *bit.tb);
		}
		for (struct BitSet &bit : bits_solid) {
			if ((flags & bit.bit) == bit.bit)
				gen(bit.sizeX, bit.sizeY, bit.normal,
				    bit.offset, *bit.tb);
		}
		for (struct BitSet &bit : bits_doors) {
			if ((flags & bit.bit) == bit.bit)
				gen(bit.sizeX, bit.sizeY, bit.normal,
				    bit.offset, *bit.tb);
		}
		for (struct BitSet &bit : bits_windows) {
			if ((flags & bit.bit) == bit.bit)
				gen(bit.sizeX, bit.sizeY, bit.normal,
				    bit.offset, *bit.tb);
		}
		for (struct BitSet &bit : bits_int_corners1) {
			if ((corners_flags2 & bit.bit) == bit.bit) {
				if (bit.bit == (1ULL << 14) &&
				    (corners_flags2 & (1ULL << 16)))
					continue;
				if (bit.bit == (1ULL << 15) &&
				    (corners_flags2 & (1ULL << 16)))
					continue;
				if (bit.bit == (1ULL << 16) &&
				    (corners_flags2 & (1ULL << 15)))
					continue;
				if (bit.bit == (1ULL << 17) &&
				    (corners_flags2 & (1ULL << 15)))
					continue;
				gen(bit.sizeX, bit.sizeY, bit.normal,
				    bit.offset, *bit.tb);
			}
		}
		for (struct BitSet &bit : bits_int_corners2) {
			if ((corners_flags2 & bit.bit) == bit.bit) {
				if (bit.bit == (1ULL << 14) &&
				    (corners_flags2 & (1ULL << 17)))
					continue;
				if (bit.bit == (1ULL << 15) &&
				    (corners_flags2 & (1ULL << 17)))
					continue;
				if (bit.bit == (1ULL << 16) &&
				    (corners_flags2 & (1ULL << 14)))
					continue;
				if (bit.bit == (1ULL << 17) &&
				    (corners_flags2 & (1ULL << 14)))
					continue;
				gen(bit.sizeX, bit.sizeY, bit.normal,
				    bit.offset, *bit.tb);
			}
		}
		for (auto &v : floortb.getVertices()) {
			// red floor color
			v.mUV *= 0.08f;
			v.mUV.x += 0.51f;
			v.mUV.x = Ogre::Math::Clamp(v.mUV.x, 0.5f, 0.6f);
			v.mUV.y = Ogre::Math::Clamp(v.mUV.y, 0.0f, 0.1f);
		}
		for (auto &v : ceilingtb.getVertices()) {
			// ceiling color
			v.mUV *= 0.08f;
			v.mUV.x += 0.61f;
			v.mUV.x = Ogre::Math::Clamp(v.mUV.x, 0.6f, 0.7f);
			v.mUV.y = Ogre::Math::Clamp(v.mUV.y, 0.0f, 0.1f);
		}
		for (auto &v : extwalltb.getVertices()) {
			// yellow wall color
			v.mUV *= 0.08f;
			v.mUV.x += 0.71f;
			v.mUV.x = Ogre::Math::Clamp(v.mUV.x, 0.7f, 0.8f);
			v.mUV.y = Ogre::Math::Clamp(v.mUV.y, 0.0f, 0.1f);
		}
		clampUV(e, intwalltb, "internalWalls");
		clampUV(e, intwindowstb, "internalWalls");
		for (auto &tb : std::vector<Procedural::TriangleBuffer *>(
			     { &floortb, &ceilingtb, &intwalltb })) {
			if ((*tb).getVertices().size() > 0)
				cellTb.append(*tb);
		}
		if (intwindowstb.getVertices().size() > 0)
			intwinTb.append(intwindowstb);
		if (extwalltb.getVertices().size() > 0)
			exteriorTb.append(extwalltb);
	}
};

void clampUV(flecs::entity e, Procedural::TriangleBuffer &tb,
	     const Ogre::String &rectKey)
{
	ZoneScoped;
	Ogre::String props = e.get<TerrainItem>().properties;
	nlohmann::json jp = nlohmann::json::parse(props);
	nlohmann::json colorRects = nlohmann::json::object();
	if (jp.find("colorRects") != jp.end())
		colorRects = jp["colorRects"];
	if (colorRects.find(rectKey) == colorRects.end())
		return;
	float left = colorRects[rectKey]["left"].get<float>();
	float right = colorRects[rectKey]["right"].get<float>();
	float top = colorRects[rectKey]["top"].get<float>();
	float bottom = colorRects[rectKey]["bottom"].get<float>();
	float umul = (right - left) * 0.8f;
	float vmul = (bottom - top) * 0.8f;
	for (auto &v : tb.getVertices()) {
		v.mUV.x *= umul;
		v.mUV.y *= vmul;
		v.mUV.x += left + 0.01f;
		v.mUV.y += top + 0.01f;
		v.mUV.x = Ogre::Math::Clamp(v.mUV.x, left, right);
		v.mUV.y = Ogre::Math::Clamp(v.mUV.y, top, bottom);
	}
}
Ogre::MaterialPtr createTownMaterial(flecs::entity e, bool force)
{
	ZoneScoped;
	Ogre::MaterialPtr townMaterial;
	Ogre::String props = e.get<TerrainItem>().properties;
	nlohmann::json jp = nlohmann::json::parse(props);
	nlohmann::json colorRects = nlohmann::json::object();
	if (jp.find("colorRects") != jp.end())
		colorRects = jp["colorRects"];
	Ogre::String materialName = "proceduralMaterialTown" +
				    Ogre::StringConverter::toString(e.id());
	townMaterial =
		Ogre::MaterialManager::getSingleton().getByName(materialName);
	if (!townMaterial || force) {
		Procedural::TextureBuffer colorAtlas(1024);
		Procedural::RectangleTexture drawAtlas(&colorAtlas);
		Ogre::ColourValue normalYellow(0.8f, 0.6f, 0, 1);
		Ogre::ColourValue roadBrown(0.4f, 0.3f, 0.3f, 1);
		Ogre::ColourValue bollardGrey(0.2f, 0.2f, 0.2f, 1);
		Ogre::ColourValue floorRed(0.5f, 0.2f, 0.2f, 1);
		Ogre::ColourValue ceilingWhite(0.8f, 0.8f, 0.85f, 1);
		Ogre::ColourValue wallYellow(0.8f, 0.6f, 0, 1);
		Ogre::ColourValue intwallBlue(0.6f, 0.6f, 0.8f, 1);
		drawAtlas.setRectangle(Ogre::RealRect(0.0f, 0.0f, 0.4f, 1.0f))
			.setColour(normalYellow)
			.process();
		drawAtlas.setRectangle(Ogre::RealRect(0.2f, 0.0f, 0.3f, 1.0f))
			.setColour(bollardGrey)
			.process();
		drawAtlas.setRectangle(Ogre::RealRect(0.4f, 0.0f, 1.0f, 1.0f))
			.setColour(roadBrown)
			.process();
		drawAtlas.setRectangle(Ogre::RealRect(0.5f, 0.0f, 0.6f, 0.1f))
			.setColour(floorRed)
			.process();
		drawAtlas.setRectangle(Ogre::RealRect(0.6f, 0.0f, 0.7f, 0.1f))
			.setColour(ceilingWhite)
			.process();
		drawAtlas.setRectangle(Ogre::RealRect(0.7f, 0.0f, 0.8f, 0.1f))
			.setColour(wallYellow)
			.process();
		drawAtlas.setRectangle(Ogre::RealRect(0.8f, 0.0f, 0.9f, 0.1f))
			.setColour(intwallBlue)
			.process();
		for (auto it = colorRects.begin(); it != colorRects.end();
		     it++) {
			std::cout << it.key() << std::endl;
			nlohmann::json rect = it.value();
			float left = rect["left"].get<float>();
			float top = rect["top"].get<float>();
			float right = rect["right"].get<float>();
			float bottom = rect["bottom"].get<float>();
			Ogre::ColourValue colour;
			from_json(rect["color"], colour);
			drawAtlas
				.setRectangle(Ogre::RealRect(left, top, right,
							     bottom))
				.setColour(colour)
				.process();
		}
		Ogre::String textureName =
			"proceduralTextureTown" +
			Ogre::StringConverter::toString(e.id());
		Ogre::TexturePtr townTexture =
			Ogre::TextureManager::getSingleton().getByName(
				textureName);
		if (townTexture) {
			Ogre::TexturePtr newTownTexture =
				colorAtlas.createTexture(textureName + "_copy");
			newTownTexture->copyToTexture(townTexture);
			Ogre::TextureManager::getSingleton().remove(
				newTownTexture);
		} else
			Ogre::TexturePtr townTexture =
				colorAtlas.createTexture(textureName);

		colorAtlas.saveImage("tmp4.png");
		if (!townMaterial) {
			townMaterial =
				Ogre::MaterialManager::getSingletonPtr()->create(
					"proceduralMaterialTown" +
						Ogre::StringConverter::toString(
							e.id()),
					Ogre::ResourceGroupManager::
						DEFAULT_RESOURCE_GROUP_NAME);
			townMaterial->getTechnique(0)->getPass(0)->setShininess(
				0);
			townMaterial->getTechnique(0)->getPass(0)->setDiffuse(
				Ogre::ColourValue::White);
			townMaterial->getTechnique(0)->getPass(0)->setSpecular(
				Ogre::ColourValue(1.0f, 1.0f, 0.9f));
			townMaterial->getTechnique(0)
				->getPass(0)
				->createTextureUnitState(textureName);
			if (Ogre::RTShader::ShaderGenerator::initialize()) {
				townMaterial->prepare();
				Ogre::RTShader::ShaderGenerator
					*mShaderGenerator =
						Ogre::RTShader::ShaderGenerator::
							getSingletonPtr();
				mShaderGenerator->createShaderBasedTechnique(
					*townMaterial,
					Ogre::MaterialManager::DEFAULT_SCHEME_NAME,
					Ogre::RTShader::ShaderGenerator::
						DEFAULT_SCHEME_NAME);
				Ogre::RTShader::RenderState *pMainRenderState =
					mShaderGenerator->getRenderState(
						Ogre::RTShader::ShaderGenerator::
							DEFAULT_SCHEME_NAME,
						*townMaterial);
			}
		} else {
#if 0
            Ogre::TextureUnitState *state =
                townMaterial->getTechnique(0)
                    ->getPass(0)
                    ->getTextureUnitState(textureName);
            state->
#endif
		}
	}
	return townMaterial;
}
void createTownWindows(flecs::entity e)
{
	ZoneScoped;
	Ogre::MeshPtr frame1mesh =
		Ogre::MeshManager::getSingleton().getByName("window-frame1");
	if (!frame1mesh) {
		Procedural::TriangleBuffer frame1tb;
		Procedural::TriangleBuffer glass1tb;
		Procedural::BoxGenerator()
			.setSizeX(1.6f)
			.setSizeY(2.0f)
			//.setSizeZ(0.34f)
			.setSizeZ(0.05f)
			.setNumSegY(2)
			.setNumSegX(2)
			.setNumSegZ(2)
			.setPosition(Ogre::Vector3(0, 1.0f, 0))
			.setEnableNormals(true)
			.addToTriangleBuffer(glass1tb);
		Procedural::BoxGenerator()
			.setSizeX(0.1f)
			.setSizeY(2.0f)
			.setSizeZ(0.34f)
			.setNumSegY(2)
			.setNumSegX(2)
			.setNumSegZ(2)
			.setPosition(Ogre::Vector3(-0.8f + 0.05f, 1.0f, 0))
			.setEnableNormals(true)
			.addToTriangleBuffer(frame1tb);
		Procedural::BoxGenerator()
			.setSizeX(0.1f)
			.setSizeY(2.0f)
			.setSizeZ(0.34f)
			.setNumSegY(2)
			.setNumSegX(2)
			.setNumSegZ(2)
			.setPosition(Ogre::Vector3(0.8f - 0.05f, 1.0f, 0))
			.setEnableNormals(true)
			.addToTriangleBuffer(frame1tb);
		Procedural::BoxGenerator()
			.setSizeX(1.6f)
			.setSizeY(0.1f)
			.setSizeZ(0.34f)
			.setNumSegY(2)
			.setNumSegX(2)
			.setNumSegZ(2)
			.setPosition(Ogre::Vector3(0, 2.0f, 0))
			.setEnableNormals(true)
			.addToTriangleBuffer(frame1tb);
		float l = 0.36f;
		Procedural::BoxGenerator()
			.setSizeX(1.6f)
			.setSizeY(0.15f)
			.setSizeZ(l)
			.setNumSegY(2)
			.setNumSegX(2)
			.setNumSegZ(2)
			.setPosition(Ogre::Vector3(0, 0.0f, -(l - 0.34f)))
			.setEnableNormals(true)
			.addToTriangleBuffer(frame1tb);
		clampUV(e, frame1tb, "windowsFrameColor");
		for (const auto &v : frame1tb.getVertices())
			std::cout << v.mPosition << " " << v.mUV << std::endl;
		frame1mesh = frame1tb.transformToMesh("window-frame1");
		std::cout << "submeshes: " << frame1mesh->getSubMeshes().size()
			  << std::endl;
		while (frame1mesh->getSubMeshes().size() > 1) {
			std::cout << "destroying submesh: "
				  << frame1mesh->getSubMeshes().size() - 1
				  << std::endl;
			frame1mesh->destroySubMesh(
				frame1mesh->getSubMeshes().size() - 1);
		}
		int count = 0;
		bool crash = false;
		for (count = 0; count < frame1mesh->getSubMeshes().size();
		     count++) {
			Ogre::SubMesh *submesh =
				frame1mesh->getSubMeshes()[count];
			std::cout << frame1mesh->getName()
				  << ": submesh: " << count << " " << submesh
				  << std::endl;
			if (submesh)
				submesh->parent = frame1mesh.get();
			else
				crash = true;
		}
		OgreAssert(!crash, "no submesh");
		Ogre::LodConfig configFrame(frame1mesh);
		setupLods(configFrame);
		Ogre::MeshPtr meshGlass =
			glass1tb.transformToMesh("window-glass1");
		Ogre::LodConfig configGlass(meshGlass);
		setupLods(configGlass);
	}
}
void createTownDoors(flecs::entity e)
{
	ZoneScoped;
	Ogre::MeshPtr extframe1mesh =
		Ogre::MeshManager::getSingleton().getByName(
			"external-door-frame1");
	if (!extframe1mesh) {
		Procedural::TriangleBuffer extframe1tb;
		Procedural::TriangleBuffer intframe1tb;
		Procedural::TriangleBuffer extdoor1tb;
		Procedural::TriangleBuffer intdoor1tb;
		// external
		Procedural::BoxGenerator()
			.setSizeX(0.1f)
			.setSizeY(3.0f)
			.setSizeZ(0.34f)
			.setNumSegY(2)
			.setNumSegX(2)
			.setNumSegZ(2)
			.setPosition(Ogre::Vector3(-0.7f + 0.05f, 1.5f, 0))
			.setEnableNormals(true)
			.addToTriangleBuffer(extframe1tb);
		Procedural::BoxGenerator()
			.setSizeX(0.1f)
			.setSizeY(3.0f)
			.setSizeZ(0.34f)
			.setNumSegY(2)
			.setNumSegX(2)
			.setNumSegZ(2)
			.setPosition(Ogre::Vector3(0.7f - 0.05f, 1.5f, 0))
			.setEnableNormals(true)
			.addToTriangleBuffer(extframe1tb);
		Procedural::BoxGenerator()
			.setSizeX(1.4f)
			.setSizeY(0.1f)
			.setSizeZ(0.34f)
			.setNumSegY(2)
			.setNumSegX(2)
			.setNumSegZ(2)
			.setPosition(Ogre::Vector3(0, 3.0f, 0))
			.setEnableNormals(true)
			.addToTriangleBuffer(extframe1tb);
		Procedural::BoxGenerator()
			.setSizeX(1.4f)
			.setSizeY(0.15f)
			.setSizeZ(0.45f)
			.setNumSegY(2)
			.setNumSegX(2)
			.setNumSegZ(2)
			.setPosition(Ogre::Vector3(0, 0.0f, (0.45f - 0.34f)))
			.setEnableNormals(true)
			.addToTriangleBuffer(extframe1tb);
		// internal
		Procedural::BoxGenerator()
			.setSizeX(0.1f)
			.setSizeY(3.0f)
			.setSizeZ(0.2f)
			.setNumSegY(2)
			.setNumSegX(2)
			.setNumSegZ(2)
			.setPosition(Ogre::Vector3(-0.7f + 0.05f, 1.5f,
						   0.2f / 2.0f - 0.05f))
			.setEnableNormals(true)
			.addToTriangleBuffer(intframe1tb);
		Procedural::BoxGenerator()
			.setSizeX(0.1f)
			.setSizeY(3.0f)
			.setSizeZ(0.2f)
			.setNumSegY(2)
			.setNumSegX(2)
			.setNumSegZ(2)
			.setPosition(Ogre::Vector3(0.7f - 0.05f, 1.5f,
						   0.2f / 2.0f - 0.05f))
			.setEnableNormals(true)
			.addToTriangleBuffer(intframe1tb);
		Procedural::BoxGenerator()
			.setSizeX(1.4f)
			.setSizeY(0.1f)
			.setSizeZ(0.2f)
			.setNumSegY(2)
			.setNumSegX(2)
			.setNumSegZ(2)
			.setPosition(
				Ogre::Vector3(0, 3.0f, 0.2f / 2.0f - 0.05f))
			.setEnableNormals(true)
			.addToTriangleBuffer(intframe1tb);
		Procedural::BoxGenerator()
			.setSizeX(1.4f)
			.setSizeY(0.15f)
			.setSizeZ(0.2f)
			.setNumSegY(2)
			.setNumSegX(2)
			.setNumSegZ(2)
			.setPosition(
				Ogre::Vector3(0, 0.0f, 0.2f / 2.0f - 0.05f))
			.setEnableNormals(true)
			.addToTriangleBuffer(intframe1tb);
		clampUV(e, extframe1tb, "externalDoorsFrameColor");
		clampUV(e, intframe1tb, "internalDoorsFrameColor");
		clampUV(e, extdoor1tb, "externalDoorColor");
		clampUV(e, intdoor1tb, "internalDoorColor");
		if (extframe1tb.getVertices().size() > 0) {
			extframe1mesh = extframe1tb.transformToMesh(
				"external-door-frame1");
			Ogre::LodConfig configExtFrame(extframe1mesh);
			setupLods(configExtFrame);
		}
		if (intframe1tb.getVertices().size() > 0) {
			Ogre::MeshPtr intframe1mesh =
				intframe1tb.transformToMesh(
					"internal-door-frame1");
			Ogre::LodConfig configIntFrame(intframe1mesh);
			setupLods(configIntFrame);
		}
		if (extdoor1tb.getVertices().size() > 0) {
			Ogre::MeshPtr meshExtDoor =
				extdoor1tb.transformToMesh("external-door1");
			Ogre::LodConfig configExtDoor(meshExtDoor);
			setupLods(configExtDoor);
		}
		if (intdoor1tb.getVertices().size() > 0) {
			Ogre::MeshPtr meshIntDoor =
				intdoor1tb.transformToMesh("internal-door1");
			Ogre::LodConfig configIntDoor(meshIntDoor);
			setupLods(configIntDoor);
		}
	}
}
struct TownTask {
	virtual void operator()(flecs::entity e,
				const nlohmann::json &jdistrict, int index,
				Ogre::SceneNode *sceneNode,
				Ogre::StaticGeometry *geo) = 0;
	virtual void wait() = 0;
	virtual ~TownTask() {};
	TownTask() {};
	static void addMeshToStatic(Ogre::StaticGeometry *geo,
				    Ogre::MeshPtr mesh,
				    Ogre::MaterialPtr material,
				    const Ogre::Vector3 &position,
				    const Ogre::Quaternion &orientation)
	{
		ZoneScoped;
		Ogre::Entity *ent =
			ECS::get<EngineData>().mScnMgr->createEntity(mesh);
		ent->setMaterial(material);
		geo->addEntity(ent, position, orientation);
		ECS::get<EngineData>().mScnMgr->destroyEntity(ent);
	}
	static void addStaticBodyMesh(flecs::entity e, Ogre::MeshPtr mesh,
				      const Ogre::Vector3 &position,
				      const Ogre::Quaternion &orientation)
	{
		ZoneScoped;
		JPH::ShapeRefC shape =
			JoltPhysicsWrapper::getSingleton().createMeshShape(
				mesh);
		JPH::BodyID id = JoltPhysicsWrapper::getSingleton().createBody(
			shape, 0, position, orientation,
			JPH::EMotionType::Static, Layers::NON_MOVING);
		JoltPhysicsWrapper::getSingleton().addBody(
			id, JPH::EActivation::Activate);
		flecs::entity ce =
			ECS::get().entity().child_of(e).set<JPH::BodyID>(id);
	}
};

struct TownPlazza : TownTask {
	std::shared_future<bool> townPlazzaComplete;
	void createTownPlazza(flecs::entity e, const nlohmann::json &jdistrict,
			      int index, Ogre::SceneNode *sceneNode,
			      Ogre::StaticGeometry *geo)
	{
		ZoneScopedN("createTownPlazza");
		struct QueueData {
			Procedural::TriangleBuffer tb;
			Ogre::Vector3 position;
			Ogre::Quaternion rotation;
			nlohmann::json jp;
			Procedural::Shape *shape;
			Ogre::String meshName;
			Ogre::MaterialPtr material;
			Ogre::StaticGeometry *geo;
			flecs::entity e;
		};
		if (townPlazzaComplete.valid() &&
		    townPlazzaComplete.wait_for(std::chrono::seconds(0)) !=
			    std::future_status::ready)
			townPlazzaComplete.wait();

		auto promise = std::make_shared<std::promise<bool> >();
		townPlazzaComplete = promise->get_future();
		Ogre::MaterialPtr townMaterial;
		townMaterial = Ogre::MaterialManager::getSingleton().getByName(
			"proceduralMaterialTown" +
			Ogre::StringConverter::toString(e.id()));
		Ogre::Vector3 worldPosition = sceneNode->_getDerivedPosition();
		Ogre::Quaternion worldOrientation =
			sceneNode->_getDerivedOrientation();
		Procedural::TriangleBuffer tb;
		Ogre::String meshName =
			"plazzaMesh" + Ogre::StringConverter::toString(index) +
			"_" + Ogre::StringConverter::toString(e.id());
		QueueData *queueData =
			new QueueData({ tb, worldPosition, worldOrientation,
					jdistrict, new Procedural::Shape,
					meshName, townMaterial, geo, e });
		Ogre::Vector3 localPosition(0, 0, 0);
		Ogre::Quaternion localRotation = Ogre::Quaternion::IDENTITY;
		from_json(queueData->jp["position"], localPosition);
		from_json(queueData->jp["rotation"], localRotation);
		Ogre::MeshPtr mesh =
			Ogre::MeshManager::getSingleton().getByName(
				queueData->meshName);
		if (mesh)
			Ogre::MeshManager::getSingleton().remove(mesh);
		Ogre::Root::getSingleton().getWorkQueue()->addTask([promise,
								    queueData,
								    localPosition,
								    localRotation]() {
			ZoneScoped;
			const nlohmann::json &jp = queueData->jp;
			float radius = 5.0f;
			float height = 0.2f;
			float elevation = 0.0f;
			bool plazza = false;
			if (jp.find("plazza") != jp.end())
				plazza = jp["plazza"].get<bool>();
			if (!plazza)
				return;
			if (jp.find("radius") != jp.end())
				radius = jp["radius"].get<float>();
			if (jp.find("height") != jp.end())
				height = jp["height"].get<float>();
			if (jp.find("elevation") != jp.end())
				elevation = jp["elevation"].get<float>();
			if (height < 0.1f)
				height = 0.1f;
			if (radius < 5.0f)
				radius = 5.0f;
			float mh = 4.0f;
			Procedural::Shape *plazzaShape = queueData->shape;
			plazzaShape->addPoint(0, -height - mh - mh);
			plazzaShape->addPoint(radius * 0.5f + mh,
					      -height - mh - mh);
			plazzaShape->addPoint(radius + mh, -height - mh);
			plazzaShape->addPoint(radius, -height);
			plazzaShape->addPoint(radius, 0.0f);
			plazzaShape->addPoint(radius - 0.1f, 0.1f);
			plazzaShape->addPoint(radius - mh + 0.1f, height);
			plazzaShape->addPoint(radius - mh, height + 0.1f);
			plazzaShape->addPoint(radius * 0.5f + mh, height);
			plazzaShape->addPoint(0, height);
			Procedural::Lathe()
				.setShapeToExtrude(plazzaShape)
				.setEnableNormals(true)
				.setPosition(Ogre::Vector3(
					0.0f, height / 2.0f + elevation, 0.0f))
				.setNumSeg(24)
				.addToTriangleBuffer(queueData->tb);
			for (auto &v : queueData->tb.getVertices()) {
				v.mUV *= 0.08f;
				v.mUV.x += 0.41f;
				v.mUV.x =
					Ogre::Math::Clamp(v.mUV.x, 0.4f, 0.5f);
				v.mUV.y =
					Ogre::Math::Clamp(v.mUV.y, 0.0f, 0.1f);
			}
			Ogre::Root::getSingleton()
				.getWorkQueue()
				->addMainThreadTask([queueData, localPosition,
						     localRotation]() {
					ZoneScopedN(
						"createTownPlazza::MainThread");
					Ogre::Vector3 worldPlazzaCenter =
						queueData->position +
						localPosition;
					Ogre::MeshPtr mesh =
						queueData->tb.transformToMesh(
							queueData->meshName);
					Ogre::LodConfig config(mesh);
					setupLods(config);
					addMeshToStatic(queueData->geo, mesh,
							queueData->material,
							worldPlazzaCenter,
							queueData->rotation *
								localRotation);
					addStaticBodyMesh(
						queueData->e, mesh,
						worldPlazzaCenter,
						queueData->rotation *
							localRotation);
					delete queueData;
				});
			promise->set_value(true);
		});
	}
	void operator()(flecs::entity e, const nlohmann::json &jdistrict,
			int index, Ogre::SceneNode *sceneNode,
			Ogre::StaticGeometry *geo)
	{
		ZoneScoped;
		createTownPlazza(e, jdistrict, index, sceneNode, geo);
	}
	void wait()
	{
		ZoneScoped;
		if (townPlazzaComplete.valid() &&
		    townPlazzaComplete.wait_for(std::chrono::seconds(0)) !=
			    std::future_status::ready)
			townPlazzaComplete.wait();
	}
};

struct TownLots : TownTask {
	std::shared_future<bool> townLotsComplete;
	void createTownLots(flecs::entity e, const nlohmann::json &jdistrict,
			    int index, Ogre::SceneNode *sceneNode,
			    Ogre::StaticGeometry *geo)
	{
		ZoneScopedN("createTownLots");
		struct QueueData {
			Procedural::TriangleBuffer tb;
			Ogre::Vector3 position;
			Ogre::Quaternion rotation;
			nlohmann::json jp;
			Procedural::Shape *shape;
			Ogre::String meshName;
			Ogre::MaterialPtr material;
			Ogre::StaticGeometry *geo;
			flecs::entity e;
		};
		struct LotData {
			int width, depth;
			float angle, elevation;
			struct QueueData queueData;
		};

		if (townLotsComplete.valid() &&
		    townLotsComplete.wait_for(std::chrono::seconds(0)) !=
			    std::future_status::ready)
			townLotsComplete.wait();

		auto promise = std::make_shared<std::promise<bool> >();
		townLotsComplete = promise->get_future();
		Ogre::MaterialPtr townMaterial;
		townMaterial = Ogre::MaterialManager::getSingleton().getByName(
			"proceduralMaterialTown" +
			Ogre::StringConverter::toString(e.id()));
		nlohmann::json jlots = nlohmann::json::array();
		if (jdistrict.find("lots") != jdistrict.end())
			jlots = jdistrict["lots"];
		Ogre::Vector3 centerPosition = sceneNode->_getDerivedPosition();
		Ogre::Quaternion centerOrientation =
			sceneNode->_getDerivedOrientation();
		float baseHeight = 4.0f;
		const nlohmann::json &jp = jdistrict;
		Ogre::Vector3 localPosition(0, 0, 0);
		Ogre::Quaternion localRotation = Ogre::Quaternion::IDENTITY;
		float delevation = 0.0f;
		float radius = 50.0f;
		if (jp.find("elevation") != jp.end())
			delevation = jp["elevation"].get<float>();
		if (jp.find("radius") != jp.end())
			radius = jp["radius"].get<float>();
		from_json(jp["position"], localPosition);
		from_json(jp["rotation"], localRotation);
		centerPosition = centerPosition + localPosition +
				 Ogre::Vector3(0, delevation, 0);
		centerOrientation = centerOrientation * localRotation;
		static std::vector<LotData> lots;
		lots.resize(0);
		lots.reserve(jlots.size());
		{
			int count = 0;
			for (const auto &jb : jlots) {
				Procedural::TriangleBuffer tb;
				Ogre::String meshName =
					"lotbase" +
					Ogre::StringConverter::toString(count) +
					"_" +
					Ogre::StringConverter::toString(
						e.id()) +
					"_" +
					Ogre::StringConverter::toString(index);
				Ogre::MeshPtr mesh =
					Ogre::MeshManager::getSingleton()
						.getByName(meshName);
				if (mesh)
					Ogre::MeshManager::getSingleton().remove(
						mesh);
				int depth = 10;
				int width = 10;
				float angle = 0.0f;
				float elevation = 0.0f;
				if (jb.find("depth") != jb.end())
					depth = jb["depth"].get<int>();
				if (jb.find("width") != jb.end())
					width = jb["width"].get<int>();
				if (jb.find("angle") != jb.end())
					angle = jb["angle"].get<float>();
				if (jb.find("elevation") != jb.end())
					elevation =
						jb["elevation"].get<float>();
				OgreAssert(width > 1 && depth > 1 &&
						   baseHeight > 1,
					   "Bad stuff happen");
				lots.push_back(
					{ width,
					  depth,
					  angle,
					  elevation,
					  { tb, centerPosition,
					    centerOrientation, jdistrict,
					    new Procedural::Shape, meshName,
					    townMaterial, geo, e } });
				count++;
			}
		}
		OgreAssert(lots.size() > 0, "bad lots count");
		Ogre::Root::getSingleton().getWorkQueue()->addTask([radius,
								    baseHeight,
								    promise]() {
			ZoneScoped;
			std::cout << "lots count: " << lots.size() << std::endl;
			OgreAssert(lots.size() > 0, "bad lots count");
			for (auto &lot : lots) {
				float distance = radius;

				Ogre::Quaternion rotation = Ogre::Quaternion(
					Ogre::Degree(lot.angle),
					Ogre::Vector3::UNIT_Y);
				Ogre::Vector3 offset =
					lot.queueData.rotation * rotation *
					(Ogre::Vector3::UNIT_Z * distance);
				Procedural::BoxGenerator()
					.setSizeX(4.0f * (float)lot.width)
					.setSizeY(baseHeight)
					.setSizeZ(4.0f * (float)lot.depth)
					.setNumSegY(8)
					.setNumSegX(8)
					.setNumSegZ(8)
					.setEnableNormals(true)
					.setPosition(Ogre::Vector3(
						0.0f,
						-baseHeight / 2.0f +
							lot.elevation - 0.01f,
						0.0f))
					.addToTriangleBuffer(lot.queueData.tb);
				OgreAssert(
					lot.queueData.tb.getVertices().size() >
						8,
					"bad box");
				for (auto &v : lot.queueData.tb.getVertices()) {
					v.mUV *= 0.08f;
					v.mUV.x += 0.41f;
					v.mUV.x = Ogre::Math::Clamp(v.mUV.x,
								    0.4f, 0.5f);
					v.mUV.y = Ogre::Math::Clamp(v.mUV.y,
								    0.0f, 0.1f);
				}
			}
			Ogre::Root::getSingleton()
				.getWorkQueue()
				->addMainThreadTask([promise, radius]() {
					ZoneScopedN(
						"createTownLots::MainThread");
					float distance = radius;
					for (auto &lot : lots) {
						std::cout
							<< lot.queueData.meshName
							<< std::endl;
						Ogre::MeshPtr mesh =
							Ogre::MeshManager::getSingleton()
								.getByName(
									lot.queueData
										.meshName);
						if (mesh)
							Ogre::MeshManager::
								getSingleton()
									.remove(mesh);
						mesh = lot.queueData.tb.transformToMesh(
							lot.queueData.meshName);
						Ogre::Quaternion rotation =
							Ogre::Quaternion(
								Ogre::Degree(
									lot.angle),
								Ogre::Vector3::
									UNIT_Y);
						Ogre::Vector3 offset =
							lot.queueData.rotation *
							rotation *
							(Ogre::Vector3::UNIT_Z *
							 distance);
						Ogre::LodConfig config(mesh);
						setupLods(config);
						addMeshToStatic(
							lot.queueData.geo, mesh,
							lot.queueData.material,
							lot.queueData.position +
								offset,
							lot.queueData.rotation *
								rotation);
						addStaticBodyMesh(
							lot.queueData.e, mesh,
							lot.queueData.position +
								offset,
							lot.queueData.rotation *
								rotation);
					}
					//OgreAssert(false, "TownLots");
				});
			promise->set_value(true);
		});
	}
	void operator()(flecs::entity e, const nlohmann::json &jdistrict,
			int index, Ogre::SceneNode *sceneNode,
			Ogre::StaticGeometry *geo)
	{
		ZoneScoped;
		createTownLots(e, jdistrict, index, sceneNode, geo);
	}
	void wait()
	{
		ZoneScoped;
		if (townLotsComplete.valid() &&
		    townLotsComplete.wait_for(std::chrono::seconds(0)) !=
			    std::future_status::ready)
			townLotsComplete.wait();
	}
};
struct TownCells : TownTask {
	std::shared_future<bool> townCellsComplete;
	void createCells(flecs::entity e, const nlohmann::json &jdistrict,
			 int index, Ogre::SceneNode *sceneNode,
			 Ogre::StaticGeometry *geo)
	{
		ZoneScoped;
		struct QueueData {
			Procedural::TriangleBuffer tb;
			Ogre::Vector3 position;
			Ogre::Quaternion rotation;
			nlohmann::json jp;
			Procedural::Shape *shape;
			Ogre::String meshName;
			Ogre::MaterialPtr material;
			Ogre::StaticGeometry *geo;
			flecs::entity e;
		};
		struct LotData {
			int width, depth;
			float angle, elevation;
			nlohmann::json jlot;
			Procedural::TriangleBuffer cellTb;
			Procedural::TriangleBuffer intwinTb;
			Procedural::TriangleBuffer exteriorTb;
			struct QueueData queueData;
		};

		if (townCellsComplete.valid() &&
		    townCellsComplete.wait_for(std::chrono::seconds(0)) !=
			    std::future_status::ready)
			townCellsComplete.wait();

		auto promise = std::make_shared<std::promise<bool> >();
		townCellsComplete = promise->get_future();
		Ogre::MaterialPtr townMaterial;
		townMaterial = Ogre::MaterialManager::getSingleton().getByName(
			"proceduralMaterialTown" +
			Ogre::StringConverter::toString(e.id()));
		nlohmann::json jlots = nlohmann::json::array();
		if (jdistrict.find("lots") != jdistrict.end())
			jlots = jdistrict["lots"];
		Ogre::Vector3 centerPosition = sceneNode->_getDerivedPosition();
		Ogre::Quaternion centerOrientation =
			sceneNode->_getDerivedOrientation();
		float baseHeight = 4.0f;
		const nlohmann::json &jp = jdistrict;
		Ogre::Vector3 localPosition(0, 0, 0);
		Ogre::Quaternion localRotation = Ogre::Quaternion::IDENTITY;
		float delevation = 0.0f;
		float radius = 50.0f;
		if (jp.find("elevation") != jp.end())
			delevation = jp["elevation"].get<float>();
		if (jp.find("radius") != jp.end())
			radius = jp["radius"].get<float>();
		from_json(jp["position"], localPosition);
		from_json(jp["rotation"], localRotation);
		centerPosition = centerPosition + localPosition +
				 Ogre::Vector3(0, delevation, 0);
		centerOrientation = centerOrientation * localRotation;
		static std::vector<LotData> lots;
		lots.resize(0);
		lots.reserve(jlots.size());
		{
			int count = 0;
			for (const auto &jb : jlots) {
				Procedural::TriangleBuffer tb;
				Procedural::TriangleBuffer cellTb;
				Procedural::TriangleBuffer intwinTb;
				Procedural::TriangleBuffer exteriorTb;
				Ogre::String meshName =
					"lotcells" +
					Ogre::StringConverter::toString(count) +
					"_" +
					Ogre::StringConverter::toString(
						e.id()) +
					"_" +
					Ogre::StringConverter::toString(index);
				Ogre::MeshPtr mesh =
					Ogre::MeshManager::getSingleton()
						.getByName(meshName);
				if (mesh)
					Ogre::MeshManager::getSingleton().remove(
						mesh);
				int depth = 10;
				int width = 10;
				float angle = 0.0f;
				float elevation = 0.0f;
				if (jb.find("depth") != jb.end())
					depth = jb["depth"].get<int>();
				if (jb.find("width") != jb.end())
					width = jb["width"].get<int>();
				if (jb.find("angle") != jb.end())
					angle = jb["angle"].get<float>();
				if (jb.find("elevation") != jb.end())
					elevation =
						jb["elevation"].get<float>();
				OgreAssert(width > 1 && depth > 1 &&
						   baseHeight > 1,
					   "Bad stuff happen");
				if (jb.find("cells") != jb.end())
					lots.push_back(
						{ width,
						  depth,
						  angle,
						  elevation,
						  jb,
						  cellTb,
						  intwinTb,
						  exteriorTb,
						  { tb, centerPosition,
						    centerOrientation,
						    jdistrict,
						    new Procedural::Shape,
						    meshName, townMaterial, geo,
						    e } });
				count++;
			}
		}
		Ogre::Root::getSingleton().getWorkQueue()->addTask([radius,
								    baseHeight,
								    promise]() {
			{
				ZoneScoped;
				int count = 0;
				for (auto &lot : lots) {
					for (auto &jcell : lot.jlot["cells"]) {
						struct ProcessCells pcells;
						int x = jcell["x"].get<int>();
						int y = jcell["y"].get<int>();
						int z = jcell["z"].get<int>();
						uint64_t flags =
							jcell["flags"]
								.get<uint64_t>();
						Ogre::Vector3 cellOffset(
							x * 2.0f, y * 4.0f,
							z * 2.0f);
						if (flags == 0) {
							Procedural::BoxGenerator()
								.setSizeX(2.0f +
									  0.4f)
								.setSizeY(0.2f)
								.setSizeZ(2.0f +
									  0.4f)
								.setNumSegY(8)
								.setNumSegX(8)
								.setNumSegZ(8)
								.setEnableNormals(
									true)
								.setPosition(
									cellOffset)
								.addToTriangleBuffer(
									pcells.floortb);
							lot.cellTb.append(
								pcells.floortb);

						} else
							pcells.process(
								lot.queueData.e,
								cellOffset,
								flags,
								lot.cellTb,
								lot.intwinTb,
								lot.exteriorTb);
					}
				}
			}
			Ogre::Root::getSingleton().getWorkQueue()->addMainThreadTask([radius,
										      baseHeight,
										      promise]() {
				ZoneScopedN("createCells::MainThread");
				int count = 0;
				for (auto &lot : lots) {
					float distance = radius;
					Ogre::Quaternion rotation =
						Ogre::Quaternion(
							Ogre::Degree(lot.angle),
							Ogre::Vector3::UNIT_Y);
					Ogre::Vector3 offset =
						lot.queueData.rotation *
						rotation *
						(Ogre::Vector3::UNIT_Z *
						 distance);
					Ogre::Vector3 worldCenterPosition =
						lot.queueData.position +
						offset +
						Ogre::Vector3(0, lot.elevation,
							      0);
					Ogre::Quaternion worldCenterOrientation =
						lot.queueData.rotation *
						rotation;
					if (lot.cellTb.getVertices().size() >
					    0) {
						Ogre::MeshPtr mesh =
							Ogre::MeshManager::getSingleton()
								.getByName(
									lot.queueData
										.meshName +
									"_cells");
						if (mesh)
							Ogre::MeshManager::
								getSingleton()
									.remove(mesh);
						std::cout
							<< "vertex count: "
							<< lot.cellTb
								   .getVertices()
								   .size()
							<< std::endl;
						mesh = lot.cellTb.transformToMesh(
							lot.queueData.meshName);
						/* debug save mesh */
						// serializeMesh(mesh, meshName);
						Ogre::LodConfig config(mesh);
						setupLods(config);
						addMeshToStatic(
							lot.queueData.geo, mesh,
							lot.queueData.material,
							worldCenterPosition,
							worldCenterOrientation);
						addStaticBodyMesh(
							lot.queueData.e, mesh,
							worldCenterPosition,
							worldCenterOrientation);
					}
					if (lot.intwinTb.getVertices().size() >
					    0) {
						Ogre::MeshPtr mesh =
							Ogre::MeshManager::getSingleton()
								.getByName(
									lot.queueData
										.meshName +
									"_intwin");
						if (mesh)
							Ogre::MeshManager::
								getSingleton()
									.remove(mesh);
						std::cout
							<< "vertex count: "
							<< lot.intwinTb
								   .getVertices()
								   .size()
							<< std::endl;
						mesh = lot.intwinTb.transformToMesh(
							lot.queueData.meshName +
							"_intwin");
						OgreAssert(mesh, "bad mesh");
						/* debug save mesh */
						// serializeMesh(mesh, meshName);
						Ogre::LodConfig config(mesh);
						setupLods(config);
						addMeshToStatic(
							lot.queueData.geo, mesh,
							lot.queueData.material,
							worldCenterPosition,
							worldCenterOrientation);
						addStaticBodyMesh(
							lot.queueData.e, mesh,
							worldCenterPosition,
							worldCenterOrientation);
					}
					if (lot.exteriorTb.getVertices().size() >
					    0) {
						Ogre::MeshPtr mesh =
							Ogre::MeshManager::getSingleton()
								.getByName(
									lot.queueData
										.meshName +
									"_exterior");
						if (mesh)
							Ogre::MeshManager::
								getSingleton()
									.remove(mesh);
						std::cout
							<< "vertex count: "
							<< lot.exteriorTb
								   .getVertices()
								   .size()
							<< std::endl;
						mesh = lot.exteriorTb.transformToMesh(
							lot.queueData.meshName +
							"_exterior");
						/* debug save mesh */
						// serializeMesh(mesh, meshName);
						Ogre::LodConfig config(mesh);
						setupLods(config);
						addMeshToStatic(
							lot.queueData.geo, mesh,
							lot.queueData.material,
							worldCenterPosition,
							worldCenterOrientation);
						addStaticBodyMesh(
							lot.queueData.e, mesh,
							worldCenterPosition,
							worldCenterOrientation);
					}
					count++;
				}
			});
			promise->set_value(true);
		});
	}
	void operator()(flecs::entity e, const nlohmann::json &jdistrict,
			int index, Ogre::SceneNode *sceneNode,
			Ogre::StaticGeometry *geo)
	{
		ZoneScoped;
		createCells(e, jdistrict, index, sceneNode, geo);
	}
	void wait()
	{
		ZoneScoped;
		if (townCellsComplete.valid() &&
		    townCellsComplete.wait_for(std::chrono::seconds(0)) !=
			    std::future_status::ready)
			townCellsComplete.wait();
	}
};

struct TownRoofs : TownTask {
	std::shared_future<bool> townRoofsComplete;

private:
	// Constants
	static constexpr float CELL_WIDTH = 2.0f;
	static constexpr float CELL_HEIGHT = 4.0f;
	static constexpr float CELL_DEPTH = 2.0f;
	static constexpr float EXTEND = 0.24f;
	static constexpr float SQRT2 = 1.4142f;
	struct DistrictContext {
		Ogre::Vector3 centerPosition;
		Ogre::Quaternion centerOrientation;
		float radius;
	};
	struct LotContext {
		int width, depth;
		float angle;
		float elevation;
		nlohmann::json jlot;
		Ogre::Vector3 worldPosition;
		Ogre::Quaternion worldOrientation;
	};
	struct RoofPlacement {
		int type;
		int posX, posY, posZ;
		int sizeX, sizeZ;
		float baseHeight, maxHeight;
	};
	struct RoofGenerationData {
		flecs::entity entity;
		Ogre::MaterialPtr material;
		Ogre::StaticGeometry *geo;
		Ogre::Vector3 districtPosition;
		Ogre::Quaternion districtOrientation;
		float districtRadius;
		std::string baseMeshName;
	};
	DistrictContext parseDistrictContext(const nlohmann::json &jp,
					     Ogre::SceneNode *sceneNode)
	{
		DistrictContext ctx;

		// Parse base position and rotation
		Ogre::Vector3 localPosition(0, 0, 0);
		Ogre::Quaternion localRotation = Ogre::Quaternion::IDENTITY;
		from_json(jp["position"], localPosition);
		from_json(jp["rotation"], localRotation);

		// Parse elevation and radius
		float elevation = jp.value("elevation", 0.0f);
		ctx.radius = jp.value("radius", 50.0f);

		// Calculate world transforms for district center
		ctx.centerPosition = sceneNode->_getDerivedPosition() +
				     localPosition +
				     Ogre::Vector3(0, elevation, 0);
		ctx.centerOrientation =
			sceneNode->_getDerivedOrientation() * localRotation;

		return ctx;
	}
	LotContext createLotContext(const DistrictContext &districtCtx,
				    const nlohmann::json &jlot, int lotIndex)
	{
		LotContext lot;
		lot.jlot = jlot;

		// Parse lot properties
		lot.width = jlot.value("width", 10);
		lot.depth = jlot.value("depth", 10);
		lot.angle = jlot.value("angle", 0.0f);
		lot.elevation = jlot.value("elevation", 0.0f);

		OgreAssert(lot.width > 1 && lot.depth > 1,
			   "Invalid lot dimensions");

		// Calculate lot world transform
		Ogre::Quaternion lotRotation(Ogre::Degree(lot.angle),
					     Ogre::Vector3::UNIT_Y);
		Ogre::Vector3 offset =
			districtCtx.centerOrientation * lotRotation *
			(Ogre::Vector3::UNIT_Z * districtCtx.radius);

		lot.worldPosition = districtCtx.centerPosition + offset;
		lot.worldOrientation =
			districtCtx.centerOrientation * lotRotation;

		return lot;
	}
	std::vector<RoofPlacement>
	parseRoofPlacements(const nlohmann::json &jlot)
	{
		std::vector<RoofPlacement> roofs;

		if (!jlot.contains("roofs") || !jlot["roofs"].is_array()) {
			return roofs;
		}

		for (const auto &jroof : jlot["roofs"]) {
			RoofPlacement roof;
			roof.type = jroof["type"].get<int>();
			roof.posX = jroof["position_x"].get<int>();
			roof.posY = jroof["position_y"].get<int>();
			roof.posZ = jroof["position_z"].get<int>();
			roof.sizeX = jroof["size_x"].get<int>();
			roof.sizeZ = jroof["size_z"].get<int>();
			roof.baseHeight = jroof["base_height"].get<float>();
			roof.maxHeight = jroof["max_height"].get<float>();

			roofs.push_back(roof);
		}

		return roofs;
	}

	Ogre::Vector3 calculateLocalPosition(const RoofPlacement &roof)
	{
		float x = static_cast<float>(roof.posX) * CELL_WIDTH - 1.0f +
			  static_cast<float>(roof.sizeX) * CELL_WIDTH / 2.0f;
		float z = static_cast<float>(roof.posZ) * CELL_DEPTH - 1.0f +
			  static_cast<float>(roof.sizeZ) * CELL_DEPTH / 2.0f;
		return Ogre::Vector3(x, 0.0f, z);
	}
	void transformToWorld(Procedural::TriangleBuffer &tb,
			      const LotContext &lotCtx)
	{
		if (tb.getVertices().empty())
			return;

		std::vector<Procedural::TriangleBuffer::Vertex> &vertices =
			tb.getVertices();
		for (auto &v : vertices) {
			// Apply lot rotation to local position
			Ogre::Vector3 rotatedPos =
				lotCtx.worldOrientation * v.mPosition;
			// Add lot world position and elevation
			v.mPosition = lotCtx.worldPosition + rotatedPos +
				      Ogre::Vector3(0, lotCtx.elevation, 0);

			// Rotate normals
			v.mNormal = lotCtx.worldOrientation * v.mNormal;
		}
	}
	void generateRoofType0(const RoofPlacement &roof,
			       const LotContext &lotCtx,
			       Procedural::TriangleBuffer &tbTop,
			       Procedural::TriangleBuffer &tbSide)
	{
		float width = static_cast<float>(roof.sizeX) * CELL_WIDTH;
		float depth = static_cast<float>(roof.sizeZ) * CELL_DEPTH;
		Ogre::Vector3 localPos = calculateLocalPosition(roof);

		// Type 0 Y calculation from original code:
		// (float)position_y * 4.0f + baseHeight / 2.0f + lot.elevation
		float baseY = static_cast<float>(roof.posY) * CELL_HEIGHT +
			      roof.baseHeight / 2.0f;

		// Main roof box - position already includes baseHeight/2
		Procedural::BoxGenerator()
			.setSizeX(width)
			.setSizeY(roof.baseHeight)
			.setSizeZ(depth)
			.setNumSegY(1)
			.setNumSegX(1)
			.setNumSegZ(1)
			.setEnableNormals(true)
			.setPosition(
				Ogre::Vector3(localPos.x, baseY, localPos.z))
			.addToTriangleBuffer(tbTop);

		// Edge trim pieces - Y calculation from original:
		// (float)position_y * 4.0f + baseHeight / 2.0f + lot.elevation + extend
		float trimBaseY = static_cast<float>(roof.posY) * CELL_HEIGHT +
				  roof.baseHeight / 2.0f;

		// Z+ edge
		Procedural::BoxGenerator()
			.setSizeX(width)
			.setSizeY(roof.baseHeight + EXTEND * 2.0f)
			.setSizeZ(EXTEND)
			.setNumSegY(1)
			.setNumSegX(1)
			.setNumSegZ(1)
			.setEnableNormals(true)
			.setPosition(Ogre::Vector3(
				localPos.x, trimBaseY + EXTEND,
				localPos.z + depth / 2.0f + EXTEND / 2.0f))
			.addToTriangleBuffer(tbSide);

		// Z- edge
		Procedural::BoxGenerator()
			.setSizeX(width)
			.setSizeY(roof.baseHeight + EXTEND * 2.0f)
			.setSizeZ(EXTEND)
			.setNumSegY(1)
			.setNumSegX(1)
			.setNumSegZ(1)
			.setEnableNormals(true)
			.setPosition(Ogre::Vector3(
				localPos.x, trimBaseY + EXTEND,
				localPos.z - depth / 2.0f - EXTEND / 2.0f))
			.addToTriangleBuffer(tbSide);

		// X+ edge
		Procedural::BoxGenerator()
			.setSizeX(EXTEND)
			.setSizeY(roof.baseHeight + EXTEND * 2.0f)
			.setSizeZ(depth + EXTEND * 2.0f)
			.setNumSegY(1)
			.setNumSegX(1)
			.setNumSegZ(1)
			.setEnableNormals(true)
			.setPosition(Ogre::Vector3(
				localPos.x + width / 2.0f + EXTEND / 2.0f,
				trimBaseY + EXTEND, localPos.z))
			.addToTriangleBuffer(tbSide);

		// X- edge
		Procedural::BoxGenerator()
			.setSizeX(EXTEND)
			.setSizeY(roof.baseHeight + EXTEND * 2.0f)
			.setSizeZ(depth + EXTEND * 2.0f)
			.setNumSegY(1)
			.setNumSegX(1)
			.setNumSegZ(1)
			.setEnableNormals(true)
			.setPosition(Ogre::Vector3(
				localPos.x - width / 2.0f - EXTEND / 2.0f,
				trimBaseY + EXTEND, localPos.z))
			.addToTriangleBuffer(tbSide);
	}
	void generateRoofType1(const RoofPlacement &roof,
			       const LotContext &lotCtx,
			       Procedural::TriangleBuffer &tbTop,
			       Procedural::TriangleBuffer &tbSide)
	{
		float width = static_cast<float>(roof.sizeX) * CELL_WIDTH;
		float depth = static_cast<float>(roof.sizeZ) * CELL_DEPTH;
		float q = width / 2.0f;
		float m = 1.0f;
		float d = SQRT2 * (q + m);

		Ogre::Vector3 localPos = calculateLocalPosition(roof);
		float baseY = static_cast<float>(roof.posY) * CELL_HEIGHT;

		// First angled box
		Procedural::BoxGenerator()
			.setSizeX(d)
			.setSizeY(roof.baseHeight / 2.0f)
			.setSizeZ(depth)
			.setNumSegY(1)
			.setNumSegX(1)
			.setNumSegZ(1)
			.setEnableNormals(true)
			.setOrientation(Ogre::Quaternion(
				Ogre::Degree(45),
				Ogre::Vector3::NEGATIVE_UNIT_Z))
			.setPosition(Ogre::Vector3(
				localPos.x - width / 2.0f + q + q / 2.0f +
					m / 2.0f,
				baseY + roof.baseHeight + q / 2.0f - m / 2.0f,
				localPos.z))
			.addToTriangleBuffer(tbTop);

		// Second angled box
		Procedural::BoxGenerator()
			.setSizeX(d)
			.setSizeY(roof.baseHeight / 2.0f)
			.setSizeZ(depth)
			.setNumSegY(1)
			.setNumSegX(1)
			.setNumSegZ(1)
			.setEnableNormals(true)
			.setOrientation(Ogre::Quaternion(
				Ogre::Degree(-45),
				Ogre::Vector3::NEGATIVE_UNIT_Z))
			.setPosition(Ogre::Vector3(
				localPos.x - width / 2.0f + q / 2.0f - m / 2.0f,
				baseY + roof.baseHeight + q / 2.0f - m / 2.0f,
				localPos.z))
			.addToTriangleBuffer(tbTop);

		// Side trim pieces
		float sideY = baseY + q / 2.0f;
		float sideHeight = q + roof.baseHeight * 3.0f;

		// Z+ edge
		Procedural::BoxGenerator()
			.setSizeX(width + EXTEND * 2.0f)
			.setSizeY(sideHeight)
			.setSizeZ(EXTEND)
			.setNumSegX(3)
			.setNumSegY(3)
			.setNumSegZ(1)
			.setEnableNormals(true)
			.setPosition(Ogre::Vector3(
				localPos.x, sideY + roof.baseHeight,
				localPos.z + depth / 2.0f + EXTEND / 2.0f))
			.addToTriangleBuffer(tbSide);

		// Z- edge
		Procedural::BoxGenerator()
			.setSizeX(width + EXTEND * 2.0f)
			.setSizeY(sideHeight)
			.setSizeZ(EXTEND)
			.setNumSegX(3)
			.setNumSegY(3)
			.setNumSegZ(1)
			.setEnableNormals(true)
			.setPosition(Ogre::Vector3(
				localPos.x, sideY + roof.baseHeight,
				localPos.z - depth / 2.0f - EXTEND / 2.0f))
			.addToTriangleBuffer(tbSide);

		// Apply vertex deformation
		deformSideVertices(tbSide, true);
	}
	void generateRoofType2(const RoofPlacement &roof,
			       const LotContext &lotCtx,
			       Procedural::TriangleBuffer &tbTop,
			       Procedural::TriangleBuffer &tbSide)
	{
		float width = static_cast<float>(roof.sizeX) * CELL_WIDTH;
		float depth = static_cast<float>(roof.sizeZ) * CELL_DEPTH;
		float q = depth / 2.0f;
		float m = 1.0f;
		float d = SQRT2 * (q + m);

		Ogre::Vector3 localPos = calculateLocalPosition(roof);
		float baseY = static_cast<float>(roof.posY) * CELL_HEIGHT;

		// First angled box
		Procedural::BoxGenerator()
			.setSizeX(width)
			.setSizeY(roof.baseHeight / 2.0f)
			.setSizeZ(d)
			.setNumSegY(1)
			.setNumSegX(1)
			.setNumSegZ(1)
			.setEnableNormals(true)
			.setOrientation(Ogre::Quaternion(
				Ogre::Degree(45),
				Ogre::Vector3::NEGATIVE_UNIT_X))
			.setPosition(Ogre::Vector3(localPos.x,
						   baseY + roof.baseHeight +
							   q / 2.0f - m / 2.0f,
						   localPos.z - depth / 2.0f +
							   q / 2.0f - m / 2.0f))
			.addToTriangleBuffer(tbTop);

		// Second angled box
		Procedural::BoxGenerator()
			.setSizeX(width)
			.setSizeY(roof.baseHeight / 2.0f)
			.setSizeZ(d)
			.setNumSegX(1)
			.setNumSegY(1)
			.setNumSegZ(1)
			.setEnableNormals(true)
			.setOrientation(Ogre::Quaternion(
				Ogre::Degree(-45),
				Ogre::Vector3::NEGATIVE_UNIT_X))
			.setPosition(Ogre::Vector3(
				localPos.x,
				baseY + roof.baseHeight + q / 2.0f - m / 2.0f,
				localPos.z - depth / 2.0f + q + q / 2.0f +
					m / 2.0f))
			.addToTriangleBuffer(tbTop);

		// Side trim pieces
		float sideY = baseY + q / 2.0f;
		float sideHeight = q + roof.baseHeight * 3.0f;
		float sideDepth = depth + 2.0f * roof.baseHeight * 3.0f;

		// X+ edge
		Procedural::BoxGenerator()
			.setSizeX(EXTEND)
			.setSizeY(sideHeight)
			.setSizeZ(sideDepth)
			.setNumSegX(2)
			.setNumSegY(2)
			.setNumSegZ(1)
			.setEnableNormals(true)
			.setPosition(Ogre::Vector3(
				localPos.x + width / 2.0f + EXTEND / 2.0f,
				sideY + roof.baseHeight, localPos.z))
			.addToTriangleBuffer(tbSide);

		// X- edge
		Procedural::BoxGenerator()
			.setSizeX(EXTEND)
			.setSizeY(sideHeight)
			.setSizeZ(sideDepth)
			.setNumSegX(2)
			.setNumSegY(2)
			.setNumSegZ(1)
			.setEnableNormals(true)
			.setPosition(Ogre::Vector3(
				localPos.x - width / 2.0f - EXTEND / 2.0f,
				sideY + roof.baseHeight, localPos.z))
			.addToTriangleBuffer(tbSide);

		// Apply vertex deformation
		deformSideVertices(tbSide, false);
	}
	void deformSideVertices(Procedural::TriangleBuffer &tbSide,
				bool deformXAxis)
	{
		if (tbSide.getVertices().empty())
			return;

		float minY = std::numeric_limits<float>::max();
		float maxY = std::numeric_limits<float>::lowest();
		float minCoord = std::numeric_limits<float>::max();
		float maxCoord = std::numeric_limits<float>::lowest();

		for (const auto &v : tbSide.getVertices()) {
			minY = std::min(minY, v.mPosition.y);
			maxY = std::max(maxY, v.mPosition.y);
			float coord = deformXAxis ? v.mPosition.x :
						    v.mPosition.z;
			minCoord = std::min(minCoord, coord);
			maxCoord = std::max(maxCoord, coord);
		}

		float midCoord = minCoord + (maxCoord - minCoord) / 2.0f;
		float coordRange = (maxCoord - minCoord) / 2.0f;
		if (coordRange < 0.001f)
			return;

		for (auto &v : tbSide.getVertices()) {
			float md = maxY - v.mPosition.y;
			float hm =
				(deformXAxis ? v.mPosition.x : v.mPosition.z) -
				midCoord;
			float he = (hm / coordRange) * md;

			if (deformXAxis) {
				v.mPosition.x = he + midCoord;
			} else {
				v.mPosition.z = he + midCoord;
			}
		}
	}
	void processLot(flecs::entity e, const LotContext &lotCtx,
			const RoofGenerationData &genData,
			Procedural::TriangleBuffer &tbCombined)
	{
		std::vector<RoofPlacement> roofs =
			parseRoofPlacements(lotCtx.jlot);

		for (auto &roof : roofs) {
			Procedural::TriangleBuffer tbTop;
			Procedural::TriangleBuffer tbSide;

			// Generate roof in local space
			switch (roof.type) {
			case 0:
				generateRoofType0(roof, lotCtx, tbTop, tbSide);
				break;
			case 1:
				generateRoofType1(roof, lotCtx, tbTop, tbSide);
				break;
			case 2:
				generateRoofType2(roof, lotCtx, tbTop, tbSide);
				break;
			default:
				continue;
			}

			// Transform to world space
			if (tbTop.getVertices().size() > 0) {
				transformToWorld(tbTop, lotCtx);
				clampUV(e, tbTop, "roofTop");
				tbCombined.append(tbTop);
			}
			if (tbSide.getVertices().size() > 0) {
				transformToWorld(tbSide, lotCtx);
				clampUV(e, tbSide, "roofSide");
				tbCombined.append(tbSide);
			}
		}
	}

public:
	void createTownRoofs(flecs::entity e, const nlohmann::json &jdistrict,
			     int index, Ogre::SceneNode *sceneNode,
			     Ogre::StaticGeometry *geo)
	{
		ZoneScoped;

		if (townRoofsComplete.valid() &&
		    townRoofsComplete.wait_for(std::chrono::seconds(0)) !=
			    std::future_status::ready) {
			townRoofsComplete.wait();
		}

		auto promise = std::make_shared<std::promise<bool> >();
		townRoofsComplete = promise->get_future();

		Ogre::MaterialPtr townMaterial =
			Ogre::MaterialManager::getSingleton().getByName(
				"proceduralMaterialTown" +
				Ogre::StringConverter::toString(e.id()));

		DistrictContext districtCtx =
			parseDistrictContext(jdistrict, sceneNode);

		RoofGenerationData genData{
			.entity = e,
			.material = townMaterial,
			.geo = geo,
			.districtPosition = districtCtx.centerPosition,
			.districtOrientation = districtCtx.centerOrientation,
			.districtRadius = districtCtx.radius,
			.baseMeshName =
				"roofbase_" +
				Ogre::StringConverter::toString(e.id()) + "_" +
				Ogre::StringConverter::toString(index)
		};

		Ogre::Root::getSingleton().getWorkQueue()->addTask([this,
								    jdistrict,
								    districtCtx,
								    genData,
								    promise]() {
			ZoneScoped;

			std::vector<std::pair<LotContext,
					      Procedural::TriangleBuffer> >
				lotBuffers;

			if (jdistrict.contains("lots") &&
			    jdistrict["lots"].is_array()) {
				const nlohmann::json &lots = jdistrict["lots"];
				int lotIndex = 0;

				for (const auto &jlot : lots) {
					if (!jlot.contains("roofs"))
						continue;

					LotContext lotCtx = createLotContext(
						districtCtx, jlot, lotIndex);
					Procedural::TriangleBuffer tbCombined;

					processLot(genData.entity, lotCtx,
						   genData, tbCombined);

					if (tbCombined.getVertices().size() >
					    0) {
						lotBuffers.emplace_back(
							lotCtx, tbCombined);
					}

					lotIndex++;
				}
			}

			Ogre::Root::getSingleton()
				.getWorkQueue()
				->addMainThreadTask([this, genData, lotBuffers,
						     promise]() {
					ZoneScopedN(
						"createTownRoofs::MainThread");

					for (size_t i = 0;
					     i < lotBuffers.size(); ++i) {
						std::cout
							<< i << ": vertices: "
							<< lotBuffers[i]
								   .second
								   .getVertices()
								   .size()
							<< std::endl;
					}

					int lotIndex = 0;
					for (const auto &[lotCtx, tb] :
					     lotBuffers) {
						std::string meshName =
							genData.baseMeshName +
							"_" +
							std::to_string(
								lotIndex);

						Ogre::MeshPtr existingMesh =
							Ogre::MeshManager::getSingleton()
								.getByName(
									meshName);
						if (existingMesh) {
							Ogre::MeshManager::getSingleton()
								.remove(existingMesh);
						}

						// Create mesh (vertices are already in world space)
						Ogre::MeshPtr mesh =
							tb.transformToMesh(
								meshName);
						Ogre::LodConfig config(mesh);
						setupLods(config);

						// Add to static geometry (position at origin since vertices are in world space)
						addMeshToStatic(
							genData.geo, mesh,
							genData.material,
							Ogre::Vector3::ZERO,
							Ogre::Quaternion::
								IDENTITY);

						addStaticBodyMesh(
							genData.entity, mesh,
							Ogre::Vector3::ZERO,
							Ogre::Quaternion::
								IDENTITY);

						lotIndex++;
					}
				});

			promise->set_value(true);
		});
	}
	void operator()(flecs::entity e, const nlohmann::json &jdistrict,
			int index, Ogre::SceneNode *sceneNode,
			Ogre::StaticGeometry *geo)
	{
		ZoneScoped;
		createTownRoofs(e, jdistrict, index, sceneNode, geo);
	}

	void wait()
	{
		ZoneScoped;
		if (townRoofsComplete.valid() &&
		    townRoofsComplete.wait_for(std::chrono::seconds(0)) !=
			    std::future_status::ready) {
			townRoofsComplete.wait();
		}
	}

#if 0

    void createTownRoofs(flecs::entity e, const nlohmann::json &jdistrict,
                 int index, Ogre::SceneNode *sceneNode,
                 Ogre::StaticGeometry *geo)
    {
        ZoneScoped;
        struct QueueData {
            Procedural::TriangleBuffer tb;
            Ogre::Vector3 position;
            Ogre::Quaternion rotation;
            nlohmann::json jp;
            Procedural::Shape *shape;
            Ogre::String meshName;
            Ogre::MaterialPtr material;
            Ogre::StaticGeometry *geo;
            flecs::entity e;
        };
        struct LotData {
            int width, depth;
            float angle, elevation;
            nlohmann::json jlot;
            Procedural::TriangleBuffer tb;
            Procedural::TriangleBuffer tbTop;
            Procedural::TriangleBuffer tbSide;
            struct QueueData queueData;
        };

        if (townRoofsComplete.valid() &&
            townRoofsComplete.wait_for(std::chrono::seconds(0)) !=
                std::future_status::ready)
            townRoofsComplete.wait();

        auto promise = std::make_shared<std::promise<bool> >();
        townRoofsComplete = promise->get_future();
        Ogre::MaterialPtr townMaterial;
        townMaterial = Ogre::MaterialManager::getSingleton().getByName(
            "proceduralMaterialTown" +
            Ogre::StringConverter::toString(e.id()));
        nlohmann::json jlots = nlohmann::json::array();
        if (jdistrict.find("lots") != jdistrict.end())
            jlots = jdistrict["lots"];
        Ogre::Vector3 centerPosition = sceneNode->_getDerivedPosition();
        Ogre::Quaternion centerOrientation =
            sceneNode->_getDerivedOrientation();
        const nlohmann::json &jp = jdistrict;
        Ogre::Vector3 localPosition(0, 0, 0);
        Ogre::Quaternion localRotation = Ogre::Quaternion::IDENTITY;
        float delevation = 0.0f;
        float radius = 50.0f;
        if (jp.find("elevation") != jp.end())
            delevation = jp["elevation"].get<float>();
        if (jp.find("radius") != jp.end())
            radius = jp["radius"].get<float>();
        from_json(jp["position"], localPosition);
        from_json(jp["rotation"], localRotation);
        centerPosition = centerPosition + localPosition +
                 Ogre::Vector3(0, delevation, 0);
        centerOrientation = centerOrientation * localRotation;
        static std::vector<LotData> lots;
        lots.resize(0);
        lots.reserve(jlots.size());
        {
            int count = 0;
            for (const auto &jb : jlots) {
                if (jb.find("roofs") != jb.end()) {
                    Procedural::TriangleBuffer tb;
                    Procedural::TriangleBuffer tbTop;
                    Procedural::TriangleBuffer tbSide;
                    Ogre::String meshName =
                        "roofbase" +
                        Ogre::StringConverter::toString(
                            count) +
                        "_" +
                        Ogre::StringConverter::toString(
                            e.id()) +
                        "_" +
                        Ogre::StringConverter::toString(
                            index);
                    Ogre::MeshPtr mesh =
                        Ogre::MeshManager::getSingleton()
                            .getByName(meshName);
                    if (mesh)
                        Ogre::MeshManager::getSingleton()
                            .remove(mesh);
                    int depth = 10;
                    int width = 10;
                    float angle = 0.0f;
                    float elevation = 0.0f;
                    if (jb.find("depth") != jb.end())
                        depth = jb["depth"].get<int>();
                    if (jb.find("width") != jb.end())
                        width = jb["width"].get<int>();
                    if (jb.find("angle") != jb.end())
                        angle = jb["angle"].get<float>();
                    if (jb.find("elevation") != jb.end())
                        elevation =
                            jb["elevation"]
                                .get<float>();
                    OgreAssert(width > 1 && depth > 1,
                           "Bad stuff happen");
                    if (jb.find("cells") != jb.end())
                        lots.push_back(
                            { width,
                              depth,
                              angle,
                              elevation,
                              jb,
                              tb,
                              tbTop,
                              tbSide,
                              { tb, centerPosition,
                                centerOrientation,
                                jdistrict,
                                new Procedural::Shape,
                                meshName,
                                townMaterial, geo,
                                e } });
                }
                count++;
            }
        }
        Ogre::Root::getSingleton().getWorkQueue()->addTask([radius,
                                    promise]() {
            ZoneScoped;
            for (auto &lot : lots) {
                for (auto &jroof : lot.jlot["roofs"]) {
                    int roofType = jroof["type"].get<int>();
                    int position_x =
                        jroof["position_x"].get<int>();
                    int position_y =
                        jroof["position_y"].get<int>();
                    int position_z =
                        jroof["position_z"].get<int>();
                    int width = jroof["size_x"].get<int>();
                    int depth = jroof["size_z"].get<int>();
                    float baseHeight =
                        jroof["base_height"]
                            .get<float>();
                    float maxHeight =
                        jroof["max_height"].get<float>();
                    float extend = 0.24f;
                    if (roofType == 0) {
                        Procedural::TriangleBuffer tbTop;
                        Procedural::TriangleBuffer
                            tbSide;
                        Procedural::BoxGenerator()
                            .setSizeX(2.0f *
                                  (float)width)
                            .setSizeY(baseHeight)
                            .setSizeZ(2.0f *
                                  (float)depth)
                            .setNumSegY(1)
                            .setNumSegX(1)
                            .setNumSegZ(1)
                            .setEnableNormals(true)
                            .setPosition(Ogre::Vector3(
                                (float)position_x *
                                        2.0f -
                                    1.0f +
                                    (float)width *
                                        2.0f /
                                        2.0f,
                                (float)position_y *
                                        4.0f +
                                    baseHeight /
                                        2.0f +
                                    lot.elevation,
                                (float)position_z *
                                        2.0f -
                                    1.0f +
                                    (float)depth *
                                        2.0f /
                                        2.0f))
                            .addToTriangleBuffer(
                                tbTop);
                        Procedural::BoxGenerator()
                            .setSizeX(2.0f *
                                  (float)width)
                            .setSizeY(baseHeight +
                                  extend * 2.0f)
                            .setSizeZ(extend)
                            .setNumSegY(1)
                            .setNumSegX(1)
                            .setNumSegZ(1)
                            .setEnableNormals(true)
                            .setPosition(Ogre::Vector3(
                                (float)position_x *
                                        2.0f -
                                    1.0f +
                                    (float)width *
                                        2.0f /
                                        2.0f,
                                (float)position_y *
                                        4.0f +
                                    baseHeight /
                                        2.0f +
                                    lot.elevation +
                                    extend,
                                (float)position_z *
                                        2.0f -
                                    1.0f +
                                    (float)depth *
                                        2.0f /
                                        2.0f +
                                    (float)depth *
                                        2.0f /
                                        2.0f +
                                    extend /
                                        2.0f))
                            .addToTriangleBuffer(
                                tbSide);
                        Procedural::BoxGenerator()
                            .setSizeX(2.0f *
                                  (float)width)
                            .setSizeY(baseHeight +
                                  extend * 2.0f)
                            .setSizeZ(extend)
                            .setNumSegY(1)
                            .setNumSegX(1)
                            .setNumSegZ(1)
                            .setEnableNormals(true)
                            .setPosition(Ogre::Vector3(
                                (float)position_x *
                                        2.0f -
                                    1.0f +
                                    (float)width *
                                        2.0f /
                                        2.0f,
                                (float)position_y *
                                        4.0f +
                                    baseHeight /
                                        2.0f +
                                    lot.elevation +
                                    extend,
                                (float)position_z *
                                        2.0f -
                                    1.0f +
                                    (float)depth *
                                        2.0f /
                                        2.0f -
                                    (float)depth *
                                        2.0f /
                                        2.0f -
                                    extend /
                                        2.0f))
                            .addToTriangleBuffer(
                                tbSide);
                        Procedural::BoxGenerator()
                            .setSizeX(extend)
                            .setSizeY(baseHeight +
                                  extend * 2.0f)
                            .setSizeZ(
                                2.0f * (float)depth +
                                extend * 2.0f)
                            .setNumSegY(1)
                            .setNumSegX(1)
                            .setNumSegZ(1)
                            .setEnableNormals(true)
                            .setPosition(Ogre::Vector3(
                                (float)position_x *
                                        2.0f -
                                    1.0f +
                                    (float)width *
                                        2.0f /
                                        2.0f +
                                    (float)width *
                                        2.0f /
                                        2.0f +
                                    extend /
                                        2.0f,
                                (float)position_y *
                                        4.0f +
                                    baseHeight /
                                        2.0f +
                                    lot.elevation +
                                    extend,
                                (float)position_z *
                                        2.0f -
                                    1.0f +
                                    (float)depth *
                                        2.0f /
                                        2.0f))
                            .addToTriangleBuffer(
                                tbSide);
                        Procedural::BoxGenerator()
                            .setSizeX(extend)
                            .setSizeY(baseHeight +
                                  extend * 2.0f)
                            .setSizeZ(
                                2.0f * (float)depth +
                                extend * 2.0f)
                            .setNumSegY(1)
                            .setNumSegX(1)
                            .setNumSegZ(1)
                            .setEnableNormals(true)
                            .setPosition(Ogre::Vector3(
                                (float)position_x *
                                        2.0f -
                                    1.0f +
                                    (float)width *
                                        2.0f /
                                        2.0f -
                                    (float)width *
                                        2.0f /
                                        2.0f -
                                    extend /
                                        2.0f,
                                (float)position_y *
                                        4.0f +
                                    baseHeight /
                                        2.0f +
                                    lot.elevation +
                                    extend,
                                (float)position_z *
                                        2.0f -
                                    1.0f +
                                    (float)depth *
                                        2.0f /
                                        2.0f))
                            .addToTriangleBuffer(
                                tbSide);
                        OgreAssert(
                            tbTop.getVertices()
                                    .size() >
                                8,
                            "bad box");
                        clampUV(lot.queueData.e, tbTop,
                            "roofTop");
                        clampUV(lot.queueData.e, tbSide,
                            "roofSide");
                        lot.tbTop.append(tbTop);
                        lot.tbSide.append(tbSide);
                    } else if (roofType == 1) {
                        Procedural::TriangleBuffer tbTop;
                        Procedural::TriangleBuffer
                            tbSide;
                        float q = 2.0f * (float)width /
                              2.0f;
                        float m = 1.0f;
                        float d = 1.4142f * (q + m);
                        Procedural::BoxGenerator()
                            .setSizeX(d)
                            .setSizeY(baseHeight /
                                  2.0f)
                            .setSizeZ(2.0f *
                                  (float)depth)
                            .setNumSegY(1)
                            .setNumSegX(1)
                            .setNumSegZ(1)
                            .setEnableNormals(true)
                            .setOrientation(Ogre::Quaternion(
                                Ogre::Degree(45),
                                Ogre::Vector3::
                                    NEGATIVE_UNIT_Z))
                            .setPosition(Ogre::Vector3(
                                (float)position_x *
                                        2.0f -
                                    1.0f +
                                    (float)q +
                                    q / 2.0f +
                                    m / 2.0f,
                                (float)position_y *
                                        4.0f +
                                    baseHeight /
                                        2.0f +
                                    lot.elevation +
                                    q / 2.0f -
                                    m / 2.0f +
                                    baseHeight /
                                        4.0f,
                                (float)position_z *
                                        2.0f -
                                    1.0f +
                                    (float)depth *
                                        2.0f /
                                        2.0f))
                            .addToTriangleBuffer(
                                tbTop);
                        Procedural::BoxGenerator()
                            .setSizeX(d)
                            .setSizeY(baseHeight /
                                  2.0f)
                            .setSizeZ(2.0f *
                                  (float)depth)
                            .setNumSegY(1)
                            .setNumSegX(1)
                            .setNumSegZ(1)
                            .setEnableNormals(true)
                            .setOrientation(Ogre::Quaternion(
                                Ogre::Degree(
                                    -45),
                                Ogre::Vector3::
                                    NEGATIVE_UNIT_Z))
                            .setPosition(Ogre::Vector3(
                                (float)position_x *
                                        2.0f -
                                    1.0f +
                                    q / 2.0f -
                                    m / 2.0f,
                                (float)position_y *
                                        4.0f +
                                    baseHeight /
                                        2.0f +
                                    lot.elevation +
                                    q / 2.0f -
                                    m / 2.0f +
                                    baseHeight /
                                        4.0f,
                                (float)position_z *
                                        2.0f -
                                    1.0f +
                                    (float)depth *
                                        2.0f /
                                        2.0f))
                            .addToTriangleBuffer(
                                tbTop);
                        Procedural::BoxGenerator()
                            .setSizeX(
                                2.0f * (float)width +
                                extend * 2.0f)
                            .setSizeY(q +
                                  baseHeight *
                                      2.5f)
                            .setSizeZ(extend)
                            .setNumSegX(3)
                            .setNumSegY(3)
                            .setNumSegZ(1)
                            .setEnableNormals(true)
                            .setPosition(Ogre::Vector3(
                                (float)position_x *
                                        2.0f -
                                    1.0f +
                                    q,
                                (float)position_y *
                                        4.0f +
                                    lot.elevation +
                                    q / 2.0f,
                                (float)position_z *
                                        2.0f -
                                    1.0f +
                                    (float)depth *
                                        2.0f /
                                        2.0f +
                                    (float)depth *
                                        2.0f /
                                        2.0f +
                                    extend /
                                        2.0f))
                            .addToTriangleBuffer(
                                tbSide);
                        Procedural::BoxGenerator()
                            .setSizeX(
                                2.0f * (float)width +
                                extend * 2.0f)
                            .setSizeY(q +
                                  baseHeight *
                                      2.5f)
                            .setSizeZ(extend)
                            .setNumSegX(3)
                            .setNumSegY(3)
                            .setNumSegZ(1)
                            .setEnableNormals(true)
                            .setPosition(Ogre::Vector3(
                                (float)position_x *
                                        2.0f -
                                    1.0f +
                                    q,
                                (float)position_y *
                                        4.0f +
                                    lot.elevation +
                                    q / 2.0f,
                                (float)position_z *
                                        2.0f -
                                    1.0f +
                                    (float)depth *
                                        2.0f /
                                        2.0f -
                                    (float)depth *
                                        2.0f /
                                        2.0f -
                                    extend /
                                        2.0f))
                            .addToTriangleBuffer(
                                tbSide);
                        float minY = 0.0f;
                        float maxY = 0.0f;
                        float maxX = 0.0f;
                        float minX = 0.0f;
                        int count = 0;
                        for (auto &v :
                             tbSide.getVertices()) {
                            if (count == 0) {
                                minY = v.mPosition
                                           .y;
                                maxY = v.mPosition
                                           .y;
                                minX = v.mPosition
                                           .x;
                                maxX = v.mPosition
                                           .x;
                            } else {
                                if (v.mPosition
                                        .y >
                                    maxY)
                                    maxY = v.mPosition
                                               .y;
                                if (v.mPosition
                                        .y <
                                    minY)
                                    minY = v.mPosition
                                               .y;
                                if (v.mPosition
                                        .x >
                                    maxX)
                                    maxX = v.mPosition
                                               .x;
                                if (v.mPosition
                                        .x <
                                    minX)
                                    minX = v.mPosition
                                               .x;
                            }
                            count++;
                        }
                        float midX =
                            minX +
                            (maxX - minX) / 2.0f;
                        for (auto &v :
                             tbSide.getVertices()) {
                            float md =
                                maxY -
                                v.mPosition.y;
                            float hm =
                                v.mPosition.x -
                                midX;
                            float he =
                                (hm / ((maxX -
                                    minX) /
                                       2.0f)) *
                                md;
                            v.mPosition.x =
                                he + midX;
                        }
                        clampUV(lot.queueData.e, tbTop,
                            "roofTop");
                        clampUV(lot.queueData.e, tbSide,
                            "roofSide");
                        lot.tbTop.append(tbTop);
                        lot.tbTop.append(tbSide);
                    } else if (roofType == 2) {
                        Procedural::TriangleBuffer tbTop;
                        Procedural::TriangleBuffer
                            tbSide;
                        float q = 2.0f * (float)depth /
                              2.0f;
                        float m = 1.0f;
                        float d = 1.4142f * (q + m);
                        Procedural::BoxGenerator()
                            .setSizeX(2.0f *
                                  (float)width)
                            .setSizeY(baseHeight /
                                  2.0f)
                            .setSizeZ(d)
                            .setNumSegY(1)
                            .setNumSegX(1)
                            .setNumSegZ(1)
                            .setEnableNormals(true)
                            .setOrientation(Ogre::Quaternion(
                                Ogre::Degree(45),
                                Ogre::Vector3::
                                    NEGATIVE_UNIT_X))
                            .setPosition(Ogre::Vector3(
                                (float)position_x *
                                        2.0f -
                                    1.0f +
                                    (float)width *
                                        2.0f /
                                        2.0f,
                                (float)position_y *
                                        4.0f +
                                    baseHeight /
                                        2.0f +
                                    lot.elevation +
                                    q / 2.0f -
                                    m / 2.0f +
                                    baseHeight /
                                        4.0f,
                                (float)position_z *
                                        2.0f -
                                    1.0f +
                                    q / 2.0f -
                                    m / 2.0f))
                            .addToTriangleBuffer(
                                tbTop);
                        Procedural::BoxGenerator()
                            .setSizeX(2.0f *
                                  (float)width)
                            .setSizeY(baseHeight /
                                  2.0f)
                            .setSizeZ(d)
                            .setNumSegX(1)
                            .setNumSegY(1)
                            .setNumSegZ(1)
                            .setEnableNormals(true)
                            .setOrientation(Ogre::Quaternion(
                                Ogre::Degree(
                                    -45),
                                Ogre::Vector3::
                                    NEGATIVE_UNIT_X))
                            .setPosition(Ogre::Vector3(
                                (float)position_x *
                                        2.0f -
                                    1.0f +
                                    (float)width *
                                        2.0f /
                                        2.0f,
                                (float)position_y *
                                        4.0f +
                                    baseHeight /
                                        2.0f +
                                    lot.elevation +
                                    q / 2.0f -
                                    m / 2.0f +
                                    baseHeight /
                                        4.0f,
                                (float)position_z *
                                        2.0f -
                                    1.0f +
                                    q +
                                    q / 2.0f +
                                    m / 2.0f))
                            .addToTriangleBuffer(
                                tbTop);
                        Procedural::BoxGenerator()
                            .setSizeX(extend)
                            .setSizeY(q +
                                  baseHeight *
                                      2.5f)
                            .setSizeZ(
                                2.0f * (float)depth +
                                2.0f * baseHeight *
                                    3.0f)
                            .setNumSegX(2)
                            .setNumSegY(2)
                            .setNumSegZ(1)
                            .setEnableNormals(true)
                            .setPosition(Ogre::Vector3(
                                (float)position_x *
                                        2.0f -
                                    1.0f +
                                    (float)width *
                                        2.0f /
                                        2.0f +
                                    (float)width *
                                        2.0f /
                                        2.0f +
                                    extend /
                                        2.0f,
                                (float)position_y *
                                        4.0f +
                                    lot.elevation +
                                    q / 2.0f,
                                (float)position_z *
                                        2.0f -
                                    1.0f +
                                    (float)depth *
                                        2.0f /
                                        2.0f))
                            .addToTriangleBuffer(
                                tbSide);
                        Procedural::BoxGenerator()
                            .setSizeX(extend)
                            .setSizeY(q +
                                  baseHeight *
                                      2.5f)
                            .setSizeZ(
                                2.0f * (float)depth +
                                2.0f * baseHeight *
                                    3.0f)
                            .setNumSegX(2)
                            .setNumSegY(2)
                            .setNumSegZ(1)
                            .setEnableNormals(true)
                            .setPosition(Ogre::Vector3(
                                (float)position_x *
                                        2.0f -
                                    1.0f +
                                    (float)width *
                                        2.0f /
                                        2.0f -
                                    (float)width *
                                        2.0f /
                                        2.0f -
                                    extend /
                                        2.0f,
                                (float)position_y *
                                        4.0f +
                                    lot.elevation +
                                    q / 2.0f,
                                (float)position_z *
                                        2.0f -
                                    1.0f +
                                    (float)depth *
                                        2.0f /
                                        2.0f))
                            .addToTriangleBuffer(
                                tbSide);
                        float minY = 0.0f;
                        float maxY = 0.0f;
                        float maxZ = 0.0f;
                        float minZ = 0.0f;
                        int count = 0;
                        for (auto &v :
                             tbSide.getVertices()) {
                            if (count == 0) {
                                minY = v.mPosition
                                           .y;
                                maxY = v.mPosition
                                           .y;
                                minZ = v.mPosition
                                           .z;
                                maxZ = v.mPosition
                                           .z;
                            } else {
                                if (v.mPosition
                                        .y >
                                    maxY)
                                    maxY = v.mPosition
                                               .y;
                                if (v.mPosition
                                        .y <
                                    minY)
                                    minY = v.mPosition
                                               .y;
                                if (v.mPosition
                                        .z >
                                    maxZ)
                                    maxZ = v.mPosition
                                               .z;
                                if (v.mPosition
                                        .z <
                                    minZ)
                                    minZ = v.mPosition
                                               .z;
                            }
                            count++;
                        }
                        float midZ =
                            minZ +
                            (maxZ - minZ) / 2.0f;
                        for (auto &v :
                             tbSide.getVertices()) {
                            float md =
                                maxY -
                                v.mPosition.y;
                            float hm =
                                v.mPosition.z -
                                midZ;
                            float he =
                                (hm / ((maxZ -
                                    minZ) /
                                       2.0f)) *
                                md;
                            v.mPosition.z =
                                he + midZ;
                        }
                        clampUV(lot.queueData.e, tbTop,
                            "roofTop");
                        clampUV(lot.queueData.e, tbSide,
                            "roofSide");
                        lot.tbTop.append(tbTop);
                        lot.tbSide.append(tbSide);
                    }
                }
                if (lot.tbTop.getVertices().size() > 0)
                    lot.tb.append(lot.tbTop);
                if (lot.tbSide.getVertices().size() > 0)
                    lot.tb.append(lot.tbSide);
            }
            Ogre::Root::getSingleton().getWorkQueue()->addMainThreadTask([radius,
                                              promise]() {
                ZoneScopedN("createTownRoofs::MainThread");
                {
                    int count = 0;
                    for (auto lot : lots) {
                        std::cout
                            << count
                            << ": vertices: "
                            << lot.tb.getVertices()
                                   .size()
                            << " "
                            << lot.tbSide
                                   .getVertices()
                                   .size()
                            << std::endl;
                        count++;
                    }
                }
                for (auto lot : lots) {
                    {
                        float distance = radius;
                        Ogre::Quaternion rotation =
                            Ogre::Quaternion(
                                Ogre::Degree(
                                    lot.angle),
                                Ogre::Vector3::
                                    UNIT_Y);
                        Ogre::Vector3 offset =
                            lot.queueData.rotation *
                            rotation *
                            (Ogre::Vector3::UNIT_Z *
                             distance);
                        Ogre::MeshPtr mesh =
                            Ogre::MeshManager::getSingleton()
                                .getByName(
                                    lot.queueData
                                        .meshName);
                        if (mesh)
                            Ogre::MeshManager::
                                getSingleton()
                                    .remove(mesh);
                        if (lot.tb.getVertices().size() >
                            0) {
                            mesh = lot.tb.transformToMesh(
                                lot.queueData
                                    .meshName);
                            Ogre::LodConfig config(
                                mesh);
                            setupLods(config);
                            addMeshToStatic(
                                lot.queueData
                                    .geo,
                                mesh,
                                lot.queueData
                                    .material,
                                lot.queueData.position +
                                    offset,
                                lot.queueData.rotation *
                                    rotation);
                            addStaticBodyMesh(
                                lot.queueData.e,
                                mesh,
                                lot.queueData.position +
                                    offset,
                                lot.queueData.rotation *
                                    rotation);
                        }
                    }
                }
            });
            promise->set_value(true);
        });
    }
#else
#if 0
    void createTownRoofs(flecs::entity e, const nlohmann::json &jdistrict,
                 int index, Ogre::SceneNode *sceneNode,
                 Ogre::StaticGeometry *geo)
    {
        ZoneScoped;

        // Wait for previous task
        if (townRoofsComplete.valid() &&
            townRoofsComplete.wait_for(std::chrono::seconds(0)) !=
                std::future_status::ready) {
            townRoofsComplete.wait();
        }

        // Setup async task
        auto promise = std::make_shared<std::promise<bool> >();
        townRoofsComplete = promise->get_future();

        // Get town material
        Ogre::MaterialPtr townMaterial =
            Ogre::MaterialManager::getSingleton().getByName(
                "proceduralMaterialTown" +
                Ogre::StringConverter::toString(e.id()));

        // Parse district context
        DistrictContext districtCtx =
            parseDistrictContext(jdistrict, sceneNode);

        // Prepare roof generation data
        RoofGenerationData genData{
            .entity = e,
            .material = townMaterial,
            .geo = geo,
            .districtPosition = districtCtx.centerPosition,
            .districtOrientation = districtCtx.centerOrientation,
            .districtRadius = districtCtx.radius,
            .baseMeshName =
                "roofbase_" +
                Ogre::StringConverter::toString(e.id()) + "_" +
                Ogre::StringConverter::toString(index)
        };

        // Process lots in background
        Ogre::Root::getSingleton().getWorkQueue()->addTask([this,
                                    jdistrict,
                                    districtCtx,
                                    genData,
                                    promise]() {
            ZoneScoped;

            std::vector<std::pair<LotContext,
                          Procedural::TriangleBuffer> >
                lotBuffers;

            if (jdistrict.contains("lots") &&
                jdistrict["lots"].is_array()) {
                const nlohmann::json &lots = jdistrict["lots"];
                int lotIndex = 0;

                for (const auto &jlot : lots) {
                    if (!jlot.contains("roofs"))
                        continue;

                    LotContext lotCtx = createLotContext(
                        districtCtx, jlot, lotIndex);
                    Procedural::TriangleBuffer tbCombined;

                    processLot(genData.entity, lotCtx,
                           genData, tbCombined);

                    if (tbCombined.getVertices().size() >
                        0) {
                        lotBuffers.emplace_back(
                            lotCtx, tbCombined);
                    }

                    lotIndex++;
                }
            }

            // Schedule main thread task for mesh creation
            Ogre::Root::getSingleton()
                .getWorkQueue()
                ->addMainThreadTask([this, genData, lotBuffers,
                             promise]() {
                    ZoneScopedN(
                        "createTownRoofs::MainThread");

                    // Debug output
                    for (size_t i = 0;
                         i < lotBuffers.size(); ++i) {
                        std::cout
                            << i << ": vertices: "
                            << lotBuffers[i]
                                   .second
                                   .getVertices()
                                   .size()
                            << std::endl;
                    }

                    // Create meshes and add to static geometry
                    int lotIndex = 0;
                    for (const auto &[lotCtx, tb] :
                         lotBuffers) {
                        std::string meshName =
                            genData.baseMeshName +
                            "_" +
                            std::to_string(
                                lotIndex);

                        // Remove existing mesh if any
                        Ogre::MeshPtr existingMesh =
                            Ogre::MeshManager::getSingleton()
                                .getByName(
                                    meshName);
                        if (existingMesh) {
                            Ogre::MeshManager::getSingleton()
                                .remove(existingMesh);
                        }

                        // Create new mesh
                        Ogre::MeshPtr mesh =
                            tb.transformToMesh(
                                meshName);
                        Ogre::LodConfig config(mesh);
                        setupLods(config);

                        // Calculate final world transform
                        Ogre::Quaternion lotRotation(
                            Ogre::Degree(
                                lotCtx.angle),
                            Ogre::Vector3::UNIT_Y);
                        Ogre::Vector3 offset =
                            genData.districtOrientation *
                            lotRotation *
                            (Ogre::Vector3::UNIT_Z *
                             genData.districtRadius);

                        // Add to static geometry and physics
                        addMeshToStatic(
                            genData.geo, mesh,
                            genData.material,
                            genData.districtPosition +
                                offset,
                            genData.districtOrientation *
                                lotRotation);

                        addStaticBodyMesh(
                            genData.entity, mesh,
                            genData.districtPosition +
                                offset,
                            genData.districtOrientation *
                                lotRotation);

                        lotIndex++;
                    }
                });

            promise->set_value(true);
        });
    }
#endif

#endif
};

struct TownDecorateWindows : TownTask {
	std::shared_future<bool> townDecorateWindowsComplete;

private:
	struct DistrictContext {
		Ogre::Vector3 centerPosition;
		Ogre::Quaternion centerOrientation;
		float radius;
	};
	struct LotContext {
		Ogre::Vector3 worldPosition;
		Ogre::Quaternion worldOrientation;
	};
	struct CellInfo {
		int x, y, z;
		uint64_t flags;
		Ogre::Vector3 offsetX;
		Ogre::Vector3 offsetY;
		Ogre::Vector3 offsetZ;
	};
	enum class WindowDirection {
		PositiveZ,
		NegativeZ,
		PositiveX,
		NegativeX
	};

	struct WindowPlacement {
		WindowDirection direction;
		Ogre::Vector3 position;
		Ogre::Quaternion orientation;
		bool valid;
	};

	// Constants
	static constexpr float OUT_OFFSET = 1.05f;
	static constexpr float WINDOW_VERTICAL_OFFSET = 0.8f;
	static constexpr float CELL_WIDTH = 2.0f;
	static constexpr float CELL_HEIGHT = 4.0f;
	static constexpr float CELL_DEPTH = 2.0f;
	DistrictContext parseDistrictContext(const nlohmann::json &jp,
					     Ogre::SceneNode *sceneNode)
	{
		DistrictContext ctx;

		// Parse base position and rotation
		Ogre::Vector3 localPosition(0, 0, 0);
		Ogre::Quaternion localRotation = Ogre::Quaternion::IDENTITY;
		from_json(jp["position"], localPosition);
		from_json(jp["rotation"], localRotation);

		// Parse elevation and radius
		float elevation = jp.value("elevation", 0.0f);
		ctx.radius = jp.value("radius", 50.0f);

		// Calculate world transforms for district center
		ctx.centerPosition = sceneNode->_getDerivedPosition() +
				     localPosition +
				     Ogre::Vector3(0, elevation, 0);
		ctx.centerOrientation =
			sceneNode->_getDerivedOrientation() * localRotation;

		return ctx;
	}
	LotContext createLotContext(const DistrictContext &districtCtx,
				    const nlohmann::json &jlot)
	{
		LotContext lot;

		// Parse lot properties
		float angle = jlot.value("angle", 0.0f);
		float elevation = jlot.value("elevation", 0.0f);

		// Calculate lot rotation and position (offset from district center)
		Ogre::Quaternion lotRotation(Ogre::Degree(angle),
					     Ogre::Vector3::UNIT_Y);
		Ogre::Vector3 offset =
			districtCtx.centerOrientation * lotRotation *
			(Ogre::Vector3::UNIT_Z * districtCtx.radius);

		// Lot world position = district center + offset + lot elevation
		lot.worldPosition = districtCtx.centerPosition + offset +
				    Ogre::Vector3(0, elevation, 0);

		// Lot orientation = district orientation * lot rotation
		lot.worldOrientation =
			districtCtx.centerOrientation * lotRotation;

		return lot;
	}
	CellInfo createCellInfo(const nlohmann::json &jcell,
				const LotContext &lotCtx)
	{
		CellInfo cell;
		cell.x = jcell.value("x", 0);
		cell.y = jcell.value("y", 0);
		cell.z = jcell.value("z", 0);
		cell.flags = jcell.value("flags", 0ULL);

		// Calculate cell offsets
		cell.offsetX = lotCtx.worldOrientation * Ogre::Vector3::UNIT_X *
			       static_cast<float>(cell.x) * CELL_WIDTH;
		cell.offsetZ = lotCtx.worldOrientation * Ogre::Vector3::UNIT_Z *
			       static_cast<float>(cell.z) * CELL_DEPTH;
		cell.offsetY = Ogre::Vector3(0, cell.y * CELL_HEIGHT, 0);

		return cell;
	}
	std::vector<WindowPlacement>
	checkForWindows(const nlohmann::json &jcell, const CellInfo &cell,
			const LotContext &lotCtx)
	{
		std::vector<WindowPlacement> windows;

		// Helper lambda to create window placement
		auto addWindow = [&](WindowDirection dir,
				     const Ogre::Vector3 &offsetDir,
				     float rotationDegrees) {
			WindowPlacement window;
			window.direction = dir;
			window.valid = true;

			// Calculate offset with vertical adjustment
			Ogre::Vector3 offset =
				lotCtx.worldOrientation * offsetDir *
					OUT_OFFSET +
				Ogre::Vector3::UNIT_Y * WINDOW_VERTICAL_OFFSET;

			window.position = lotCtx.worldPosition + cell.offsetX +
					  cell.offsetZ + cell.offsetY + offset;

			window.orientation =
				lotCtx.worldOrientation *
				Ogre::Quaternion(Ogre::Degree(rotationDegrees),
						 Ogre::Vector3::UNIT_Y);
			windows.push_back(window);
		};

		// Check each window direction using the original jcell JSON object
		if (Items::CellsScript::isBit(jcell, "windowz+")) {
			addWindow(WindowDirection::PositiveZ,
				  Ogre::Vector3::UNIT_Z, 0.0f);
		}
		if (Items::CellsScript::isBit(jcell, "windowz-")) {
			addWindow(WindowDirection::NegativeZ,
				  Ogre::Vector3::NEGATIVE_UNIT_Z, 180.0f);
		}
		if (Items::CellsScript::isBit(jcell, "windowx+")) {
			addWindow(WindowDirection::PositiveX,
				  Ogre::Vector3::UNIT_X, 90.0f);
		}
		if (Items::CellsScript::isBit(jcell, "windowx-")) {
			addWindow(WindowDirection::NegativeX,
				  Ogre::Vector3::NEGATIVE_UNIT_X, -90.0f);
		}

		return windows;
	}

	void placeWindow(Ogre::StaticGeometry *geo,
			 const WindowPlacement &window,
			 Ogre::MaterialPtr townMaterial)
	{
		// Create window entity
		Ogre::Entity *ent =
			ECS::get<EngineData>().mScnMgr->createEntity(
				"window-cell", "window-frame1");
		ent->setMaterial(townMaterial);

		// Add to static geometry
		geo->addEntity(ent, window.position, window.orientation);

		// Clean up
		ECS::get<EngineData>().mScnMgr->destroyEntity(ent);
	}
	void processLot(flecs::entity e, const nlohmann::json &jlot,
			const DistrictContext &districtCtx,
			Ogre::StaticGeometry *geo,
			Ogre::MaterialPtr townMaterial)
	{
		// Validate lot dimensions
		int depth = jlot.value("depth", 10);
		int width = jlot.value("width", 10);
		OgreAssert(width > 1 && depth > 1, "Invalid lot dimensions");

		// Create lot context with position offset from district center
		LotContext lotCtx = createLotContext(districtCtx, jlot);

		// Process cells
		if (jlot.contains("cells") && jlot["cells"].is_array()) {
			const nlohmann::json &cells = jlot["cells"];
			for (nlohmann::json::const_iterator it = cells.begin();
			     it != cells.end(); ++it) {
				const nlohmann::json &jcell = *it;

				CellInfo cell = createCellInfo(jcell, lotCtx);
				std::vector<WindowPlacement> windows =
					checkForWindows(jcell, cell, lotCtx);

				for (const auto &window : windows) {
					placeWindow(geo, window, townMaterial);
				}
			}
		}
	}

public:
	void createDecorateWindows(flecs::entity e,
				   const nlohmann::json &jdistrict,
				   int /*index*/, Ogre::SceneNode *sceneNode,
				   Ogre::StaticGeometry *geo)
	{
		ZoneScoped;
		// FIXME: make window placement a background task.

		// Create town material
		Ogre::MaterialPtr townMaterial = createTownMaterial(e);

		// Parse district context (center point)
		DistrictContext districtCtx =
			parseDistrictContext(jdistrict, sceneNode);

		// Process lots if they exist
		if (jdistrict.contains("lots") &&
		    jdistrict["lots"].is_array()) {
			const nlohmann::json &lots = jdistrict["lots"];
			for (nlohmann::json::const_iterator it = lots.begin();
			     it != lots.end(); ++it) {
				const nlohmann::json &jlot = *it;
				std::cout << jlot.dump() << std::endl;
				processLot(e, jlot, districtCtx, geo,
					   townMaterial);
			}
		}
	}
	void operator()(flecs::entity e, const nlohmann::json &jdistrict,
			int index, Ogre::SceneNode *sceneNode,
			Ogre::StaticGeometry *geo)
	{
		ZoneScoped;
		createDecorateWindows(e, jdistrict, index, sceneNode, geo);
	}
	void wait()
	{
		ZoneScoped;
		if (townDecorateWindowsComplete.valid() &&
		    townDecorateWindowsComplete.wait_for(std::chrono::seconds(
			    0)) != std::future_status::ready)
			townDecorateWindowsComplete.wait();
	}
};
struct TownDecorateDoors : TownTask {
	std::shared_future<bool> townDecorateDoorsComplete;

private:
	struct DistrictContext {
		Ogre::Vector3 centerPosition;
		Ogre::Quaternion centerOrientation;
		float radius;
	};

	struct LotContext {
		Ogre::Vector3 worldPosition;
		Ogre::Quaternion worldOrientation;
	};

	struct CellInfo {
		int x, y, z;
		uint64_t flags;
		Ogre::Vector3 offsetX;
		Ogre::Vector3 offsetY;
		Ogre::Vector3 offsetZ;
	};

	enum class DoorType { External, Internal };

	enum class DoorDirection { PositiveZ, NegativeZ, PositiveX, NegativeX };

	struct DoorPlacement {
		DoorType type;
		DoorDirection direction;
		Ogre::Vector3 position;
		Ogre::Quaternion orientation;
		bool valid;
	};
	DistrictContext parseDistrictContext(const nlohmann::json &jp,
					     Ogre::SceneNode *sceneNode)
	{
		DistrictContext ctx;

		// Parse base position and rotation
		Ogre::Vector3 localPosition(0, 0, 0);
		Ogre::Quaternion localRotation = Ogre::Quaternion::IDENTITY;
		from_json(jp["position"], localPosition);
		from_json(jp["rotation"], localRotation);

		// Parse elevation and radius
		float elevation = jp.value("elevation", 0.0f);
		ctx.radius = jp.value("radius", 50.0f);

		// Calculate world transforms for district center
		ctx.centerPosition = sceneNode->_getDerivedPosition() +
				     localPosition +
				     Ogre::Vector3(0, elevation, 0);
		ctx.centerOrientation =
			sceneNode->_getDerivedOrientation() * localRotation;

		return ctx;
	}
	LotContext createLotContext(const DistrictContext &districtCtx,
				    const nlohmann::json &jlot)
	{
		LotContext lot;

		// Parse lot properties
		float angle = jlot.value("angle", 0.0f);
		float elevation = jlot.value("elevation", 0.0f);

		// Calculate lot rotation and position (offset from district center)
		Ogre::Quaternion lotRotation(Ogre::Degree(angle),
					     Ogre::Vector3::UNIT_Y);
		Ogre::Vector3 offset =
			districtCtx.centerOrientation * lotRotation *
			(Ogre::Vector3::UNIT_Z * districtCtx.radius);

		// Lot world position = district center + offset + lot elevation
		lot.worldPosition = districtCtx.centerPosition + offset +
				    Ogre::Vector3(0, elevation, 0);

		// Lot orientation = district orientation * lot rotation
		lot.worldOrientation =
			districtCtx.centerOrientation * lotRotation;

		return lot;
	}
	CellInfo createCellInfo(const nlohmann::json &jcell,
				const LotContext &lotCtx)
	{
		CellInfo cell;
		cell.x = jcell.value("x", 0);
		cell.y = jcell.value("y", 0);
		cell.z = jcell.value("z", 0);
		cell.flags = jcell.value("flags", 0ULL);

		// Calculate cell offsets
		cell.offsetX = lotCtx.worldOrientation * Ogre::Vector3::UNIT_X *
			       static_cast<float>(cell.x) * 2.0f;
		cell.offsetZ = lotCtx.worldOrientation * Ogre::Vector3::UNIT_Z *
			       static_cast<float>(cell.z) * 2.0f;
		cell.offsetY = Ogre::Vector3(0, cell.y * 4.0f, 0);

		return cell;
	}
	std::vector<DoorPlacement> checkForDoors(const nlohmann::json &jcell,
						 const CellInfo &cell,
						 const LotContext &lotCtx)
	{
		std::vector<DoorPlacement> doors;
		const float outOffset = 1.05f;
		auto addDoor = [&](DoorType type, DoorDirection dir,
				   const Ogre::Vector3 &offsetDir,
				   float rotationDegrees) {
			DoorPlacement door;
			door.type = type;
			door.direction = dir;
			door.valid = true;

			// Calculate offset and rotation
			Ogre::Vector3 offset =
				lotCtx.worldOrientation * offsetDir * outOffset;
			door.position = lotCtx.worldPosition + cell.offsetX +
					cell.offsetZ + cell.offsetY + offset;
			door.orientation =
				lotCtx.worldOrientation *
				Ogre::Quaternion(Ogre::Degree(rotationDegrees),
						 Ogre::Vector3::UNIT_Y);
			doors.push_back(door);
		};
		if (Items::CellsScript::isBit(jcell, "doorz+")) {
			addDoor(DoorType::External, DoorDirection::PositiveZ,
				Ogre::Vector3::UNIT_Z, 0.0f);
		}
		if (Items::CellsScript::isBit(jcell, "idoorz+")) {
			addDoor(DoorType::Internal, DoorDirection::PositiveZ,
				Ogre::Vector3::UNIT_Z, 0.0f);
		}
		if (Items::CellsScript::isBit(jcell, "doorz-")) {
			addDoor(DoorType::External, DoorDirection::NegativeZ,
				Ogre::Vector3::NEGATIVE_UNIT_Z, 180.0f);
		}
		if (Items::CellsScript::isBit(jcell, "idoorz-")) {
			addDoor(DoorType::Internal, DoorDirection::NegativeZ,
				Ogre::Vector3::NEGATIVE_UNIT_Z, 180.0f);
		}
		if (Items::CellsScript::isBit(jcell, "doorx+")) {
			addDoor(DoorType::External, DoorDirection::PositiveX,
				Ogre::Vector3::UNIT_X, 90.0f);
		}
		if (Items::CellsScript::isBit(jcell, "idoorx+")) {
			addDoor(DoorType::Internal, DoorDirection::PositiveX,
				Ogre::Vector3::UNIT_X, 90.0f);
		}
		if (Items::CellsScript::isBit(jcell, "doorx-")) {
			addDoor(DoorType::External, DoorDirection::NegativeX,
				Ogre::Vector3::NEGATIVE_UNIT_X, -90.0f);
		}
		if (Items::CellsScript::isBit(jcell, "idoorx-")) {
			addDoor(DoorType::Internal, DoorDirection::NegativeX,
				Ogre::Vector3::NEGATIVE_UNIT_X, -90.0f);
		}

		return doors;
	}

	void placeDoor(Ogre::StaticGeometry *geo, const DoorPlacement &door,
		       Ogre::MaterialPtr townMaterial)
	{
		const char *meshName = (door.type == DoorType::External) ?
					       "external-door-frame1" :
					       "internal-door-frame1";

		if (door.direction == DoorDirection::PositiveZ ||
		    door.direction == DoorDirection::NegativeZ) {
			// Use entity approach for Z-axis doors (original code used this)
			const char *entityName =
				(door.type == DoorType::External) ?
					"door-cell" :
					"idoor-cell";

			Ogre::Entity *ent =
				ECS::get<EngineData>().mScnMgr->createEntity(
					entityName, meshName);
			ent->setMaterial(townMaterial);

			geo->addEntity(ent, door.position, door.orientation);
			ECS::get<EngineData>().mScnMgr->destroyEntity(ent);
		} else {
			// Use mesh approach for X-axis doors (original code used addMeshToStatic)
			Ogre::MeshPtr mesh =
				Ogre::MeshManager::getSingleton().getByName(
					meshName);
			if (mesh) {
				addMeshToStatic(geo, mesh, townMaterial,
						door.position,
						door.orientation);
			}
		}
	}
	void processLot(flecs::entity e, const nlohmann::json &jlot,
			const DistrictContext &districtCtx,
			Ogre::StaticGeometry *geo,
			Ogre::MaterialPtr townMaterial)
	{
		// Validate lot dimensions
		int depth = jlot.value("depth", 10);
		int width = jlot.value("width", 10);
		OgreAssert(width > 1 && depth > 1, "Invalid lot dimensions");

		// Create lot context with position offset from district center
		LotContext lotCtx = createLotContext(districtCtx, jlot);

		// Process cells
		if (jlot.contains("cells") && jlot["cells"].is_array()) {
			const nlohmann::json &cells = jlot["cells"];
			for (nlohmann::json::const_iterator it = cells.begin();
			     it != cells.end(); ++it) {
				const nlohmann::json &jcell = *it;

				CellInfo cell = createCellInfo(jcell, lotCtx);
				std::vector<DoorPlacement> doors =
					checkForDoors(jcell, cell, lotCtx);

				for (const auto &door : doors) {
					placeDoor(geo, door, townMaterial);
				}
			}
		}
	}

public:
	void createDecorateDoors(flecs::entity e,
				 const nlohmann::json &jdistrict, int /*index*/,
				 Ogre::SceneNode *sceneNode,
				 Ogre::StaticGeometry *geo)
	{
		ZoneScoped;
		// FIXME: make door placement a background task.

		// Create town material
		Ogre::MaterialPtr townMaterial = createTownMaterial(e);

		// Parse district context (center point)
		DistrictContext districtCtx =
			parseDistrictContext(jdistrict, sceneNode);

		// Process lots if they exist
		if (jdistrict.contains("lots") &&
		    jdistrict["lots"].is_array()) {
			const nlohmann::json &lots = jdistrict["lots"];
			for (nlohmann::json::const_iterator it = lots.begin();
			     it != lots.end(); ++it) {
				const nlohmann::json &jlot = *it;
				std::cout << jlot.dump() << std::endl;
				processLot(e, jlot, districtCtx, geo,
					   townMaterial);
			}
		}
	}
#if 0
    void createDecorateDoors(flecs::entity e,
                 const nlohmann::json &jdistrict, int index,
                 Ogre::SceneNode *sceneNode,
                 Ogre::StaticGeometry *geo)
    {
        ZoneScoped;
        // FIXME: make furniture placement a background task.
        Ogre::MaterialPtr townMaterial = createTownMaterial(e);
        const nlohmann::json &jp = jdistrict;
        nlohmann::json jlots = nlohmann::json::array();
        float baseHeight = 4.0f;
        Ogre::Vector3 localPosition(0, 0, 0);
        Ogre::Quaternion localRotation = Ogre::Quaternion::IDENTITY;
        Ogre::Vector3 centerPosition = sceneNode->_getDerivedPosition();
        Ogre::Quaternion centerOrientation =
            sceneNode->_getDerivedOrientation();
        float delevation = 0.0f;
        float radius = 50.0f;
        if (jp.find("elevation") != jp.end())
            delevation = jp["elevation"].get<float>();
        if (jp.find("radius") != jp.end())
            radius = jp["radius"].get<float>();
        from_json(jp["position"], localPosition);
        from_json(jp["rotation"], localRotation);
        centerPosition = centerPosition + localPosition +
                 Ogre::Vector3(0, delevation, 0);
        centerOrientation = centerOrientation * localRotation;
        if (jdistrict.find("lots") != jdistrict.end())
            jlots = jdistrict["lots"];
        for (const auto &jb : jlots) {
            float angle = 0.0f;
            int depth = 10;
            int width = 10;
            float distance = radius;
            float elevation = 0.0f;
            std::cout << jb.dump() << std::endl;
            if (jb.find("angle") != jb.end())
                angle = jb["angle"].get<float>();
            if (jb.find("depth") != jb.end())
                depth = jb["depth"].get<int>();
            if (jb.find("width") != jb.end())
                width = jb["width"].get<int>();
            if (jb.find("elevation") != jb.end())
                elevation = jb["elevation"].get<float>();

            OgreAssert(width > 1 && depth > 1 && baseHeight > 1,
                   "Bad stuff happen");

            Ogre::Quaternion rotation = Ogre::Quaternion(
                Ogre::Degree(angle), Ogre::Vector3::UNIT_Y);
            Ogre::Vector3 offset =
                centerOrientation * rotation *
                (Ogre::Vector3::UNIT_Z * distance);
            Ogre::Vector3 worldCenterPosition =
                centerPosition + offset +
                Ogre::Vector3(0, elevation, 0);
            Ogre::Quaternion worldCenterOrientation =
                centerOrientation * rotation;
            float outOffset = 1.05f;
            if (jb.find("cells") != jb.end()) {
                for (auto &jcell : jb["cells"]) {
                    int x = jcell["x"].get<int>();
                    int y = jcell["y"].get<int>();
                    int z = jcell["z"].get<int>();
                    uint64_t flags =
                        jcell["flags"].get<uint64_t>();
                    Ogre::Vector3 cellOffset(
                        x * 2.0f, y * 4.0f, z * 2.0f);
                    Ogre::Vector3 offsetX =
                        worldCenterOrientation *
                        Ogre::Vector3::UNIT_X *
                        (float)x * 2.0f;
                    Ogre::Vector3 offsetZ =
                        worldCenterOrientation *
                        Ogre::Vector3::UNIT_Z *
                        (float)z * 2.0f;
                    Ogre::Vector3 offsetY(0, y * 4.0f, 0);
                    if (Items::CellsScript::isBit(
                            jcell, "doorz+")) {
                        Ogre::Entity *ent =
                            ECS::get<EngineData>()
                                .mScnMgr
                                ->createEntity(
                                    "door-cell",
                                    "external-door-frame1");
                        ent->setMaterial(townMaterial);
                        Ogre::Vector3 offset =
                            worldCenterOrientation *
                            Ogre::Vector3::UNIT_Z *
                            outOffset;
                        geo->addEntity(
                            ent,
                            worldCenterPosition +
                                offsetX +
                                offsetZ +
                                offsetY +
                                offset,
                            worldCenterOrientation);
                        ECS::get<EngineData>()
                            .mScnMgr->destroyEntity(
                                ent);
                    } else if (Items::CellsScript::isBit(
                               jcell, "idoorz+")) {
                        Ogre::Entity *ent =
                            ECS::get<EngineData>()
                                .mScnMgr
                                ->createEntity(
                                    "idoor-cell",
                                    "internal-door-frame1");
                        ent->setMaterial(townMaterial);
                        Ogre::Vector3 offset =
                            worldCenterOrientation *
                            Ogre::Vector3::UNIT_Z *
                            outOffset;
                        geo->addEntity(
                            ent,
                            worldCenterPosition +
                                offsetX +
                                offsetZ +
                                offsetY +
                                offset,
                            worldCenterOrientation);
                        ECS::get<EngineData>()
                            .mScnMgr->destroyEntity(
                                ent);
                    }
                    if (Items::CellsScript::isBit(
                            jcell, "doorz-")) {
                        Ogre::Entity *ent =
                            ECS::get<EngineData>()
                                .mScnMgr
                                ->createEntity(
                                    "door-cell",
                                    "external-door-frame1");
                        ent->setMaterial(townMaterial);
                        Ogre::Vector3 offset =
                            worldCenterOrientation *
                            Ogre::Vector3::
                                NEGATIVE_UNIT_Z *
                            outOffset;
                        Ogre::Quaternion rotation =
                            worldCenterOrientation *
                            Ogre::Quaternion(
                                Ogre::Degree(
                                    180),
                                Ogre::Vector3::
                                    UNIT_Y);
                        geo->addEntity(
                            ent,
                            worldCenterPosition +
                                offsetX +
                                offsetZ +
                                offsetY +
                                offset,
                            rotation);
                        ECS::get<EngineData>()
                            .mScnMgr->destroyEntity(
                                ent);
                    } else if (Items::CellsScript::isBit(
                               jcell, "idoorz-")) {
                        Ogre::Entity *ent =
                            ECS::get<EngineData>()
                                .mScnMgr
                                ->createEntity(
                                    "idoor-cell",
                                    "internal-door-frame1");
                        ent->setMaterial(townMaterial);
                        Ogre::Vector3 offset =
                            worldCenterOrientation *
                            Ogre::Vector3::
                                NEGATIVE_UNIT_Z *
                            outOffset;
                        Ogre::Quaternion rotation =
                            worldCenterOrientation *
                            Ogre::Quaternion(
                                Ogre::Degree(
                                    180),
                                Ogre::Vector3::
                                    UNIT_Y);
                        geo->addEntity(
                            ent,
                            worldCenterPosition +
                                offsetX +
                                offsetZ +
                                offsetY +
                                offset,
                            rotation);
                        ECS::get<EngineData>()
                            .mScnMgr->destroyEntity(
                                ent);
                    }
                    if (Items::CellsScript::isBit(
                            jcell, "doorx+")) {
                        Ogre::Vector3 offset =
                            worldCenterOrientation *
                            Ogre::Vector3::UNIT_X *
                            outOffset;
                        Ogre::Quaternion rotation =
                            worldCenterOrientation *
                            Ogre::Quaternion(
                                Ogre::Degree(
                                    90),
                                Ogre::Vector3::
                                    UNIT_Y);
                        addMeshToStatic(
                            geo,
                            Ogre::MeshManager::getSingleton()
                                .getByName(
                                    "external-door-frame1"),
                            townMaterial,
                            worldCenterPosition +
                                offsetX +
                                offsetZ +
                                offsetY +
                                offset,
                            rotation);
                    } else if (Items::CellsScript::isBit(
                               jcell, "idoorx+")) {
                        Ogre::Vector3 offset =
                            worldCenterOrientation *
                            Ogre::Vector3::UNIT_X *
                            outOffset;
                        Ogre::Quaternion rotation =
                            worldCenterOrientation *
                            Ogre::Quaternion(
                                Ogre::Degree(
                                    90),
                                Ogre::Vector3::
                                    UNIT_Y);
                        addMeshToStatic(
                            geo,
                            Ogre::MeshManager::getSingleton()
                                .getByName(
                                    "internal-door-frame1"),
                            townMaterial,
                            worldCenterPosition +
                                offsetX +
                                offsetZ +
                                offsetY +
                                offset,
                            rotation);
                    }
                    if (Items::CellsScript::isBit(
                            jcell, "doorx-")) {
                        Ogre::Vector3 offset =
                            worldCenterOrientation *
                            Ogre::Vector3::
                                NEGATIVE_UNIT_X *
                            outOffset;
                        Ogre::Quaternion rotation =
                            worldCenterOrientation *
                            Ogre::Quaternion(
                                Ogre::Degree(
                                    -90),
                                Ogre::Vector3::
                                    UNIT_Y);
                        addMeshToStatic(
                            geo,
                            Ogre::MeshManager::getSingleton()
                                .getByName(
                                    "external-door-frame1"),
                            townMaterial,
                            worldCenterPosition +
                                offsetX +
                                offsetZ +
                                offsetY +
                                offset,
                            rotation);
                    } else if (Items::CellsScript::isBit(
                               jcell, "idoorx-")) {
                        Ogre::Vector3 offset =
                            worldCenterOrientation *
                            Ogre::Vector3::
                                NEGATIVE_UNIT_X *
                            outOffset;
                        Ogre::Quaternion rotation =
                            worldCenterOrientation *
                            Ogre::Quaternion(
                                Ogre::Degree(
                                    -90),
                                Ogre::Vector3::
                                    UNIT_Y);
                        addMeshToStatic(
                            geo,
                            Ogre::MeshManager::getSingleton()
                                .getByName(
                                    "internal-door-frame1"),
                            townMaterial,
                            worldCenterPosition +
                                offsetX +
                                offsetZ +
                                offsetY +
                                offset,
                            rotation);
                    }
                }
            }
        }
    }
#endif
	void operator()(flecs::entity e, const nlohmann::json &jdistrict,
			int index, Ogre::SceneNode *sceneNode,
			Ogre::StaticGeometry *geo)
	{
		ZoneScoped;
		createDecorateDoors(e, jdistrict, index, sceneNode, geo);
	}
	void wait()
	{
		ZoneScoped;
		if (townDecorateDoorsComplete.valid() &&
		    townDecorateDoorsComplete.wait_for(std::chrono::seconds(
			    0)) != std::future_status::ready)
			townDecorateDoorsComplete.wait();
	}
};
struct TownDecorateFurniture : TownTask {
	std::shared_future<bool> townDecorateFurnitureComplete;
	/* here we place all the furniture as scene objects */
private:
	struct DistrictContext {
		Ogre::Vector3 centerPosition;
		Ogre::Quaternion centerOrientation;
		float radius;
	};

	struct LotContext {
		Ogre::Vector3 worldPosition;
		Ogre::Quaternion worldOrientation;
		float elevation;
	};

	struct CellPlacement {
		int x, y, z;
		int rotation;
		Ogre::String meshName;
		Ogre::Vector3 position;
		Ogre::Quaternion orientation;
		bool valid;
	};

	Ogre::MeshPtr getFurnitureMesh(const Ogre::String &meshName)
	{
		Ogre::MeshPtr mesh =
			Ogre::MeshManager::getSingleton().getByName(meshName);
		if (!mesh) {
			if (Ogre::ResourceGroupManager::getSingleton()
				    .resourceExists("General", meshName)) {
				mesh = Ogre::MeshManager::getSingleton().load(
					meshName, "General");
				Ogre::LodConfig meshconf(mesh);
				setupLods(meshconf);
			}
		}
		return mesh;
	}
	void placeFurnitureMesh(flecs::entity e, Ogre::MeshPtr mesh,
				const Ogre::Vector3 &position,
				const Ogre::Quaternion &rotation)
	{
		static Ogre::String materialName1 = "";
		static Ogre::String materialName2 = "";
		Ogre::Entity *ent =
			ECS::get<EngineData>().mScnMgr->createEntity(
				mesh->getName());
		Ogre::String tmpMatName =
			mesh->getSubMesh(0)->getMaterialName();
		if (tmpMatName.substr(0, 14) == "furniture-sofa") {
			if (materialName2 == "")
				materialName2 = tmpMatName;
			else
				ent->setMaterialName(

					materialName2);
		} else {
			if (materialName1 == "")
				materialName1 = tmpMatName;
			else
				ent->setMaterialName(materialName1);
		}
		Ogre::SceneNode *node = ECS::get<EngineData>()
						.mScnMgr->getRootSceneNode()
						->createChildSceneNode();
		node->_setDerivedPosition(position);
		node->_setDerivedOrientation(rotation);
		node->attachObject(ent);
		ent->setRenderingDistance(60.0f);
		addStaticBodyMesh(e, mesh, position, rotation);
		ECS::get().entity().child_of(e).set<FurnitureInstance>(
			{ node });
	}
	DistrictContext parseDistrictContext(const nlohmann::json &jp,
					     Ogre::SceneNode *sceneNode)
	{
		DistrictContext ctx;

		// Parse base position and rotation
		Ogre::Vector3 localPosition(0, 0, 0);
		Ogre::Quaternion localRotation = Ogre::Quaternion::IDENTITY;
		from_json(jp["position"], localPosition);
		from_json(jp["rotation"], localRotation);

		// Parse elevation and radius
		float elevation = jp.value("elevation", 0.0f);
		ctx.radius = jp.value("radius", 50.0f);

		// Calculate world transforms for district center
		ctx.centerPosition = sceneNode->_getDerivedPosition() +
				     localPosition +
				     Ogre::Vector3(0, elevation, 0);
		ctx.centerOrientation =
			sceneNode->_getDerivedOrientation() * localRotation;

		return ctx;
	}
	LotContext createLotContext(const DistrictContext &districtCtx,
				    const nlohmann::json &jlot)
	{
		LotContext lot;

		// Parse lot properties
		float angle = jlot.value("angle", 0.0f);
		lot.elevation = jlot.value("elevation", 0.0f);

		// Calculate lot rotation and position (offset from district center)
		Ogre::Quaternion lotRotation(Ogre::Degree(angle),
					     Ogre::Vector3::UNIT_Y);
		Ogre::Vector3 offset =
			districtCtx.centerOrientation * lotRotation *
			(Ogre::Vector3::UNIT_Z * districtCtx.radius);

		// Lot world position = district center + offset + lot elevation
		lot.worldPosition = districtCtx.centerPosition + offset +
				    Ogre::Vector3(0, lot.elevation, 0);

		// Lot orientation = district orientation * lot rotation
		lot.worldOrientation =
			districtCtx.centerOrientation * lotRotation;

		return lot;
	}
	CellPlacement createCellPlacement(const nlohmann::json &jfcell,
					  const LotContext &lotCtx)
	{
		CellPlacement cell;
		cell.valid = false;

		// Check if cell has furniture with mesh
		if (!jfcell.is_object() || !jfcell.contains("furniture")) {
			return cell;
		}

		const nlohmann::json &furniture = jfcell["furniture"];
		if (!furniture.is_object() || !furniture.contains("mesh")) {
			return cell;
		}

		cell.x = jfcell.value("x", 0);
		cell.y = jfcell.value("y", 0);
		cell.z = jfcell.value("z", 0);
		cell.rotation = jfcell.value("rotation", 2);
		cell.meshName = furniture["mesh"].get<Ogre::String>();

		// Calculate cell offsets within the lot
		Ogre::Vector3 offsetX = lotCtx.worldOrientation *
					Ogre::Vector3::UNIT_X *
					static_cast<float>(cell.x) * 2.0f;
		Ogre::Vector3 offsetZ = lotCtx.worldOrientation *
					Ogre::Vector3::UNIT_Z *
					static_cast<float>(cell.z) * 2.0f;
		Ogre::Vector3 offsetY(0, cell.y * 4.0f, 0);

		// Cell position = lot position + cell offsets
		cell.position =
			lotCtx.worldPosition + offsetX + offsetZ + offsetY;

		// Cell orientation = lot orientation * rotation
		cell.orientation =
			lotCtx.worldOrientation *
			Ogre::Quaternion(Ogre::Degree(90.0f * cell.rotation),
					 Ogre::Vector3::UNIT_Y);

		cell.valid = true;
		return cell;
	}

	void processLot(flecs::entity e, const nlohmann::json &jlot,
			const DistrictContext &districtCtx)
	{
		// Validate lot dimensions
		int depth = jlot.value("depth", 10);
		int width = jlot.value("width", 10);
		OgreAssert(width > 1 && depth > 1, "Invalid lot dimensions");

		// Create lot context with position offset from district center
		LotContext lotCtx = createLotContext(districtCtx, jlot);

		// Process furniture cells
		if (jlot.contains("furniture_cells") &&
		    jlot["furniture_cells"].is_array()) {
			const nlohmann::json &cells = jlot["furniture_cells"];
			for (nlohmann::json::const_iterator it = cells.begin();
			     it != cells.end(); ++it) {
				const nlohmann::json &jfcell = *it;

				CellPlacement cell =
					createCellPlacement(jfcell, lotCtx);
				if (!cell.valid)
					continue;

				Ogre::MeshPtr mesh =
					getFurnitureMesh(cell.meshName);
				if (mesh) {
					placeFurnitureMesh(e, mesh,
							   cell.position,
							   cell.orientation);
				}
			}
		}
	}

public:
	void createDecorateFurniture(flecs::entity e,
				     const nlohmann::json &jdistrict,
				     int /*index*/, Ogre::SceneNode *sceneNode,
				     Ogre::StaticGeometry * /*geo*/)
	{
		ZoneScoped;
		// FIXME: make furniture placement a background task.

		// Create town material (required for furniture)
		createTownMaterial(e);

		// Parse district context (center point)
		DistrictContext districtCtx =
			parseDistrictContext(jdistrict, sceneNode);

		// Process lots if they exist
		if (jdistrict.contains("lots") &&
		    jdistrict["lots"].is_array()) {
			const nlohmann::json &lots = jdistrict["lots"];
			for (nlohmann::json::const_iterator it = lots.begin();
			     it != lots.end(); ++it) {
				const nlohmann::json &jlot = *it;
				std::cout << jlot.dump() << std::endl;
				processLot(e, jlot, districtCtx);
			}
		}
	}
	void operator()(flecs::entity e, const nlohmann::json &jdistrict,
			int index, Ogre::SceneNode *sceneNode,
			Ogre::StaticGeometry *geo)
	{
		createDecorateFurniture(e, jdistrict, index, sceneNode, geo);
	}
	void wait()
	{
		if (townDecorateFurnitureComplete.valid() &&
		    townDecorateFurnitureComplete.wait_for(std::chrono::seconds(
			    0)) != std::future_status::ready)
			townDecorateFurnitureComplete.wait();
	}
};

void registerTownNPCs(flecs::entity e)
{
	ECS::get_mut<CharacterManagerModule>().registerTownCharacters(e);
	ECS::modified<CharacterManagerModule>();
}
void createTown(flecs::entity e, Ogre::SceneNode *sceneNode,
		Ogre::StaticGeometry *geo)
{
	ZoneScopedN("createTown");
	std::cout << "createTown " << e.id() << std::endl;
	Ogre::MaterialPtr townMaterial = createTownMaterial(e);
	static std::vector<TownTask *> townTasks;
	townTasks.clear();
	createTownWindows(e);
	createTownDoors(e);
	{
		std::list<flecs::entity> destroy;
		// Remove physics entities
		ECS::get()
			.query_builder<JPH::BodyID>()
			.with(flecs::ChildOf, e)
			.build()
			.each([&](flecs::entity de, JPH::BodyID &id) {
				destroy.push_back(de);
			});
		// Remove furniture entities
		ECS::get()
			.query_builder<FurnitureInstance>()
			.with(flecs::ChildOf, e)
			.build()
			.each([&](flecs::entity de,
				  FurnitureInstance &instance) {
				destroy.push_back(de);
			});
		for (flecs::entity de : destroy)
			de.destruct();
		destroy.clear();
		Ogre::String props = e.get<TerrainItem>().properties;
		nlohmann::json jp = nlohmann::json::parse(props);
		Ogre::Vector3 worldPosition = sceneNode->_getDerivedPosition();
		Ogre::Quaternion worldOrientation =
			sceneNode->_getDerivedOrientation();
		int index = 0;
		for (const auto &jdistrict : jp["districts"]) {
			TownPlazza *townPlazzaTask = new TownPlazza;
			TownLots *townLotsTask = new TownLots;
			TownCells *townCellsTask = new TownCells;
			TownRoofs *townRoofsTask = new TownRoofs;
			TownDecorateWindows *townDecorateWindowsTask =
				new TownDecorateWindows;
			TownDecorateDoors *townDecorateDoorsTask =
				new TownDecorateDoors;
			TownDecorateFurniture *townDecorateFurnitureTask =
				new TownDecorateFurniture;
			townTasks.push_back(townPlazzaTask);
			townTasks.push_back(townLotsTask);
			townTasks.push_back(townCellsTask);
			townTasks.push_back(townRoofsTask);
			townTasks.push_back(townDecorateWindowsTask);
			townTasks.push_back(townDecorateDoorsTask);
			townTasks.push_back(townDecorateFurnitureTask);
			for (auto m : townTasks)
				(*m)(e, jdistrict, index, sceneNode, geo);
			index++;
		}
	}
	Ogre::Root::getSingleton().getWorkQueue()->addTask([geo]() {
		ZoneScoped;
		for (auto m : townTasks)
			m->wait();
		Ogre::Root::getSingleton().getWorkQueue()->addMainThreadTask(
			[geo]() {
				ZoneScopedN("createTown::MainThread");
				for (auto m : townTasks) {
					delete m;
				}
				townTasks.clear();

				geo->build();
			});
	});
}

void registerTown(flecs::entity e)
{
	ZoneScoped;
	registerTownNPCs(e);
	if (ECS::get().entity<CharacterAIModule>().is_valid())
		if (ECS::get().has<CharacterAIModule>()) {
			ECS::get_mut<CharacterAIModule>().createAI(e);
			ECS::modified<CharacterAIModule>();
		}
	createTownActionNodes(e);
}

void createTownActionNodes(flecs::entity e)
{
	const TerrainItem &item = e.get<TerrainItem>();
	Ogre::String props = item.properties;
	nlohmann::json jprops = nlohmann::json::parse(props);
#if 0
    Ogre::MaterialPtr townMaterial = createTownMaterial(e);
#endif
	for (const auto &jdistrict : jprops["districts"]) {
		const nlohmann::json &jp = jdistrict;
		nlohmann::json jlots = nlohmann::json::array();
		float baseHeight = 4.0f;
		Ogre::Vector3 localPosition(0, 0, 0);
		Ogre::Quaternion localRotation = Ogre::Quaternion::IDENTITY;
		Ogre::Vector3 centerPosition = item.position;
		Ogre::Quaternion centerOrientation = item.orientation;
		float delevation = 0.0f;
		float radius = 50.0f;
		if (jp.find("elevation") != jp.end())
			delevation = jp["elevation"].get<float>();
		if (jp.find("radius") != jp.end())
			radius = jp["radius"].get<float>();
		from_json(jp["position"], localPosition);
		from_json(jp["rotation"], localRotation);
		centerPosition = centerPosition + localPosition +
				 Ogre::Vector3(0, delevation, 0);
		centerOrientation = centerOrientation * localRotation;
		if (jdistrict.find("lots") != jdistrict.end())
			jlots = jdistrict["lots"];
		for (const auto &jb : jlots) {
			float angle = 0.0f;
			int depth = 10;
			int width = 10;
			float distance = radius;
			float elevation = 0.0f;
			std::cout << jb.dump() << std::endl;
			if (jb.find("angle") != jb.end())
				angle = jb["angle"].get<float>();
			if (jb.find("depth") != jb.end())
				depth = jb["depth"].get<int>();
			if (jb.find("width") != jb.end())
				width = jb["width"].get<int>();
			if (jb.find("elevation") != jb.end())
				elevation = jb["elevation"].get<float>();

			OgreAssert(width > 1 && depth > 1 && baseHeight > 1,
				   "Bad stuff happen");

			Ogre::Quaternion rotation = Ogre::Quaternion(
				Ogre::Degree(angle), Ogre::Vector3::UNIT_Y);
			Ogre::Vector3 offset =
				centerOrientation * rotation *
				(Ogre::Vector3::UNIT_Z * distance);
			Ogre::Vector3 worldCenterPosition =
				centerPosition + offset +
				Ogre::Vector3(0, elevation, 0);
			Ogre::Quaternion worldCenterOrientation =
				centerOrientation * rotation;
			float outOffset = 1.05f;
			if (jb.find("furniture_cells") != jb.end()) {
				for (auto &jfcell : jb["furniture_cells"]) {
					int x = jfcell["x"].get<int>();
					int y = jfcell["y"].get<int>();
					int z = jfcell["z"].get<int>();
					nlohmann::json furniture =
						jfcell["furniture"];
					Ogre::Vector3 cellOffset(
						x * 2.0f, y * 4.0f, z * 2.0f);
					Ogre::Vector3 offsetX =
						worldCenterOrientation *
						Ogre::Vector3::UNIT_X *
						(float)x * 2.0f;
					Ogre::Vector3 offsetZ =
						worldCenterOrientation *
						Ogre::Vector3::UNIT_Z *
						(float)z * 2.0f;
					Ogre::Vector3 offsetY(0, y * 4.0f, 0);
					//                    static Ogre::String materialName1 = "";
					//                    static Ogre::String materialName2 = "";
					int rotation = 2;
					if (jfcell.find("rotation") !=
					    jfcell.end())
						rotation = jfcell["rotation"]
								   .get<int>();

					Ogre::Vector3 offset =
						worldCenterOrientation *
						Ogre::Vector3::UNIT_Z * 0.0f;

					if (furniture.find("actions") !=
					    furniture.end()) {
						for (const auto &action :
						     furniture["actions"]) {
							std::cout
								<< "SENSOR: "
								<< action.dump()
								<< std::endl;
							std::cout
								<< furniture
									   .dump()
								<< std::endl;
							Ogre::Vector3
								actionPosition;
							actionPosition.x =
								action["position_x"]
									.get<float>();
							actionPosition.y =
								action["position_y"]
									.get<float>();
							actionPosition.z =
								action["position_z"]
									.get<float>();
							Ogre::Quaternion worldSensorRotation =
								worldCenterOrientation *
								Ogre::Quaternion(
									Ogre::Degree(
										90.0f *
										(float)rotation),
									Ogre::Vector3::
										UNIT_Y);
							Ogre::Vector3 worldSensorPosition =
								worldCenterPosition +
								offsetX +
								offsetZ +
								offsetY +
								offset +
								worldSensorRotation *
									actionPosition;
							if (ECS::get()
								    .has<ActionNodeList>()) {
								ActionNodeList::ActionNode
									anode;
								anode.action =
									action["action"]
										.get<Ogre::String>();
								anode.action_text =
									action["action_text"]
										.get<Ogre::String>();
								anode.radius =
									action["radius"]
										.get<float>();
								anode.height =
									action["height"]
										.get<float>();
								anode.props =
									action;
								anode.props
									["town"] =
									e.id();
								anode.props
									["index"] =
									-1;
								anode.position =
									worldSensorPosition;
								anode.rotation =
									worldSensorRotation;
								anode.dynamic =
									false;
								ECS::get_mut<
									ActionNodeList>()
									.addNode(
										anode);
								std::cout
									<< "action: "
									<< action.dump(
										   4)
									<< std::endl;
								ECS::modified<
									ActionNodeList>();
							}
						}
					}
				}
			}
		}
	}
}

}
}
