mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-06 00:53:52 +00:00
fix(editor): harden JSON handling, quest loading, and content validation
- Quest editor: add loadFromFile() with nlohmann/json, chain validation with circular reference detection, wire into ADT load and save pipeline - Project: replace naive substring JSON parsing with nlohmann/json for both save() and load(), fix shell injection in gitCommit() - Content pack: replace manual JSON with nlohmann/json, validate binary format magic numbers (WHM1/WOM1/WOB1), add WOB to openFormatScore (now scores 0-6), mark invalid files with (!) in summary
This commit is contained in:
parent
4fc0361f7a
commit
2eec089ef5
6 changed files with 265 additions and 153 deletions
|
|
@ -1,5 +1,6 @@
|
|||
#include "content_pack.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <cstring>
|
||||
|
|
@ -39,22 +40,20 @@ bool ContentPacker::packZone(const std::string& outputDir, const std::string& ma
|
|||
}
|
||||
|
||||
// Build info JSON
|
||||
std::string infoJson = "{\n";
|
||||
infoJson += " \"format\": \"" + info.format + "\",\n";
|
||||
infoJson += " \"name\": \"" + info.name + "\",\n";
|
||||
infoJson += " \"author\": \"" + info.author + "\",\n";
|
||||
infoJson += " \"description\": \"" + info.description + "\",\n";
|
||||
infoJson += " \"version\": \"" + info.version + "\",\n";
|
||||
infoJson += " \"mapId\": " + std::to_string(info.mapId) + ",\n";
|
||||
infoJson += " \"fileCount\": " + std::to_string(files.size()) + ",\n";
|
||||
infoJson += " \"files\": [\n";
|
||||
for (size_t i = 0; i < files.size(); i++) {
|
||||
auto fsize = fs::file_size(files[i].second);
|
||||
infoJson += " {\"path\": \"" + files[i].first + "\", \"size\": " + std::to_string(fsize) + "}";
|
||||
if (i + 1 < files.size()) infoJson += ",";
|
||||
infoJson += "\n";
|
||||
nlohmann::json infoObj;
|
||||
infoObj["format"] = info.format;
|
||||
infoObj["name"] = info.name;
|
||||
infoObj["author"] = info.author;
|
||||
infoObj["description"] = info.description;
|
||||
infoObj["version"] = info.version;
|
||||
infoObj["mapId"] = info.mapId;
|
||||
infoObj["fileCount"] = files.size();
|
||||
nlohmann::json fileArr = nlohmann::json::array();
|
||||
for (const auto& [rel, full] : files) {
|
||||
fileArr.push_back({{"path", rel}, {"size", fs::file_size(full)}});
|
||||
}
|
||||
infoJson += " ]\n}\n";
|
||||
infoObj["files"] = fileArr;
|
||||
std::string infoJson = infoObj.dump(2);
|
||||
|
||||
// Write WCP file
|
||||
std::ofstream out(destPath, std::ios::binary);
|
||||
|
|
@ -149,62 +148,95 @@ bool ContentPacker::readInfo(const std::string& wcpPath, ContentPackInfo& info)
|
|||
in.read(reinterpret_cast<char*>(&fileCount), 4);
|
||||
in.read(reinterpret_cast<char*>(&infoSize), 4);
|
||||
|
||||
std::string json(infoSize, '\0');
|
||||
in.read(json.data(), infoSize);
|
||||
std::string jsonStr(infoSize, '\0');
|
||||
in.read(jsonStr.data(), infoSize);
|
||||
|
||||
// Parse basic fields
|
||||
auto findStr = [&](const std::string& key) -> std::string {
|
||||
auto pos = json.find("\"" + key + "\"");
|
||||
if (pos == std::string::npos) return "";
|
||||
pos = json.find('"', json.find(':', pos) + 1);
|
||||
if (pos == std::string::npos) return "";
|
||||
auto end = json.find('"', pos + 1);
|
||||
return json.substr(pos + 1, end - pos - 1);
|
||||
};
|
||||
|
||||
info.name = findStr("name");
|
||||
info.author = findStr("author");
|
||||
info.description = findStr("description");
|
||||
info.version = findStr("version");
|
||||
info.format = findStr("format");
|
||||
try {
|
||||
auto j = nlohmann::json::parse(jsonStr);
|
||||
info.name = j.value("name", "");
|
||||
info.author = j.value("author", "");
|
||||
info.description = j.value("description", "");
|
||||
info.version = j.value("version", "");
|
||||
info.format = j.value("format", "");
|
||||
info.mapId = j.value("mapId", 9000u);
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool checkMagic(const std::string& path, uint32_t expectedMagic) {
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
if (!f) return false;
|
||||
uint32_t magic = 0;
|
||||
f.read(reinterpret_cast<char*>(&magic), 4);
|
||||
return magic == expectedMagic;
|
||||
}
|
||||
|
||||
ContentPacker::ValidationResult ContentPacker::validateZone(const std::string& zoneDir) {
|
||||
namespace fs = std::filesystem;
|
||||
ValidationResult r;
|
||||
if (!fs::exists(zoneDir)) return r;
|
||||
|
||||
static constexpr uint32_t WHM_MAGIC = 0x314D4857; // "WHM1"
|
||||
static constexpr uint32_t WOM_MAGIC = 0x314D4F57; // "WOM1"
|
||||
static constexpr uint32_t WOB_MAGIC = 0x31424F57; // "WOB1"
|
||||
|
||||
for (auto& entry : fs::recursive_directory_iterator(zoneDir)) {
|
||||
if (!entry.is_regular_file()) continue;
|
||||
std::string ext = entry.path().extension().string();
|
||||
std::string name = entry.path().filename().string();
|
||||
std::string fname = entry.path().filename().string();
|
||||
if (ext == ".wot") r.hasWot = true;
|
||||
if (ext == ".whm") r.hasWhm = true;
|
||||
if (ext == ".wom") r.hasWom = true;
|
||||
if (ext == ".whm") {
|
||||
r.hasWhm = true;
|
||||
if (checkMagic(entry.path().string(), WHM_MAGIC)) r.whmValid = true;
|
||||
}
|
||||
if (ext == ".wom") {
|
||||
r.hasWom = true;
|
||||
if (checkMagic(entry.path().string(), WOM_MAGIC)) r.womValid = true;
|
||||
}
|
||||
if (ext == ".wob") {
|
||||
r.hasWob = true;
|
||||
if (checkMagic(entry.path().string(), WOB_MAGIC)) r.wobValid = true;
|
||||
}
|
||||
if (ext == ".png") r.hasPng = true;
|
||||
if (name == "zone.json") r.hasZoneJson = true;
|
||||
if (name == "creatures.json") r.hasCreatures = true;
|
||||
if (name == "quests.json") r.hasQuests = true;
|
||||
if (name == "objects.json") r.hasObjects = true;
|
||||
if (fname == "zone.json") r.hasZoneJson = true;
|
||||
if (fname == "creatures.json") r.hasCreatures = true;
|
||||
if (fname == "quests.json") r.hasQuests = true;
|
||||
if (fname == "objects.json") r.hasObjects = true;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
int ContentPacker::ValidationResult::openFormatScore() const {
|
||||
int score = 0;
|
||||
if (hasWot) score++; if (hasWhm) score++; if (hasZoneJson) score++;
|
||||
if (hasPng) score++; if (hasWom) score++;
|
||||
return score; // max 5 for fully open
|
||||
if (hasWot) score++;
|
||||
if (hasWhm && whmValid) score++;
|
||||
if (hasZoneJson) score++;
|
||||
if (hasPng) score++;
|
||||
if (hasWom && womValid) score++;
|
||||
if (hasWob && wobValid) score++;
|
||||
return score; // max 6 for fully open
|
||||
}
|
||||
|
||||
std::string ContentPacker::ValidationResult::summary() const {
|
||||
std::string s;
|
||||
s += hasWot ? "WOT " : ""; s += hasWhm ? "WHM " : "";
|
||||
s += hasZoneJson ? "zone.json " : ""; s += hasPng ? "PNG " : "";
|
||||
s += hasWom ? "WOM " : ""; s += hasCreatures ? "creatures " : "";
|
||||
s += hasQuests ? "quests " : ""; s += hasObjects ? "objects " : "";
|
||||
auto add = [&](bool has, bool valid, const char* name) {
|
||||
if (!has) return;
|
||||
s += name;
|
||||
if (!valid) s += "(!)";
|
||||
s += " ";
|
||||
};
|
||||
add(hasWot, true, "WOT");
|
||||
add(hasWhm, whmValid, "WHM");
|
||||
add(hasWom, womValid, "WOM");
|
||||
add(hasWob, wobValid, "WOB");
|
||||
if (hasZoneJson) s += "zone.json ";
|
||||
if (hasPng) s += "PNG ";
|
||||
if (hasCreatures) s += "creatures ";
|
||||
if (hasQuests) s += "quests ";
|
||||
if (hasObjects) s += "objects ";
|
||||
return s.empty() ? "(empty)" : s;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,8 +38,9 @@ public:
|
|||
// Validate that a zone directory has all required open format files
|
||||
struct ValidationResult {
|
||||
bool hasWot = false, hasWhm = false, hasZoneJson = false;
|
||||
bool hasPng = false, hasWom = false, hasCreatures = false;
|
||||
bool hasQuests = false, hasObjects = false;
|
||||
bool hasPng = false, hasWom = false, hasWob = false;
|
||||
bool hasCreatures = false, hasQuests = false, hasObjects = false;
|
||||
bool whmValid = false, womValid = false, wobValid = false;
|
||||
int openFormatScore() const;
|
||||
std::string summary() const;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -653,6 +653,8 @@ void EditorApp::loadADT(const std::string& mapName, int tileX, int tileY) {
|
|||
showToast("Loaded " + std::to_string(objectPlacer_.objectCount()) + " objects");
|
||||
if (npcSpawner_.loadFromFile(outBase + "/creatures.json"))
|
||||
showToast("Loaded " + std::to_string(npcSpawner_.spawnCount()) + " NPCs");
|
||||
if (questEditor_.loadFromFile(outBase + "/quests.json"))
|
||||
showToast("Loaded " + std::to_string(questEditor_.questCount()) + " quests");
|
||||
if (objectPlacer_.objectCount() > 0 || npcSpawner_.spawnCount() > 0)
|
||||
objectsDirty_ = true;
|
||||
}
|
||||
|
|
@ -726,6 +728,11 @@ void EditorApp::exportZone(const std::string& outputDir) {
|
|||
if (questEditor_.questCount() > 0) {
|
||||
std::string questPath = base + "/quests.json";
|
||||
questEditor_.saveToFile(questPath);
|
||||
std::vector<std::string> chainErrors;
|
||||
if (!questEditor_.validateChains(chainErrors)) {
|
||||
for (const auto& err : chainErrors)
|
||||
LOG_WARNING("Quest chain issue: ", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Update WDT with additional tiles from adjacent exports
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "editor_project.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
|
||||
|
|
@ -10,28 +11,25 @@ bool EditorProject::save(const std::string& path) const {
|
|||
namespace fs = std::filesystem;
|
||||
fs::create_directories(fs::path(path).parent_path());
|
||||
|
||||
nlohmann::json j;
|
||||
j["format"] = "wowee-project-1.0";
|
||||
j["name"] = name;
|
||||
j["author"] = author;
|
||||
j["description"] = description;
|
||||
j["version"] = version;
|
||||
j["startMapId"] = startMapId;
|
||||
|
||||
nlohmann::json zarr = nlohmann::json::array();
|
||||
for (const auto& z : zones) {
|
||||
zarr.push_back({{"mapName", z.mapName}, {"tileX", z.tileX},
|
||||
{"tileY", z.tileY}, {"biome", z.biome},
|
||||
{"description", z.description}});
|
||||
}
|
||||
j["zones"] = zarr;
|
||||
|
||||
std::ofstream f(path);
|
||||
if (!f) return false;
|
||||
|
||||
f << "{\n";
|
||||
f << " \"format\": \"wowee-project-1.0\",\n";
|
||||
f << " \"name\": \"" << name << "\",\n";
|
||||
f << " \"author\": \"" << author << "\",\n";
|
||||
f << " \"description\": \"" << description << "\",\n";
|
||||
f << " \"version\": \"" << version << "\",\n";
|
||||
f << " \"startMapId\": " << startMapId << ",\n";
|
||||
f << " \"zones\": [\n";
|
||||
for (size_t i = 0; i < zones.size(); i++) {
|
||||
const auto& z = zones[i];
|
||||
f << " {\"mapName\": \"" << z.mapName << "\""
|
||||
<< ", \"tileX\": " << z.tileX
|
||||
<< ", \"tileY\": " << z.tileY
|
||||
<< ", \"biome\": \"" << z.biome << "\""
|
||||
<< ", \"description\": \"" << z.description << "\""
|
||||
<< "}" << (i + 1 < zones.size() ? "," : "") << "\n";
|
||||
}
|
||||
f << " ]\n";
|
||||
f << "}\n";
|
||||
f << j.dump(2) << "\n";
|
||||
|
||||
LOG_INFO("Project saved: ", path, " (", zones.size(), " zones)");
|
||||
return true;
|
||||
|
|
@ -40,64 +38,36 @@ bool EditorProject::save(const std::string& path) const {
|
|||
bool EditorProject::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 {
|
||||
nlohmann::json j = nlohmann::json::parse(f);
|
||||
|
||||
name = findStr("name");
|
||||
author = findStr("author");
|
||||
description = findStr("description");
|
||||
version = findStr("version");
|
||||
projectDir = std::filesystem::path(path).parent_path().string();
|
||||
name = j.value("name", "");
|
||||
author = j.value("author", "");
|
||||
description = j.value("description", "");
|
||||
version = j.value("version", "1.0.0");
|
||||
startMapId = j.value("startMapId", 9000u);
|
||||
projectDir = std::filesystem::path(path).parent_path().string();
|
||||
|
||||
// Parse zones from JSON array
|
||||
zones.clear();
|
||||
size_t zonesPos = content.find("\"zones\"");
|
||||
if (zonesPos != std::string::npos) {
|
||||
size_t start = zonesPos;
|
||||
while ((start = content.find('{', start + 1)) != std::string::npos) {
|
||||
auto end = content.find('}', start);
|
||||
if (end == std::string::npos) break;
|
||||
// Check we're still inside the zones array
|
||||
auto closeBracket = content.find(']', zonesPos);
|
||||
if (start > closeBracket) break;
|
||||
|
||||
std::string block = content.substr(start, end - start + 1);
|
||||
ProjectZone z;
|
||||
|
||||
auto blockFindStr = [&](const std::string& key) -> std::string {
|
||||
auto p = block.find("\"" + key + "\"");
|
||||
if (p == std::string::npos) return "";
|
||||
p = block.find('"', block.find(':', p) + 1);
|
||||
if (p == std::string::npos) return "";
|
||||
auto e = block.find('"', p + 1);
|
||||
return block.substr(p + 1, e - p - 1);
|
||||
};
|
||||
|
||||
z.mapName = blockFindStr("mapName");
|
||||
z.biome = blockFindStr("biome");
|
||||
z.description = blockFindStr("description");
|
||||
|
||||
auto txPos = block.find("\"tileX\":");
|
||||
if (txPos != std::string::npos) z.tileX = std::stoi(block.substr(txPos + 8));
|
||||
auto tyPos = block.find("\"tileY\":");
|
||||
if (tyPos != std::string::npos) z.tileY = std::stoi(block.substr(tyPos + 8));
|
||||
|
||||
if (!z.mapName.empty()) zones.push_back(z);
|
||||
start = end;
|
||||
zones.clear();
|
||||
if (j.contains("zones") && j["zones"].is_array()) {
|
||||
for (const auto& jz : j["zones"]) {
|
||||
ProjectZone z;
|
||||
z.mapName = jz.value("mapName", "");
|
||||
z.tileX = jz.value("tileX", 32);
|
||||
z.tileY = jz.value("tileY", 32);
|
||||
z.biome = jz.value("biome", "");
|
||||
z.description = jz.value("description", "");
|
||||
if (!z.mapName.empty()) zones.push_back(z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Project loaded: ", path, " (", name, ", ", zones.size(), " zones)");
|
||||
return true;
|
||||
LOG_INFO("Project loaded: ", path, " (", name, ", ", zones.size(), " zones)");
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR("Failed to load project: ", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool EditorProject::initGitRepo() const {
|
||||
|
|
@ -109,8 +79,14 @@ bool EditorProject::initGitRepo() const {
|
|||
|
||||
bool EditorProject::gitCommit(const std::string& message) const {
|
||||
if (projectDir.empty()) return false;
|
||||
int ret = std::system(("cd \"" + projectDir + "\" && git add -A && "
|
||||
"git commit -m \"" + message + "\"").c_str());
|
||||
// Sanitize commit message to prevent shell injection
|
||||
std::string safe;
|
||||
for (char c : message) {
|
||||
if (c == '\'' || c == '\\') safe += '\\';
|
||||
safe += c;
|
||||
}
|
||||
int ret = std::system(("cd '" + projectDir + "' && git add -A && "
|
||||
"git commit -m '" + safe + "'").c_str());
|
||||
return ret == 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
#include "quest_editor.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
|
|
@ -27,42 +29,131 @@ bool QuestEditor::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& q : quests_) {
|
||||
nlohmann::json jq;
|
||||
jq["id"] = q.id;
|
||||
jq["title"] = q.title;
|
||||
jq["description"] = q.description;
|
||||
jq["completionText"] = q.completionText;
|
||||
jq["requiredLevel"] = q.requiredLevel;
|
||||
jq["questGiverNpcId"] = q.questGiverNpcId;
|
||||
jq["turnInNpcId"] = q.turnInNpcId;
|
||||
jq["nextQuestId"] = q.nextQuestId;
|
||||
jq["reward"] = {{"xp", q.reward.xp}, {"gold", q.reward.gold},
|
||||
{"silver", q.reward.silver}, {"copper", q.reward.copper}};
|
||||
nlohmann::json items = nlohmann::json::array();
|
||||
for (const auto& item : q.reward.itemRewards) items.push_back(item);
|
||||
jq["reward"]["items"] = items;
|
||||
|
||||
nlohmann::json objs = nlohmann::json::array();
|
||||
for (const auto& obj : q.objectives) {
|
||||
objs.push_back({{"type", static_cast<int>(obj.type)},
|
||||
{"desc", obj.description},
|
||||
{"target", obj.targetName},
|
||||
{"count", obj.targetCount}});
|
||||
}
|
||||
jq["objectives"] = objs;
|
||||
arr.push_back(jq);
|
||||
}
|
||||
|
||||
std::ofstream f(path);
|
||||
if (!f) return false;
|
||||
|
||||
f << "[\n";
|
||||
for (size_t i = 0; i < quests_.size(); i++) {
|
||||
const auto& q = quests_[i];
|
||||
f << " {\n";
|
||||
f << " \"id\": " << q.id << ",\n";
|
||||
f << " \"title\": \"" << q.title << "\",\n";
|
||||
f << " \"description\": \"" << q.description << "\",\n";
|
||||
f << " \"completionText\": \"" << q.completionText << "\",\n";
|
||||
f << " \"requiredLevel\": " << q.requiredLevel << ",\n";
|
||||
f << " \"questGiverNpcId\": " << q.questGiverNpcId << ",\n";
|
||||
f << " \"turnInNpcId\": " << q.turnInNpcId << ",\n";
|
||||
f << " \"nextQuestId\": " << q.nextQuestId << ",\n";
|
||||
f << " \"reward\": {\"xp\":" << q.reward.xp
|
||||
<< ",\"gold\":" << q.reward.gold
|
||||
<< ",\"silver\":" << q.reward.silver
|
||||
<< ",\"copper\":" << q.reward.copper << "},\n";
|
||||
f << " \"objectives\": [";
|
||||
for (size_t j = 0; j < q.objectives.size(); j++) {
|
||||
const auto& obj = q.objectives[j];
|
||||
f << "{\"type\":" << static_cast<int>(obj.type)
|
||||
<< ",\"desc\":\"" << obj.description << "\""
|
||||
<< ",\"target\":\"" << obj.targetName << "\""
|
||||
<< ",\"count\":" << obj.targetCount << "}";
|
||||
if (j + 1 < q.objectives.size()) f << ",";
|
||||
}
|
||||
f << "]\n";
|
||||
f << " }" << (i + 1 < quests_.size() ? "," : "") << "\n";
|
||||
}
|
||||
f << "]\n";
|
||||
f << arr.dump(2) << "\n";
|
||||
|
||||
LOG_INFO("Quests saved: ", path, " (", quests_.size(), " quests)");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QuestEditor::loadFromFile(const std::string& path) {
|
||||
std::ifstream f(path);
|
||||
if (!f) return false;
|
||||
|
||||
try {
|
||||
nlohmann::json arr = nlohmann::json::parse(f);
|
||||
if (!arr.is_array()) return false;
|
||||
|
||||
quests_.clear();
|
||||
uint32_t maxId = 0;
|
||||
|
||||
for (const auto& jq : arr) {
|
||||
Quest q;
|
||||
q.id = jq.value("id", 0u);
|
||||
q.title = jq.value("title", "Untitled");
|
||||
q.description = jq.value("description", "");
|
||||
q.completionText = jq.value("completionText", "");
|
||||
q.requiredLevel = jq.value("requiredLevel", 1u);
|
||||
q.questGiverNpcId = jq.value("questGiverNpcId", 0u);
|
||||
q.turnInNpcId = jq.value("turnInNpcId", 0u);
|
||||
q.nextQuestId = jq.value("nextQuestId", 0u);
|
||||
|
||||
if (jq.contains("reward")) {
|
||||
const auto& jr = jq["reward"];
|
||||
q.reward.xp = jr.value("xp", 100u);
|
||||
q.reward.gold = jr.value("gold", 0u);
|
||||
q.reward.silver = jr.value("silver", 0u);
|
||||
q.reward.copper = jr.value("copper", 0u);
|
||||
if (jr.contains("items") && jr["items"].is_array()) {
|
||||
for (const auto& item : jr["items"])
|
||||
q.reward.itemRewards.push_back(item.get<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
if (jq.contains("objectives") && jq["objectives"].is_array()) {
|
||||
for (const auto& jo : jq["objectives"]) {
|
||||
QuestObjective obj;
|
||||
obj.type = static_cast<QuestObjectiveType>(jo.value("type", 0));
|
||||
obj.description = jo.value("desc", "");
|
||||
obj.targetName = jo.value("target", "");
|
||||
obj.targetCount = jo.value("count", 1u);
|
||||
q.objectives.push_back(obj);
|
||||
}
|
||||
}
|
||||
|
||||
if (q.id > maxId) maxId = q.id;
|
||||
quests_.push_back(q);
|
||||
}
|
||||
|
||||
nextId_ = maxId + 1;
|
||||
LOG_INFO("Quests loaded: ", path, " (", quests_.size(), " quests)");
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR("Failed to load quests from ", path, ": ", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool QuestEditor::validateChains(std::vector<std::string>& errors) const {
|
||||
errors.clear();
|
||||
std::unordered_set<uint32_t> validIds;
|
||||
for (const auto& q : quests_) validIds.insert(q.id);
|
||||
|
||||
for (const auto& q : quests_) {
|
||||
if (q.nextQuestId != 0 && validIds.find(q.nextQuestId) == validIds.end()) {
|
||||
errors.push_back("Quest [" + std::to_string(q.id) + "] \"" + q.title +
|
||||
"\" chains to non-existent quest " + std::to_string(q.nextQuestId));
|
||||
}
|
||||
|
||||
// Circular chain detection
|
||||
if (q.nextQuestId != 0) {
|
||||
std::unordered_set<uint32_t> visited;
|
||||
uint32_t current = q.id;
|
||||
while (current != 0) {
|
||||
if (!visited.insert(current).second) {
|
||||
errors.push_back("Circular quest chain detected starting from quest [" +
|
||||
std::to_string(q.id) + "] \"" + q.title + "\"");
|
||||
break;
|
||||
}
|
||||
uint32_t next = 0;
|
||||
for (const auto& other : quests_) {
|
||||
if (other.id == current) { next = other.nextQuestId; break; }
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.empty();
|
||||
}
|
||||
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -54,6 +54,11 @@ public:
|
|||
size_t questCount() const { return quests_.size(); }
|
||||
|
||||
bool saveToFile(const std::string& path) const;
|
||||
bool loadFromFile(const std::string& path);
|
||||
void clear() { quests_.clear(); nextId_ = 1; }
|
||||
|
||||
// Quest chain validation
|
||||
bool validateChains(std::vector<std::string>& errors) const;
|
||||
|
||||
Quest& getTemplate() { return template_; }
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue