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 "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