#include "quest_editor.hpp" #include "core/logger.hpp" #include #include #include #include namespace wowee { namespace editor { void QuestEditor::addQuest(const Quest& q) { Quest quest = q; quest.id = nextId_++; quests_.push_back(quest); LOG_INFO("Quest added: [", quest.id, "] ", quest.title); } void QuestEditor::removeQuest(int index) { if (index >= 0 && index < static_cast(quests_.size())) quests_.erase(quests_.begin() + index); } Quest* QuestEditor::getQuest(int index) { if (index < 0 || index >= static_cast(quests_.size())) return nullptr; return &quests_[index]; } 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(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 << 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); // WoW levels 1-80 (WotLK). Cap to keep AzerothCore happy and // catch obvious typos like "999". if (q.requiredLevel == 0) q.requiredLevel = 1; if (q.requiredLevel > 255) q.requiredLevel = 80; 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); // Reward sanity caps. Highest WoW quest XP ~50k; gold realistic // cap is hundreds. Catches typo entries like "100000000 gold". if (q.reward.xp > 1'000'000) q.reward.xp = 1'000'000; if (q.reward.gold > 10000) q.reward.gold = 10000; if (q.reward.silver > 99) q.reward.silver = 99; if (q.reward.copper > 99) q.reward.copper = 99; if (jr.contains("items") && jr["items"].is_array()) { for (const auto& item : jr["items"]) { // Cap item reward count to 6 (WoW quest_template // RewardItemId[1..6] slot capacity). if (q.reward.itemRewards.size() >= 6) break; q.reward.itemRewards.push_back(item.get()); } } } if (jq.contains("objectives") && jq["objectives"].is_array()) { for (const auto& jo : jq["objectives"]) { QuestObjective obj; int t = jo.value("type", 0); // Clamp to known QuestObjectiveType range to avoid // garbage enum values from edited JSON. if (t < 0 || t > 5) t = 0; obj.type = static_cast(t); obj.description = jo.value("desc", ""); obj.targetName = jo.value("target", ""); obj.targetCount = jo.value("count", 1u); if (obj.targetCount == 0) obj.targetCount = 1; if (obj.targetCount > 1000) obj.targetCount = 1000; // Cap stored objectives to 10 (matches SQL slot capacity) // — also bounds the per-quest memory. if (q.objectives.size() >= 10) break; 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& errors) const { errors.clear(); std::unordered_set 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 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