diff --git a/src/pipeline/custom_zone_discovery.cpp b/src/pipeline/custom_zone_discovery.cpp index 1345a3c5..953b6b67 100644 --- a/src/pipeline/custom_zone_discovery.cpp +++ b/src/pipeline/custom_zone_discovery.cpp @@ -1,5 +1,6 @@ #include "pipeline/custom_zone_discovery.hpp" #include "core/logger.hpp" +#include #include #include @@ -29,30 +30,34 @@ std::vector CustomZoneDiscovery::scanDirectory(const std::string std::ifstream f(zoneJson); if (!f) continue; - std::string content((std::istreambuf_iterator(f)), - std::istreambuf_iterator()); - auto findStr = [&](const std::string& key) -> std::string { - auto pos = content.find("\"" + key + "\""); - if (pos == std::string::npos) return ""; - pos = content.find('"', content.find(':', pos) + 1); - if (pos == std::string::npos) return ""; - auto end = content.find('"', pos + 1); - return content.substr(pos + 1, end - pos - 1); - }; + try { + auto j = nlohmann::json::parse(f); - CustomZoneInfo info; - info.name = findStr("mapName"); - if (info.name.empty()) info.name = findStr("name"); - info.author = findStr("author"); - info.description = findStr("description"); - info.directory = entry.path().string(); - info.hasCreatures = fs::exists(entry.path().string() + "/creatures.json"); - info.hasQuests = fs::exists(entry.path().string() + "/quests.json"); + CustomZoneInfo info; + info.name = j.value("mapName", ""); + if (info.name.empty()) info.name = j.value("name", ""); + info.author = j.value("author", ""); + info.description = j.value("description", ""); + info.mapId = j.value("mapId", 9000u); + info.directory = entry.path().string(); + info.hasCreatures = j.value("hasCreatures", false) || + fs::exists(entry.path().string() + "/creatures.json"); + info.hasQuests = fs::exists(entry.path().string() + "/quests.json"); - if (!info.name.empty()) { - results.push_back(info); - LOG_INFO("Discovered custom zone: ", info.name, " in ", info.directory); + if (j.contains("tiles") && j["tiles"].is_array()) { + for (const auto& t : j["tiles"]) { + if (t.is_array() && t.size() >= 2) + info.tiles.push_back({t[0].get(), t[1].get()}); + } + } + + if (!info.name.empty()) { + results.push_back(info); + LOG_INFO("Discovered custom zone: ", info.name, " in ", info.directory); + } + } catch (const std::exception& e) { + LOG_WARNING("Failed to parse zone.json in ", entry.path().string(), ": ", e.what()); } } diff --git a/tools/editor/editor_app.cpp b/tools/editor/editor_app.cpp index c37fdb47..a22cb2e7 100644 --- a/tools/editor/editor_app.cpp +++ b/tools/editor/editor_app.cpp @@ -9,6 +9,7 @@ #include "pipeline/wowee_building.hpp" #include "pipeline/wmo_loader.hpp" #include "core/coordinates.hpp" +#include #include "rendering/vk_context.hpp" #include "pipeline/adt_loader.hpp" #include "pipeline/terrain_mesh.hpp" @@ -895,26 +896,24 @@ void EditorApp::exportZone(const std::string& outputDir) { int score = validation.openFormatScore(); // Write zone statistics JSON { + nlohmann::json sj; + sj["map"] = loadedMap_; + sj["tile"] = {loadedTileX_, loadedTileY_}; + sj["objects"] = objectPlacer_.objectCount(); + sj["npcs"] = npcSpawner_.spawnCount(); + sj["quests"] = questEditor_.questCount(); + sj["textures"] = usedTextures.size(); + sj["openFormatScore"] = score; + sj["formats"] = validation.summary(); std::ofstream stats(base + "/stats.json"); - if (stats) { - stats << "{\n"; - stats << " \"map\": \"" << loadedMap_ << "\",\n"; - stats << " \"tile\": [" << loadedTileX_ << "," << loadedTileY_ << "],\n"; - stats << " \"objects\": " << objectPlacer_.objectCount() << ",\n"; - stats << " \"npcs\": " << npcSpawner_.spawnCount() << ",\n"; - stats << " \"quests\": " << questEditor_.questCount() << ",\n"; - stats << " \"textures\": " << usedTextures.size() << ",\n"; - stats << " \"openFormatScore\": " << score << ",\n"; - stats << " \"formats\": \"" << validation.summary() << "\"\n"; - stats << "}\n"; - } + if (stats) stats << sj.dump(2) << "\n"; } showToast("Exported " + std::to_string(fileCount) + " files (" + - std::to_string(score) + "/5 open format)"); + std::to_string(score) + "/6 open format)"); LOG_INFO("=== Zone Export Summary ==="); LOG_INFO(" Output: ", base); - LOG_INFO(" Open format score: ", score, "/5"); + LOG_INFO(" Open format score: ", score, "/6"); LOG_INFO(" Formats: ", validation.summary()); LOG_INFO(" Terrain: WOT/WHM + heightmap/normals PNG"); LOG_INFO(" Textures: ", usedTextures.size(), " BLP→PNG"); diff --git a/tools/editor/npc_spawner.cpp b/tools/editor/npc_spawner.cpp index 4ea29e6a..8a4c674b 100644 --- a/tools/editor/npc_spawner.cpp +++ b/tools/editor/npc_spawner.cpp @@ -1,5 +1,6 @@ #include "npc_spawner.hpp" #include "core/logger.hpp" +#include #include #include #include @@ -58,46 +59,44 @@ bool NpcSpawner::saveToFile(const std::string& path) const { auto dir = std::filesystem::path(path).parent_path(); if (!dir.empty()) std::filesystem::create_directories(dir); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& s : spawns_) { + nlohmann::json js; + js["name"] = s.name; + js["model"] = s.modelPath; + js["displayId"] = s.displayId; + js["position"] = {s.position.x, s.position.y, s.position.z}; + js["orientation"] = s.orientation; + js["scale"] = s.scale; + js["level"] = s.level; + js["health"] = s.health; + js["mana"] = s.mana; + js["minDamage"] = s.minDamage; + js["maxDamage"] = s.maxDamage; + js["armor"] = s.armor; + js["faction"] = s.faction; + js["behavior"] = static_cast(s.behavior); + js["wanderRadius"] = s.wanderRadius; + js["aggroRadius"] = s.aggroRadius; + js["leashRadius"] = s.leashRadius; + js["respawnTimeMs"] = s.respawnTimeMs; + js["hostile"] = s.hostile; + js["questgiver"] = s.questgiver; + js["vendor"] = s.vendor; + js["flightmaster"] = s.flightmaster; + js["innkeeper"] = s.innkeeper; + + nlohmann::json patrol = nlohmann::json::array(); + for (const auto& p : s.patrolPath) { + patrol.push_back({p.position.x, p.position.y, p.position.z, p.waitTimeMs}); + } + js["patrol"] = patrol; + arr.push_back(js); + } + std::ofstream f(path); if (!f) { LOG_ERROR("Failed to write NPC file: ", path); return false; } - - f << "[\n"; - for (size_t i = 0; i < spawns_.size(); i++) { - const auto& s = spawns_[i]; - f << " {\n"; - f << " \"name\": \"" << s.name << "\",\n"; - f << " \"model\": \"" << s.modelPath << "\",\n"; - f << " \"displayId\": " << s.displayId << ",\n"; - f << " \"position\": [" << s.position.x << "," << s.position.y << "," << s.position.z << "],\n"; - f << " \"orientation\": " << s.orientation << ",\n"; - f << " \"scale\": " << s.scale << ",\n"; - f << " \"level\": " << s.level << ",\n"; - f << " \"health\": " << s.health << ",\n"; - f << " \"mana\": " << s.mana << ",\n"; - f << " \"minDamage\": " << s.minDamage << ",\n"; - f << " \"maxDamage\": " << s.maxDamage << ",\n"; - f << " \"armor\": " << s.armor << ",\n"; - f << " \"faction\": " << s.faction << ",\n"; - f << " \"behavior\": " << static_cast(s.behavior) << ",\n"; - f << " \"wanderRadius\": " << s.wanderRadius << ",\n"; - f << " \"aggroRadius\": " << s.aggroRadius << ",\n"; - f << " \"leashRadius\": " << s.leashRadius << ",\n"; - f << " \"respawnTimeMs\": " << s.respawnTimeMs << ",\n"; - f << " \"hostile\": " << (s.hostile ? "true" : "false") << ",\n"; - f << " \"questgiver\": " << (s.questgiver ? "true" : "false") << ",\n"; - f << " \"vendor\": " << (s.vendor ? "true" : "false") << ",\n"; - f << " \"flightmaster\": " << (s.flightmaster ? "true" : "false") << ",\n"; - f << " \"innkeeper\": " << (s.innkeeper ? "true" : "false") << ",\n"; - f << " \"patrol\": ["; - for (size_t p = 0; p < s.patrolPath.size(); p++) { - f << "[" << s.patrolPath[p].position.x << "," << s.patrolPath[p].position.y - << "," << s.patrolPath[p].position.z << "," << s.patrolPath[p].waitTimeMs << "]"; - if (p + 1 < s.patrolPath.size()) f << ","; - } - f << "]\n"; - f << " }" << (i + 1 < spawns_.size() ? "," : "") << "\n"; - } - f << "]\n"; + f << arr.dump(2) << "\n"; LOG_INFO("NPC spawns saved: ", path, " (", spawns_.size(), " creatures)"); return true; @@ -124,98 +123,68 @@ bool NpcSpawner::loadFromFile(const std::string& path) { std::ifstream f(path); if (!f) { LOG_ERROR("Failed to open NPC file: ", path); return false; } - std::string content((std::istreambuf_iterator(f)), - std::istreambuf_iterator()); + try { + auto arr = nlohmann::json::parse(f); + if (!arr.is_array()) return false; - // Minimal JSON parser — extract fields from our known format - spawns_.clear(); - selectedIdx_ = -1; + spawns_.clear(); + selectedIdx_ = -1; - auto findStr = [&](const std::string& block, const std::string& key) -> std::string { - auto pos = block.find("\"" + key + "\""); - if (pos == std::string::npos) return ""; - pos = block.find(':', pos); - if (pos == std::string::npos) return ""; - pos = block.find('"', pos + 1); - if (pos == std::string::npos) return ""; - auto end = block.find('"', pos + 1); - if (end == std::string::npos) return ""; - return block.substr(pos + 1, end - pos - 1); - }; + for (const auto& js : arr) { + CreatureSpawn s; + s.name = js.value("name", ""); + s.modelPath = js.value("model", ""); + s.displayId = js.value("displayId", 0u); + s.orientation = js.value("orientation", 0.0f); + s.scale = js.value("scale", 1.0f); + if (s.scale < 0.1f) s.scale = 1.0f; + s.level = js.value("level", 1u); + s.health = js.value("health", 100u); + s.mana = js.value("mana", 0u); + s.minDamage = js.value("minDamage", 5u); + s.maxDamage = js.value("maxDamage", 10u); + s.armor = js.value("armor", 0u); + s.faction = js.value("faction", 0u); + s.behavior = static_cast(js.value("behavior", 0)); + s.wanderRadius = js.value("wanderRadius", 0.0f); + s.aggroRadius = js.value("aggroRadius", 15.0f); + s.leashRadius = js.value("leashRadius", 40.0f); + s.respawnTimeMs = js.value("respawnTimeMs", 60000u); + s.hostile = js.value("hostile", false); + s.questgiver = js.value("questgiver", false); + s.vendor = js.value("vendor", false); + s.flightmaster = js.value("flightmaster", false); + s.innkeeper = js.value("innkeeper", false); - auto findNum = [&](const std::string& block, const std::string& key) -> float { - auto pos = block.find("\"" + key + "\""); - if (pos == std::string::npos) return 0; - pos = block.find(':', pos); - if (pos == std::string::npos) return 0; - return std::stof(block.substr(pos + 1)); - }; + if (js.contains("position") && js["position"].is_array() && js["position"].size() >= 3) { + s.position = glm::vec3(js["position"][0].get(), + js["position"][1].get(), + js["position"][2].get()); + } - auto findBool = [&](const std::string& block, const std::string& key) -> bool { - auto pos = block.find("\"" + key + "\""); - if (pos == std::string::npos) return false; - return block.find("true", pos) < block.find('\n', pos); - }; - - // Split by object boundaries - size_t start = 0; - while ((start = content.find('{', start)) != std::string::npos) { - auto end = content.find('}', start); - if (end == std::string::npos) break; - std::string block = content.substr(start, end - start + 1); - - CreatureSpawn s; - s.name = findStr(block, "name"); - s.modelPath = findStr(block, "model"); - s.displayId = static_cast(findNum(block, "displayId")); - s.orientation = findNum(block, "orientation"); - s.scale = findNum(block, "scale"); - if (s.scale < 0.1f) s.scale = 1.0f; - s.level = static_cast(std::max(1.0f, findNum(block, "level"))); - s.health = static_cast(std::max(1.0f, findNum(block, "health"))); - s.mana = static_cast(findNum(block, "mana")); - s.minDamage = static_cast(findNum(block, "minDamage")); - s.maxDamage = static_cast(findNum(block, "maxDamage")); - s.armor = static_cast(findNum(block, "armor")); - s.faction = static_cast(findNum(block, "faction")); - s.behavior = static_cast(static_cast(findNum(block, "behavior"))); - s.wanderRadius = findNum(block, "wanderRadius"); - s.aggroRadius = findNum(block, "aggroRadius"); - s.leashRadius = findNum(block, "leashRadius"); - s.respawnTimeMs = static_cast(findNum(block, "respawnTimeMs")); - s.hostile = findBool(block, "hostile"); - s.questgiver = findBool(block, "questgiver"); - s.vendor = findBool(block, "vendor"); - s.flightmaster = findBool(block, "flightmaster"); - s.innkeeper = findBool(block, "innkeeper"); - - // Parse position array - auto posStart = block.find("\"position\""); - if (posStart != std::string::npos) { - auto bk = block.find('[', posStart); - if (bk != std::string::npos) { - float vals[3] = {}; - int vi = 0; - auto p = bk + 1; - while (vi < 3 && p < block.size()) { - vals[vi++] = std::stof(block.substr(p)); - p = block.find(',', p); - if (p == std::string::npos) break; - p++; + if (js.contains("patrol") && js["patrol"].is_array()) { + for (const auto& pt : js["patrol"]) { + if (pt.is_array() && pt.size() >= 4) { + PatrolPoint pp; + pp.position = glm::vec3(pt[0].get(), pt[1].get(), pt[2].get()); + pp.waitTimeMs = pt[3].get(); + s.patrolPath.push_back(pp); + } } - s.position = glm::vec3(vals[0], vals[1], vals[2]); + } + + if (!s.name.empty()) { + s.id = nextId(); + spawns_.push_back(s); } } - if (!s.name.empty()) { - s.id = nextId(); - spawns_.push_back(s); - } - start = end + 1; + LOG_INFO("NPC spawns loaded: ", path, " (", spawns_.size(), " creatures)"); + return true; + } catch (const std::exception& e) { + LOG_ERROR("Failed to parse NPC file: ", e.what()); + return false; } - - LOG_INFO("NPC spawns loaded: ", path, " (", spawns_.size(), " creatures)"); - return true; } } // namespace editor diff --git a/tools/editor/object_placer.cpp b/tools/editor/object_placer.cpp index 92cfa446..bcb26932 100644 --- a/tools/editor/object_placer.cpp +++ b/tools/editor/object_placer.cpp @@ -1,5 +1,6 @@ #include "object_placer.hpp" #include "core/logger.hpp" +#include #include #include #include @@ -147,19 +148,21 @@ void ObjectPlacer::undoLastPlace() { bool ObjectPlacer::saveToFile(const std::string& path) const { std::filesystem::create_directories(std::filesystem::path(path).parent_path()); + + nlohmann::json arr = nlohmann::json::array(); + for (const auto& o : objects_) { + arr.push_back({ + {"type", static_cast(o.type)}, + {"path", o.path}, + {"pos", {o.position.x, o.position.y, o.position.z}}, + {"rot", {o.rotation.x, o.rotation.y, o.rotation.z}}, + {"scale", o.scale} + }); + } + std::ofstream f(path); if (!f) return false; - f << "[\n"; - for (size_t i = 0; i < objects_.size(); i++) { - const auto& o = objects_[i]; - f << " {\"type\":" << static_cast(o.type) - << ",\"path\":\"" << o.path << "\"" - << ",\"pos\":[" << o.position.x << "," << o.position.y << "," << o.position.z << "]" - << ",\"rot\":[" << o.rotation.x << "," << o.rotation.y << "," << o.rotation.z << "]" - << ",\"scale\":" << o.scale - << "}" << (i + 1 < objects_.size() ? "," : "") << "\n"; - } - f << "]\n"; + f << arr.dump(2) << "\n"; LOG_INFO("Objects saved: ", path, " (", objects_.size(), " objects)"); return true; } @@ -167,64 +170,44 @@ bool ObjectPlacer::saveToFile(const std::string& path) const { bool ObjectPlacer::loadFromFile(const std::string& path) { std::ifstream f(path); if (!f) return false; - std::string content((std::istreambuf_iterator(f)), std::istreambuf_iterator()); - objects_.clear(); - undoStack_.clear(); - selectedIdx_ = -1; + try { + auto arr = nlohmann::json::parse(f); + if (!arr.is_array()) return false; - size_t start = 0; - while ((start = content.find('{', start)) != std::string::npos) { - auto end = content.find('}', start); - if (end == std::string::npos) break; - std::string block = content.substr(start, end - start + 1); + objects_.clear(); + undoStack_.clear(); + selectedIdx_ = -1; - PlacedObject obj; - // Parse type - auto tp = block.find("\"type\":"); - if (tp != std::string::npos) obj.type = static_cast(std::stoi(block.substr(tp + 7))); + for (const auto& jo : arr) { + PlacedObject obj; + obj.type = static_cast(jo.value("type", 0)); + obj.path = jo.value("path", ""); + obj.scale = jo.value("scale", 1.0f); - // Parse path - auto pp = block.find("\"path\":\""); - if (pp != std::string::npos) { - pp += 8; - auto pe = block.find('"', pp); - if (pe != std::string::npos) obj.path = block.substr(pp, pe - pp); + if (jo.contains("pos") && jo["pos"].is_array() && jo["pos"].size() >= 3) { + obj.position = glm::vec3(jo["pos"][0].get(), + jo["pos"][1].get(), + jo["pos"][2].get()); + } + if (jo.contains("rot") && jo["rot"].is_array() && jo["rot"].size() >= 3) { + obj.rotation = glm::vec3(jo["rot"][0].get(), + jo["rot"][1].get(), + jo["rot"][2].get()); + } + + if (!obj.path.empty()) { + obj.uniqueId = nextUniqueId(); + objects_.push_back(obj); + } } - // Parse pos array - auto posP = block.find("\"pos\":["); - if (posP != std::string::npos) { - posP += 7; - obj.position.x = std::stof(block.substr(posP)); - posP = block.find(',', posP) + 1; - obj.position.y = std::stof(block.substr(posP)); - posP = block.find(',', posP) + 1; - obj.position.z = std::stof(block.substr(posP)); - } - - // Parse rot array - auto rotP = block.find("\"rot\":["); - if (rotP != std::string::npos) { - rotP += 7; - obj.rotation.x = std::stof(block.substr(rotP)); - rotP = block.find(',', rotP) + 1; - obj.rotation.y = std::stof(block.substr(rotP)); - rotP = block.find(',', rotP) + 1; - obj.rotation.z = std::stof(block.substr(rotP)); - } - - auto scP = block.find("\"scale\":"); - if (scP != std::string::npos) obj.scale = std::stof(block.substr(scP + 8)); - - if (!obj.path.empty()) { - obj.uniqueId = nextUniqueId(); - objects_.push_back(obj); - } - start = end + 1; + LOG_INFO("Objects loaded: ", path, " (", objects_.size(), " objects)"); + return true; + } catch (const std::exception& e) { + LOG_ERROR("Failed to parse objects file: ", e.what()); + return false; } - LOG_INFO("Objects loaded: ", path, " (", objects_.size(), " objects)"); - return true; } void ObjectPlacer::syncToTerrain() { diff --git a/tools/editor/zone_manifest.cpp b/tools/editor/zone_manifest.cpp index af46bcc7..2104f335 100644 --- a/tools/editor/zone_manifest.cpp +++ b/tools/editor/zone_manifest.cpp @@ -1,5 +1,6 @@ #include "zone_manifest.hpp" #include "core/logger.hpp" +#include #include #include #include @@ -12,44 +13,40 @@ bool ZoneManifest::save(const std::string& path) const { auto dir = std::filesystem::path(path).parent_path(); if (!dir.empty()) std::filesystem::create_directories(dir); - std::ofstream f(path); - if (!f) { LOG_ERROR("Failed to write zone manifest: ", path); return false; } + nlohmann::json j; + j["mapName"] = mapName; + j["displayName"] = displayName; + j["mapId"] = mapId; + j["biome"] = biome; + j["baseHeight"] = baseHeight; + j["hasCreatures"] = hasCreatures; + j["description"] = description; + j["editorVersion"] = "1.0.0"; - f << "{\n"; - f << " \"mapName\": \"" << mapName << "\",\n"; - f << " \"displayName\": \"" << displayName << "\",\n"; - f << " \"mapId\": " << mapId << ",\n"; - f << " \"biome\": \"" << biome << "\",\n"; - f << " \"baseHeight\": " << baseHeight << ",\n"; - f << " \"hasCreatures\": " << (hasCreatures ? "true" : "false") << ",\n"; - f << " \"description\": \"" << description << "\",\n"; - f << " \"editorVersion\": \"0.9.0\",\n"; - // Add export timestamp { auto now = std::chrono::system_clock::now(); auto time = std::chrono::system_clock::to_time_t(now); char timeBuf[32]; std::strftime(timeBuf, sizeof(timeBuf), "%Y-%m-%dT%H:%M:%S", std::localtime(&time)); - f << " \"exportTime\": \"" << timeBuf << "\",\n"; + j["exportTime"] = timeBuf; } - f << " \"tiles\": ["; - for (size_t i = 0; i < tiles.size(); i++) { - f << "[" << tiles[i].first << "," << tiles[i].second << "]"; - if (i + 1 < tiles.size()) f << ","; + + nlohmann::json tilesArr = nlohmann::json::array(); + for (const auto& t : tiles) tilesArr.push_back({t.first, t.second}); + j["tiles"] = tilesArr; + + nlohmann::json files; + files["wdt"] = mapName + ".wdt"; + for (const auto& t : tiles) { + std::string key = "adt_" + std::to_string(t.first) + "_" + std::to_string(t.second); + files[key] = mapName + "_" + std::to_string(t.first) + "_" + std::to_string(t.second) + ".adt"; } - f << "],\n"; - f << " \"files\": {\n"; - f << " \"wdt\": \"" << mapName << ".wdt\",\n"; - for (size_t i = 0; i < tiles.size(); i++) { - f << " \"adt_" << tiles[i].first << "_" << tiles[i].second << "\": \"" - << mapName << "_" << tiles[i].first << "_" << tiles[i].second << ".adt\""; - if (i + 1 < tiles.size() || hasCreatures) f << ","; - f << "\n"; - } - if (hasCreatures) - f << " \"creatures\": \"creatures.json\"\n"; - f << " }\n"; - f << "}\n"; + if (hasCreatures) files["creatures"] = "creatures.json"; + j["files"] = files; + + std::ofstream f(path); + if (!f) { LOG_ERROR("Failed to write zone manifest: ", path); return false; } + f << j.dump(2) << "\n"; LOG_INFO("Zone manifest saved: ", path); return true; @@ -58,30 +55,32 @@ bool ZoneManifest::save(const std::string& path) const { bool ZoneManifest::load(const std::string& path) { std::ifstream f(path); if (!f) return false; - std::string content((std::istreambuf_iterator(f)), - std::istreambuf_iterator()); - auto findStr = [&](const std::string& key) -> std::string { - auto pos = content.find("\"" + key + "\""); - if (pos == std::string::npos) return ""; - pos = content.find('"', content.find(':', pos) + 1); - if (pos == std::string::npos) return ""; - auto end = content.find('"', pos + 1); - return content.substr(pos + 1, end - pos - 1); - }; + try { + auto j = nlohmann::json::parse(f); - mapName = findStr("mapName"); - displayName = findStr("displayName"); - biome = findStr("biome"); - description = findStr("description"); + mapName = j.value("mapName", ""); + if (mapName.empty()) mapName = j.value("name", ""); + displayName = j.value("displayName", mapName); + biome = j.value("biome", ""); + description = j.value("description", ""); + mapId = j.value("mapId", 9000u); + baseHeight = j.value("baseHeight", 100.0f); + hasCreatures = j.value("hasCreatures", false); - auto numPos = content.find("\"mapId\""); - if (numPos != std::string::npos) { - numPos = content.find(':', numPos); - mapId = static_cast(std::stoi(content.substr(numPos + 1))); + tiles.clear(); + if (j.contains("tiles") && j["tiles"].is_array()) { + for (const auto& t : j["tiles"]) { + if (t.is_array() && t.size() >= 2) + tiles.push_back({t[0].get(), t[1].get()}); + } + } + + return !mapName.empty(); + } catch (const std::exception& e) { + LOG_ERROR("Failed to parse zone manifest: ", e.what()); + return false; } - - return !mapName.empty(); } } // namespace editor