refactor: migrate all remaining JSON to nlohmann/json

- npc_spawner: save/load with proper JSON (25+ fields + patrol paths)
- zone_manifest: save/load with nlohmann (was naive string concat/parse)
  - load now parses all fields: mapId, baseHeight, tiles, hasCreatures
- custom_zone_discovery: parse zone.json with nlohmann, extract mapId
  and tile coordinates (was only reading name/author/description)
- object_placer: save/load with nlohmann (was substring parsing)
- editor_app: stats.json export uses nlohmann, score display now /6

Zero naive JSON string concatenation remains in the editor codebase.
This commit is contained in:
Kelsi 2026-05-05 13:10:07 -07:00
parent 815787933b
commit 08500384e2
5 changed files with 223 additions and 268 deletions

View file

@ -1,5 +1,6 @@
#include "pipeline/custom_zone_discovery.hpp"
#include "core/logger.hpp"
#include <nlohmann/json.hpp>
#include <fstream>
#include <filesystem>
@ -29,30 +30,34 @@ std::vector<CustomZoneInfo> CustomZoneDiscovery::scanDirectory(const std::string
std::ifstream f(zoneJson);
if (!f) continue;
std::string content((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
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<int>(), t[1].get<int>()});
}
}
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());
}
}

View file

@ -9,6 +9,7 @@
#include "pipeline/wowee_building.hpp"
#include "pipeline/wmo_loader.hpp"
#include "core/coordinates.hpp"
#include <nlohmann/json.hpp>
#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");

View file

@ -1,5 +1,6 @@
#include "npc_spawner.hpp"
#include "core/logger.hpp"
#include <nlohmann/json.hpp>
#include <fstream>
#include <sstream>
#include <cmath>
@ -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<int>(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<int>(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<char>(f)),
std::istreambuf_iterator<char>());
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<CreatureBehavior>(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<float>(),
js["position"][1].get<float>(),
js["position"][2].get<float>());
}
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<uint32_t>(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<uint32_t>(std::max(1.0f, findNum(block, "level")));
s.health = static_cast<uint32_t>(std::max(1.0f, findNum(block, "health")));
s.mana = static_cast<uint32_t>(findNum(block, "mana"));
s.minDamage = static_cast<uint32_t>(findNum(block, "minDamage"));
s.maxDamage = static_cast<uint32_t>(findNum(block, "maxDamage"));
s.armor = static_cast<uint32_t>(findNum(block, "armor"));
s.faction = static_cast<uint32_t>(findNum(block, "faction"));
s.behavior = static_cast<CreatureBehavior>(static_cast<int>(findNum(block, "behavior")));
s.wanderRadius = findNum(block, "wanderRadius");
s.aggroRadius = findNum(block, "aggroRadius");
s.leashRadius = findNum(block, "leashRadius");
s.respawnTimeMs = static_cast<uint32_t>(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<float>(), pt[1].get<float>(), pt[2].get<float>());
pp.waitTimeMs = pt[3].get<uint32_t>();
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

View file

@ -1,5 +1,6 @@
#include "object_placer.hpp"
#include "core/logger.hpp"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <cmath>
#include <fstream>
@ -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<int>(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<int>(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<char>(f)), std::istreambuf_iterator<char>());
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<PlaceableType>(std::stoi(block.substr(tp + 7)));
for (const auto& jo : arr) {
PlacedObject obj;
obj.type = static_cast<PlaceableType>(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<float>(),
jo["pos"][1].get<float>(),
jo["pos"][2].get<float>());
}
if (jo.contains("rot") && jo["rot"].is_array() && jo["rot"].size() >= 3) {
obj.rotation = glm::vec3(jo["rot"][0].get<float>(),
jo["rot"][1].get<float>(),
jo["rot"][2].get<float>());
}
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() {

View file

@ -1,5 +1,6 @@
#include "zone_manifest.hpp"
#include "core/logger.hpp"
#include <nlohmann/json.hpp>
#include <fstream>
#include <filesystem>
#include <chrono>
@ -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<char>(f)),
std::istreambuf_iterator<char>());
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<uint32_t>(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<int>(), t[1].get<int>()});
}
}
return !mapName.empty();
} catch (const std::exception& e) {
LOG_ERROR("Failed to parse zone manifest: ", e.what());
return false;
}
return !mapName.empty();
}
} // namespace editor