#include "map/TileConfig.h" #include "map/TileOrientation.h" #include "common/Module.h" #include "data/DataModule.h" #include "data/JsonDocument.h" #include "filesystem/Filesystem.h" #include "filesystem/FileData.h" #include "filesystem/HotReload.h" #include "graphics/Graphics.h" #include #include #include #include #include namespace eve::map { namespace { float asFloat(const Poco::Dynamic::Var &v, float fallback) { try { if (v.isEmpty()) return fallback; return static_cast(v.convert()); } catch (...) { return fallback; } } int asInt(const Poco::Dynamic::Var &v, int fallback) { try { if (v.isEmpty()) return fallback; return v.convert(); } catch (...) { return fallback; } } bool asBool(const Poco::Dynamic::Var &v, bool fallback) { try { if (v.isEmpty()) return fallback; return v.convert(); } catch (...) { return fallback; } } std::string asString(const Poco::Dynamic::Var &v) { try { if (v.isEmpty()) return {}; return v.convert(); } catch (...) { return {}; } } bool readVec2(Poco::JSON::Object::Ptr o, const char *key, float &a, float &b) { if (!o || !o->has(key)) return false; Poco::JSON::Array::Ptr arr; try { arr = o->getArray(key); } catch (...) { return false; } if (!arr || arr->size() < 2) return false; a = asFloat(arr->get(0), a); b = asFloat(arr->get(1), b); return true; } bool readVec4(Poco::JSON::Object::Ptr o, const char *key, float &a, float &b, float &c, float &d) { if (!o || !o->has(key)) return false; Poco::JSON::Array::Ptr arr; try { arr = o->getArray(key); } catch (...) { return false; } if (!arr || arr->size() < 4) return false; a = asFloat(arr->get(0), a); b = asFloat(arr->get(1), b); c = asFloat(arr->get(2), c); d = asFloat(arr->get(3), d); return true; } int64_t fileModtime(const std::string &path) { auto *fs = eve::ModuleManager::getInstance("Filesystem"); if (!fs) fs = eve::filesystem::Filesystem::create(); eve::filesystem::Filesystem::Info info{}; if (!fs->getInfo(path, info)) return -1; return info.modtime; } graphics::Texture *tryLoadTexture(const std::string &path) { if (path.empty()) return nullptr; auto *gfx = eve::ModuleManager::getInstance("Graphics"); if (!gfx) return nullptr; try { graphics::Texture *tex = gfx->newTextureFromFile(path); if (auto *hot = eve::ModuleManager::getInstance("HotReload")) hot->bind(path, "texture"); return tex; } catch (...) { return nullptr; } } struct TilesetInfo { std::string image; int firstGid = 1; int columns = 1; int tileW = 32; int tileH = 32; int margin = 0; int spacing = 0; }; TilesetInfo readTilesetObject(Poco::JSON::Object::Ptr o) { TilesetInfo info; if (!o) return info; if (o->has("image")) info.image = asString(o->get("image")); else if (o->has("texture")) info.image = asString(o->get("texture")); if (o->has("firstgid")) info.firstGid = asInt(o->get("firstgid"), 1); else if (o->has("firstGid")) info.firstGid = asInt(o->get("firstGid"), 1); if (o->has("columns")) info.columns = asInt(o->get("columns"), 1); if (o->has("tilewidth")) info.tileW = asInt(o->get("tilewidth"), 32); else if (o->has("tileWidth")) info.tileW = asInt(o->get("tileWidth"), 32); if (o->has("tileheight")) info.tileH = asInt(o->get("tileheight"), 32); else if (o->has("tileHeight")) info.tileH = asInt(o->get("tileHeight"), 32); if (o->has("margin")) info.margin = asInt(o->get("margin"), 0); if (o->has("spacing")) info.spacing = asInt(o->get("spacing"), 0); if (info.columns <= 0 && o->has("imagewidth") && info.tileW > 0) { const int iw = asInt(o->get("imagewidth"), 0); if (iw > 0) info.columns = std::max(1, (iw - info.margin) / (info.tileW + info.spacing)); } return info; } void applyTileset(TileLayer *layer, const TilesetInfo &info) { if (!layer) return; layer->setTilesetTileSize(info.tileW, info.tileH); graphics::Texture *tex = tryLoadTexture(info.image); layer->resource()->texturePath = info.image; layer->setTileset(tex, info.firstGid, info.columns, info.margin, info.spacing); } bool decodeLayerData(Poco::JSON::Object::Ptr layerObj, size_t expectedCount, std::vector &out, std::string *error) { if (!layerObj || !layerObj->has("data")) { if (error) *error = "missing data"; return false; } try { if (layerObj->isArray("data")) { auto arr = layerObj->getArray("data"); if (!arr) { if (error) *error = "invalid data array"; return false; } out.resize(arr->size()); for (size_t i = 0; i < arr->size(); ++i) out[i] = uint32_t(asInt(arr->get(i), 0)); if (expectedCount > 0 && out.size() != expectedCount) { if (error) *error = "gid count mismatch"; return false; } return true; } } catch (...) { } std::string encoding = layerObj->has("encoding") ? asString(layerObj->get("encoding")) : ""; std::string compression = layerObj->has("compression") ? asString(layerObj->get("compression")) : ""; if (encoding != "base64") { if (error) *error = "unsupported encoding"; return false; } if (compression == "zstd") { if (error) *error = "zstd not supported; export as zlib"; return false; } const std::string b64 = asString(layerObj->get("data")); size_t decodedLen = 0; std::unique_ptr decoded(eve::data::decode("base64", b64.data(), b64.size(), decodedLen)); if (!decoded) { if (error) *error = "base64 decode failed"; return false; } const char *bytes = decoded.get(); size_t nbytes = decodedLen; std::unique_ptr inflated; if (!compression.empty()) { if (compression != "zlib" && compression != "gzip") { if (error) *error = "unsupported compression"; return false; } size_t rawsize = expectedCount * 4; try { inflated.reset(eve::data::decompress(compression, bytes, nbytes, rawsize)); } catch (...) { if (error) *error = "decompress failed"; return false; } if (!inflated) { if (error) *error = "decompress failed"; return false; } bytes = inflated.get(); nbytes = rawsize; } if (expectedCount > 0 && nbytes != expectedCount * 4) { if (error) *error = "gid byte length mismatch"; return false; } const size_t count = nbytes / 4; out.resize(count); for (size_t i = 0; i < count; ++i) { const unsigned char *p = reinterpret_cast(bytes) + i * 4; out[i] = uint32_t(p[0]) | (uint32_t(p[1]) << 8) | (uint32_t(p[2]) << 16) | (uint32_t(p[3]) << 24); } return true; } void applyLayerDraw(TileLayer *layer, Poco::JSON::Object::Ptr layerObj) { if (!layer || !layerObj) return; if (layerObj->has("visible")) layer->setVisible(asBool(layerObj->get("visible"), true)); if (layerObj->has("layer")) layer->setLayer(asInt(layerObj->get("layer"), layer->getLayer())); else if (layerObj->has("id")) layer->setLayer(asInt(layerObj->get("id"), layer->getLayer())); float r = 1, g = 1, b = 1, a = 1; if (readVec4(layerObj, "tint", r, g, b, a)) layer->setTint(r, g, b, a); else if (layerObj->has("opacity")) { a = asFloat(layerObj->get("opacity"), 1.f); layer->setTint(1.f, 1.f, 1.f, a); } float ox = layer->getX(), oy = layer->getY(); if (layerObj->has("x")) ox = asFloat(layerObj->get("x"), ox); if (layerObj->has("offsetx")) ox += asFloat(layerObj->get("offsetx"), 0.f); if (layerObj->has("y")) oy = asFloat(layerObj->get("y"), oy); if (layerObj->has("offsety")) oy += asFloat(layerObj->get("offsety"), 0.f); if (layerObj->has("x") || layerObj->has("y") || layerObj->has("offsetx") || layerObj->has("offsety")) layer->setOrigin(ox, oy); } bool isTileLayerObject(Poco::JSON::Object::Ptr o) { if (!o) return false; if (o->has("type")) { const std::string t = asString(o->get("type")); if (!t.empty() && t != "tilelayer") return false; } return o->has("data"); } bool isObjectGroup(Poco::JSON::Object::Ptr o) { if (!o) return false; if (!o->has("type")) return false; return asString(o->get("type")) == "objectgroup"; } bool parseOrientation(Poco::JSON::Object::Ptr root, TileLayer::Config *cfg, std::string *error) { if (!root || !cfg) return false; if (!root->has("orientation")) return true; const std::string o = asString(root->get("orientation")); if (o.empty() || o == "orthogonal") cfg->orientation = MapOrientation::Orthogonal; else if (o == "isometric") cfg->orientation = MapOrientation::Isometric; else if (o == "staggered") cfg->orientation = MapOrientation::Staggered; else if (o == "hexagonal") cfg->orientation = MapOrientation::Hexagonal; else { if (error) *error = "unknown orientation: " + o; return false; } if (root->has("staggeraxis")) { const std::string a = asString(root->get("staggeraxis")); cfg->staggerAxis = (a == "x") ? StaggerAxis::X : StaggerAxis::Y; } if (root->has("staggerindex")) { const std::string i = asString(root->get("staggerindex")); cfg->staggerIndex = (i == "even") ? StaggerIndex::Even : StaggerIndex::Odd; } if (root->has("hexsidelength")) cfg->hexSideLength = asFloat(root->get("hexsidelength"), 0.f); return true; } bool applyMapGlobals(TileLayer *layer, Poco::JSON::Object::Ptr root, std::string *error) { if (!layer || !root) return false; int mapW = layer->getMapWidth(); int mapH = layer->getMapHeight(); float tileW = layer->getTileWidth(); float tileH = layer->getTileHeight(); if (root->has("width")) mapW = asInt(root->get("width"), mapW); if (root->has("height")) mapH = asInt(root->get("height"), mapH); if (root->has("tilewidth")) tileW = asFloat(root->get("tilewidth"), tileW); else if (root->has("tileWidth")) tileW = asFloat(root->get("tileWidth"), tileW); if (root->has("tileheight")) tileH = asFloat(root->get("tileheight"), tileH); else if (root->has("tileHeight")) tileH = asFloat(root->get("tileHeight"), tileH); if (mapW != layer->getMapWidth() || mapH != layer->getMapHeight()) layer->resize(mapW, mapH); layer->setTileSize(tileW, tileH); if (!parseOrientation(root, &(*layer->config()), error)) return false; float ox = layer->getX(), oy = layer->getY(); if (readVec2(root, "origin", ox, oy)) layer->setOrigin(ox, oy); else { if (root->has("x")) ox = asFloat(root->get("x"), ox); if (root->has("y")) oy = asFloat(root->get("y"), oy); if (root->has("x") || root->has("y")) layer->setOrigin(ox, oy); } if (root->has("layer")) layer->setLayer(asInt(root->get("layer"), layer->getLayer())); if (root->has("visible")) layer->setVisible(asBool(root->get("visible"), layer->isVisible())); if (root->has("autoReload")) layer->resource()->autoReload = asBool(root->get("autoReload"), true); float r = 1, g = 1, b = 1, a = 1; if (readVec4(root, "tint", r, g, b, a)) layer->setTint(r, g, b, a); if (root->has("tileset") && !root->isArray("tileset")) { try { auto o = root->getObject("tileset"); if (o && !o->has("source")) applyTileset(layer, readTilesetObject(o)); } catch (...) { } } else if (root->has("tilesets")) { try { auto arr = root->getArray("tilesets"); if (arr) { for (size_t i = 0; i < arr->size(); ++i) { try { auto o = arr->getObject(i); if (o && o->has("source")) continue; applyTileset(layer, readTilesetObject(o)); break; } catch (...) { } } } } catch (...) { } } else if (root->has("image") || root->has("texture")) { applyTileset(layer, readTilesetObject(root)); } return true; } bool applyFlatLayerData(TileLayer *layer, Poco::JSON::Object::Ptr root, std::string *error) { const int mapW = layer->getMapWidth(); const int mapH = layer->getMapHeight(); const size_t need = size_t(std::max(0, mapW) * std::max(0, mapH)); if (need == 0) { if (error) *error = "empty map size"; return false; } std::vector data; if (!decodeLayerData(root, need, data, error)) return false; auto &gids = layer->tiles()->gids; gids.assign(need, 0u); const size_t n = std::min(need, data.size()); for (size_t i = 0; i < n; ++i) gids[i] = data[i]; return true; } bool applyOneLayerObject(TileLayer *layer, Poco::JSON::Object::Ptr layerObj, int mapW, int mapH, std::string *error) { if (!layer || !layerObj) return false; int w = mapW, h = mapH; if (layerObj->has("width")) w = asInt(layerObj->get("width"), w); if (layerObj->has("height")) h = asInt(layerObj->get("height"), h); if (w != layer->getMapWidth() || h != layer->getMapHeight()) layer->resize(w, h); const size_t need = size_t(std::max(0, layer->getMapWidth()) * std::max(0, layer->getMapHeight())); std::vector data; if (!decodeLayerData(layerObj, need, data, error)) return false; auto &gids = layer->tiles()->gids; gids.assign(need, 0u); const size_t n = std::min(need, data.size()); for (size_t i = 0; i < n; ++i) gids[i] = data[i]; applyLayerDraw(layer, layerObj); return true; } void parseObjectGroup(Poco::JSON::Object::Ptr group, std::vector &out) { if (!group || !group->has("objects")) return; Poco::JSON::Array::Ptr arr; try { arr = group->getArray("objects"); } catch (...) { return; } if (!arr) return; for (size_t i = 0; i < arr->size(); ++i) { Poco::JSON::Object::Ptr o; try { o = arr->getObject(i); } catch (...) { continue; } if (!o) continue; MapObject mo; if (o->has("name")) mo.name = asString(o->get("name")); if (o->has("type")) mo.type = asString(o->get("type")); else if (o->has("class")) mo.type = asString(o->get("class")); mo.x = o->has("x") ? asFloat(o->get("x"), 0.f) : 0.f; mo.y = o->has("y") ? asFloat(o->get("y"), 0.f) : 0.f; mo.width = o->has("width") ? asFloat(o->get("width"), 0.f) : 0.f; mo.height = o->has("height") ? asFloat(o->get("height"), 0.f) : 0.f; if (o->has("gid")) mo.gid = uint32_t(asInt(o->get("gid"), 0)); out.push_back(std::move(mo)); } } void abandonLayers(std::vector &layers) { for (TileLayer *layer : layers) { if (!layer) continue; layer->clear(); layer->setVisible(false); } layers.clear(); } TilesetInfo readDefaultTileset(Poco::JSON::Object::Ptr root) { TilesetInfo defaultTs; if (root->has("tilesets")) { try { auto arr = root->getArray("tilesets"); if (arr) { for (size_t i = 0; i < arr->size(); ++i) { try { auto o = arr->getObject(i); if (o && o->has("source")) continue; return readTilesetObject(o); } catch (...) { } } } } catch (...) { } } else if (root->has("tileset") && !root->isArray("tileset")) { try { auto o = root->getObject("tileset"); if (o && !o->has("source")) return readTilesetObject(o); } catch (...) { } } else if (root->has("image") || root->has("texture")) { return readTilesetObject(root); } return defaultTs; } std::vector loadMapObject(Poco::JSON::Object::Ptr root, const std::string &path, eve::filesystem::Filesystem *fs, std::vector *objects, std::string *error) { std::vector out; if (!root) { if (error) *error = "config root must be object"; return out; } int mapW = 10, mapH = 10; float tileW = 32.f, tileH = 32.f; if (root->has("width")) mapW = asInt(root->get("width"), mapW); if (root->has("height")) mapH = asInt(root->get("height"), mapH); if (root->has("tilewidth")) tileW = asFloat(root->get("tilewidth"), tileW); else if (root->has("tileWidth")) tileW = asFloat(root->get("tileWidth"), tileW); if (root->has("tileheight")) tileH = asFloat(root->get("tileheight"), tileH); else if (root->has("tileHeight")) tileH = asFloat(root->get("tileHeight"), tileH); // Validate orientation early on a temp config. TileLayer::Config orientCheck; if (!parseOrientation(root, &orientCheck, error)) return out; TilesetInfo defaultTs = readDefaultTileset(root); if (objects) objects->clear(); auto bindResource = [&](TileLayer *layer) { auto res = layer->resource(); res->path = path; if (!path.empty()) { res->modtime = fileModtime(path); if (fs) { fs->watch(path); if (auto *hot = eve::ModuleManager::getInstance("HotReload")) hot->bind(path, "tilemap"); } } if (root->has("autoReload")) res->autoReload = asBool(root->get("autoReload"), true); }; if (root->has("layers")) { try { auto arr = root->getArray("layers"); if (arr) { int sort = 0; for (size_t i = 0; i < arr->size(); ++i) { Poco::JSON::Object::Ptr lo; try { lo = arr->getObject(i); } catch (...) { continue; } if (objects && isObjectGroup(lo)) { parseObjectGroup(lo, *objects); continue; } if (!isTileLayerObject(lo)) continue; TileLayer *layer = TileLayer::createLayer(mapW, mapH, tileW, tileH); if (!applyMapGlobals(layer, root, error)) { abandonLayers(out); layer->clear(); layer->setVisible(false); return {}; } applyTileset(layer, defaultTs); if (!applyOneLayerObject(layer, lo, mapW, mapH, error)) { abandonLayers(out); layer->clear(); layer->setVisible(false); return {}; } if (!lo->has("layer") && !lo->has("id")) layer->setLayer(sort); ++sort; bindResource(layer); out.push_back(layer); } } } catch (...) { } if (!out.empty()) return out; } TileLayer *layer = TileLayer::createLayer(mapW, mapH, tileW, tileH); if (!applyMapGlobals(layer, root, error)) { layer->clear(); layer->setVisible(false); return {}; } applyTileset(layer, defaultTs); if (root->has("data")) { if (!applyFlatLayerData(layer, root, error)) { layer->clear(); layer->setVisible(false); return {}; } } bindResource(layer); out.push_back(layer); return out; } } // namespace bool applyConfigDocument(TileLayer *layer, data::JsonDocument *doc) { if (!layer || !doc || !doc->isObject()) return false; auto root = doc->object(); if (!root) return false; std::string err; if (!applyMapGlobals(layer, root, &err)) return false; if (root->has("layers")) { try { auto arr = root->getArray("layers"); if (arr) { for (size_t i = 0; i < arr->size(); ++i) { Poco::JSON::Object::Ptr lo; try { lo = arr->getObject(i); } catch (...) { continue; } if (!isTileLayerObject(lo)) continue; return applyOneLayerObject(layer, lo, layer->getMapWidth(), layer->getMapHeight(), nullptr); } } } catch (...) { } } if (root->has("data")) return applyFlatLayerData(layer, root, nullptr); return true; } bool applyConfigText(TileLayer *layer, const std::string &json, std::string *error) { auto *dm = eve::data::DataModule::create(); std::string err; std::unique_ptr doc(dm->decodeJson(json, &err)); if (!doc) { if (error) *error = err.empty() ? "invalid json" : err; return false; } if (!doc->isObject()) { if (error) *error = "config root must be object"; return false; } auto root = doc->object(); if (!root) { if (error) *error = "config root must be object"; return false; } if (!applyMapGlobals(layer, root, error)) return false; if (root->has("layers")) { try { auto arr = root->getArray("layers"); if (arr) { for (size_t i = 0; i < arr->size(); ++i) { Poco::JSON::Object::Ptr lo; try { lo = arr->getObject(i); } catch (...) { continue; } if (!isTileLayerObject(lo)) continue; return applyOneLayerObject(layer, lo, layer->getMapWidth(), layer->getMapHeight(), error); } } } catch (...) { } } if (root->has("data")) return applyFlatLayerData(layer, root, error); return true; } bool loadConfigFile(TileLayer *layer, const std::string &path, std::string *error) { if (!layer || path.empty()) { if (error) *error = "empty path"; return false; } auto *fs = eve::ModuleManager::getInstance("Filesystem"); if (!fs) fs = eve::filesystem::Filesystem::create(); std::unique_ptr data; try { data.reset(fs->read(path)); } catch (...) { if (error) *error = "read failed: " + path; return false; } if (!data || data->getSize() == 0) { if (error) *error = "empty file: " + path; return false; } std::string text(static_cast(data->getData()), data->getSize()); if (!applyConfigText(layer, text, error)) return false; auto res = layer->resource(); res->path = path; res->modtime = fileModtime(path); fs->watch(path); if (auto *hot = eve::ModuleManager::getInstance("HotReload")) hot->bind(path, "tilemap"); return true; } bool reloadConfigFile(TileLayer *layer, std::string *error) { if (!layer) return false; const std::string &path = layer->resource()->path; if (path.empty()) { if (error) *error = "no config path"; return false; } return loadConfigFile(layer, path, error); } std::vector loadMapText(const std::string &json, std::vector *objects, std::string *error) { auto *dm = eve::data::DataModule::create(); std::string err; std::unique_ptr doc(dm->decodeJson(json, &err)); if (!doc || !doc->isObject()) { if (error) *error = err.empty() ? "invalid json" : err; return {}; } return loadMapObject(doc->object(), {}, nullptr, objects, error); } std::vector loadMapFile(const std::string &path, std::vector *objects, std::string *error) { if (path.empty()) { if (error) *error = "empty path"; return {}; } auto *fs = eve::ModuleManager::getInstance("Filesystem"); if (!fs) fs = eve::filesystem::Filesystem::create(); std::unique_ptr data; try { data.reset(fs->read(path)); } catch (...) { if (error) *error = "read failed: " + path; return {}; } if (!data || data->getSize() == 0) { if (error) *error = "empty file: " + path; return {}; } std::string text(static_cast(data->getData()), data->getSize()); auto *dm = eve::data::DataModule::create(); std::string err; std::unique_ptr doc(dm->decodeJson(text, &err)); if (!doc || !doc->isObject()) { if (error) *error = err.empty() ? "invalid json" : err; return {}; } return loadMapObject(doc->object(), path, fs, objects, error); } std::vector loadMapFile(const std::string &path, std::string *error) { return loadMapFile(path, nullptr, error); } } // namespace eve::map