diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d854667..adb3fa3e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1336,6 +1336,7 @@ add_executable(wowee_editor tools/editor/cli_info_density.cpp tools/editor/cli_info_audio.cpp tools/editor/cli_world_info.cpp + tools/editor/cli_quest_objective.cpp tools/editor/editor_app.cpp tools/editor/editor_camera.cpp tools/editor/editor_viewport.cpp diff --git a/tools/editor/cli_quest_objective.cpp b/tools/editor/cli_quest_objective.cpp new file mode 100644 index 00000000..7ef46f36 --- /dev/null +++ b/tools/editor/cli_quest_objective.cpp @@ -0,0 +1,219 @@ +#include "cli_quest_objective.hpp" + +#include "quest_editor.hpp" + +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +int handleAddQuest(int& i, int argc, char** argv) { + // Append a single quest to a zone's quests.json. + // Args: [giverId] [turnInId] [xp] [level] + std::string zoneDir = argv[++i]; + std::string title = argv[++i]; + namespace fs = std::filesystem; + if (!fs::exists(zoneDir)) { + std::fprintf(stderr, "add-quest: zone '%s' does not exist\n", + zoneDir.c_str()); + return 1; + } + wowee::editor::Quest q; + q.title = title; + // Optional positional args after title. Each is read in order; + // an empty string or '-' stops consumption so users can omit + // later fields. + auto tryReadUint = [&](uint32_t& target) { + if (i + 1 >= argc || argv[i + 1][0] == '-') return false; + try { + target = static_cast<uint32_t>(std::stoul(argv[i + 1])); + ++i; + return true; + } catch (...) { return false; } + }; + tryReadUint(q.questGiverNpcId); + tryReadUint(q.turnInNpcId); + tryReadUint(q.reward.xp); + tryReadUint(q.requiredLevel); + wowee::editor::QuestEditor qe; + std::string path = zoneDir + "/quests.json"; + if (fs::exists(path)) qe.loadFromFile(path); + qe.addQuest(q); + if (!qe.saveToFile(path)) { + std::fprintf(stderr, "add-quest: failed to write %s\n", path.c_str()); + return 1; + } + std::printf("Added quest '%s' to %s (now %zu total)\n", + title.c_str(), path.c_str(), qe.questCount()); + return 0; +} + +int handleAddQuestObjective(int& i, int argc, char** argv) { + // Append a single objective to an existing quest. The quest + // must already exist (use --add-quest first); index is 0-based + // and matches --list-quests output. + std::string zoneDir = argv[++i]; + std::string idxStr = argv[++i]; + std::string typeStr = argv[++i]; + std::string targetName = argv[++i]; + std::string path = zoneDir + "/quests.json"; + if (!std::filesystem::exists(path)) { + std::fprintf(stderr, "add-quest-objective: %s not found — run --add-quest first\n", + path.c_str()); + return 1; + } + int idx; + try { idx = std::stoi(idxStr); } + catch (...) { + std::fprintf(stderr, "add-quest-objective: bad questIdx '%s'\n", idxStr.c_str()); + return 1; + } + using OT = wowee::editor::QuestObjectiveType; + OT type; + if (typeStr == "kill") type = OT::KillCreature; + else if (typeStr == "collect") type = OT::CollectItem; + else if (typeStr == "talk") type = OT::TalkToNPC; + else if (typeStr == "explore") type = OT::ExploreArea; + else if (typeStr == "escort") type = OT::EscortNPC; + else if (typeStr == "use") type = OT::UseObject; + else { + std::fprintf(stderr, + "add-quest-objective: type must be kill/collect/talk/explore/escort/use, got '%s'\n", + typeStr.c_str()); + return 1; + } + uint32_t count = 1; + if (i + 1 < argc && argv[i + 1][0] != '-') { + try { + count = static_cast<uint32_t>(std::stoul(argv[++i])); + if (count == 0) count = 1; + } catch (...) {} + } + wowee::editor::QuestEditor qe; + if (!qe.loadFromFile(path)) { + std::fprintf(stderr, "add-quest-objective: failed to load %s\n", path.c_str()); + return 1; + } + if (idx < 0 || idx >= static_cast<int>(qe.questCount())) { + std::fprintf(stderr, + "add-quest-objective: questIdx %d out of range [0, %zu)\n", + idx, qe.questCount()); + return 1; + } + wowee::editor::QuestObjective obj; + obj.type = type; + obj.targetName = targetName; + obj.targetCount = count; + // Auto-generate a description from type+name+count so addons + // and tooltips have something useful by default. The user can + // edit quests.json directly if they want bespoke prose. + const char* verb = "complete"; + switch (type) { + case OT::KillCreature: verb = "Slay"; break; + case OT::CollectItem: verb = "Collect"; break; + case OT::TalkToNPC: verb = "Talk to"; break; + case OT::ExploreArea: verb = "Explore"; break; + case OT::EscortNPC: verb = "Escort"; break; + case OT::UseObject: verb = "Use"; break; + } + obj.description = std::string(verb) + " " + + (count > 1 ? std::to_string(count) + " " : "") + + targetName; + // Quest is stored by value in the editor's vector; mutate via + // the non-const getter, which gives us a pointer we can write + // through. + wowee::editor::Quest* q = qe.getQuest(idx); + if (!q) { + std::fprintf(stderr, "add-quest-objective: getQuest(%d) returned null\n", idx); + return 1; + } + q->objectives.push_back(obj); + if (!qe.saveToFile(path)) { + std::fprintf(stderr, "add-quest-objective: failed to write %s\n", + path.c_str()); + return 1; + } + std::printf("Added objective '%s' to quest %d ('%s'), now %zu objective(s)\n", + obj.description.c_str(), idx, q->title.c_str(), + q->objectives.size()); + return 0; +} + +int handleRemoveQuestObjective(int& i, int argc, char** argv) { + // Symmetric counterpart to --add-quest-objective. Removes the + // objective at <objIdx> within quest <questIdx>. Pair with + // --info-quests / --list-quests to find the right indices. + std::string zoneDir = argv[++i]; + std::string qIdxStr = argv[++i]; + std::string oIdxStr = argv[++i]; + std::string path = zoneDir + "/quests.json"; + if (!std::filesystem::exists(path)) { + std::fprintf(stderr, "remove-quest-objective: %s not found\n", path.c_str()); + return 1; + } + int qIdx, oIdx; + try { + qIdx = std::stoi(qIdxStr); + oIdx = std::stoi(oIdxStr); + } catch (...) { + std::fprintf(stderr, "remove-quest-objective: bad index\n"); + return 1; + } + wowee::editor::QuestEditor qe; + if (!qe.loadFromFile(path)) { + std::fprintf(stderr, "remove-quest-objective: failed to load %s\n", + path.c_str()); + return 1; + } + if (qIdx < 0 || qIdx >= static_cast<int>(qe.questCount())) { + std::fprintf(stderr, + "remove-quest-objective: questIdx %d out of range [0, %zu)\n", + qIdx, qe.questCount()); + return 1; + } + wowee::editor::Quest* q = qe.getQuest(qIdx); + if (!q) return 1; + if (oIdx < 0 || oIdx >= static_cast<int>(q->objectives.size())) { + std::fprintf(stderr, + "remove-quest-objective: objIdx %d out of range [0, %zu)\n", + oIdx, q->objectives.size()); + return 1; + } + std::string removedDesc = q->objectives[oIdx].description; + q->objectives.erase(q->objectives.begin() + oIdx); + if (!qe.saveToFile(path)) { + std::fprintf(stderr, "remove-quest-objective: failed to write %s\n", + path.c_str()); + return 1; + } + std::printf("Removed objective '%s' (was index %d) from quest %d ('%s'), now %zu remaining\n", + removedDesc.c_str(), oIdx, qIdx, q->title.c_str(), + q->objectives.size()); + return 0; +} + +} // namespace + +bool handleQuestObjective(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--add-quest") == 0 && i + 2 < argc) { + outRc = handleAddQuest(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--add-quest-objective") == 0 && i + 4 < argc) { + outRc = handleAddQuestObjective(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--remove-quest-objective") == 0 && i + 3 < argc) { + outRc = handleRemoveQuestObjective(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_quest_objective.hpp b/tools/editor/cli_quest_objective.hpp new file mode 100644 index 00000000..488039ab --- /dev/null +++ b/tools/editor/cli_quest_objective.hpp @@ -0,0 +1,22 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +// Dispatch the quest / quest-objective mutation handlers. +// All three operate on a zone's quests.json: +// --add-quest append a new quest (title + opt fields) +// --add-quest-objective append an objective to an existing quest +// --remove-quest-objective remove an objective by index +// +// Reward and clone-quest operations live in their own +// modules (cli_quest_reward / cli_clone) so the per-quest +// objective logic stays focused. +// +// Returns true if matched; outRc holds the exit code. +bool handleQuestObjective(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 54b4fb4c..13b5079f 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -37,6 +37,7 @@ #include "cli_info_density.hpp" #include "cli_info_audio.hpp" #include "cli_world_info.hpp" +#include "cli_quest_objective.hpp" #include "content_pack.hpp" #include "npc_spawner.hpp" #include "object_placer.hpp" @@ -482,6 +483,9 @@ int main(int argc, char* argv[]) { if (wowee::editor::cli::handleWorldInfo(i, argc, argv, outRc)) { return outRc; } + if (wowee::editor::cli::handleQuestObjective(i, argc, argv, outRc)) { + return outRc; + } } if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) { dataPath = argv[++i]; @@ -1410,185 +1414,6 @@ int main(int argc, char* argv[]) { outPath.c_str(), col.triangles.size(), col.walkableCount(), col.steepCount()); return 0; - } else if (std::strcmp(argv[i], "--add-quest") == 0 && i + 2 < argc) { - // Append a single quest to a zone's quests.json. - // Args: <zoneDir> <title> [giverId] [turnInId] [xp] [level] - std::string zoneDir = argv[++i]; - std::string title = argv[++i]; - namespace fs = std::filesystem; - if (!fs::exists(zoneDir)) { - std::fprintf(stderr, "add-quest: zone '%s' does not exist\n", - zoneDir.c_str()); - return 1; - } - wowee::editor::Quest q; - q.title = title; - // Optional positional args after title. Each is read in order; - // an empty string or '-' stops consumption so users can omit - // later fields. - auto tryReadUint = [&](uint32_t& target) { - if (i + 1 >= argc || argv[i + 1][0] == '-') return false; - try { - target = static_cast<uint32_t>(std::stoul(argv[i + 1])); - ++i; - return true; - } catch (...) { return false; } - }; - tryReadUint(q.questGiverNpcId); - tryReadUint(q.turnInNpcId); - tryReadUint(q.reward.xp); - tryReadUint(q.requiredLevel); - wowee::editor::QuestEditor qe; - std::string path = zoneDir + "/quests.json"; - if (fs::exists(path)) qe.loadFromFile(path); - qe.addQuest(q); - if (!qe.saveToFile(path)) { - std::fprintf(stderr, "add-quest: failed to write %s\n", path.c_str()); - return 1; - } - std::printf("Added quest '%s' to %s (now %zu total)\n", - title.c_str(), path.c_str(), qe.questCount()); - return 0; - } else if (std::strcmp(argv[i], "--add-quest-objective") == 0 && i + 4 < argc) { - // Append a single objective to an existing quest. The quest - // must already exist (use --add-quest first); index is 0-based - // and matches --list-quests output. - std::string zoneDir = argv[++i]; - std::string idxStr = argv[++i]; - std::string typeStr = argv[++i]; - std::string targetName = argv[++i]; - std::string path = zoneDir + "/quests.json"; - if (!std::filesystem::exists(path)) { - std::fprintf(stderr, "add-quest-objective: %s not found — run --add-quest first\n", - path.c_str()); - return 1; - } - int idx; - try { idx = std::stoi(idxStr); } - catch (...) { - std::fprintf(stderr, "add-quest-objective: bad questIdx '%s'\n", idxStr.c_str()); - return 1; - } - using OT = wowee::editor::QuestObjectiveType; - OT type; - if (typeStr == "kill") type = OT::KillCreature; - else if (typeStr == "collect") type = OT::CollectItem; - else if (typeStr == "talk") type = OT::TalkToNPC; - else if (typeStr == "explore") type = OT::ExploreArea; - else if (typeStr == "escort") type = OT::EscortNPC; - else if (typeStr == "use") type = OT::UseObject; - else { - std::fprintf(stderr, - "add-quest-objective: type must be kill/collect/talk/explore/escort/use, got '%s'\n", - typeStr.c_str()); - return 1; - } - uint32_t count = 1; - if (i + 1 < argc && argv[i + 1][0] != '-') { - try { - count = static_cast<uint32_t>(std::stoul(argv[++i])); - if (count == 0) count = 1; - } catch (...) {} - } - wowee::editor::QuestEditor qe; - if (!qe.loadFromFile(path)) { - std::fprintf(stderr, "add-quest-objective: failed to load %s\n", path.c_str()); - return 1; - } - if (idx < 0 || idx >= static_cast<int>(qe.questCount())) { - std::fprintf(stderr, - "add-quest-objective: questIdx %d out of range [0, %zu)\n", - idx, qe.questCount()); - return 1; - } - wowee::editor::QuestObjective obj; - obj.type = type; - obj.targetName = targetName; - obj.targetCount = count; - // Auto-generate a description from type+name+count so addons - // and tooltips have something useful by default. The user can - // edit quests.json directly if they want bespoke prose. - const char* verb = "complete"; - switch (type) { - case OT::KillCreature: verb = "Slay"; break; - case OT::CollectItem: verb = "Collect"; break; - case OT::TalkToNPC: verb = "Talk to"; break; - case OT::ExploreArea: verb = "Explore"; break; - case OT::EscortNPC: verb = "Escort"; break; - case OT::UseObject: verb = "Use"; break; - } - obj.description = std::string(verb) + " " + - (count > 1 ? std::to_string(count) + " " : "") + - targetName; - // Quest is stored by value in the editor's vector; mutate via - // the non-const getter, which gives us a pointer we can write - // through. - wowee::editor::Quest* q = qe.getQuest(idx); - if (!q) { - std::fprintf(stderr, "add-quest-objective: getQuest(%d) returned null\n", idx); - return 1; - } - q->objectives.push_back(obj); - if (!qe.saveToFile(path)) { - std::fprintf(stderr, "add-quest-objective: failed to write %s\n", - path.c_str()); - return 1; - } - std::printf("Added objective '%s' to quest %d ('%s'), now %zu objective(s)\n", - obj.description.c_str(), idx, q->title.c_str(), - q->objectives.size()); - return 0; - } else if (std::strcmp(argv[i], "--remove-quest-objective") == 0 && i + 3 < argc) { - // Symmetric counterpart to --add-quest-objective. Removes the - // objective at <objIdx> within quest <questIdx>. Pair with - // --info-quests / --list-quests to find the right indices. - std::string zoneDir = argv[++i]; - std::string qIdxStr = argv[++i]; - std::string oIdxStr = argv[++i]; - std::string path = zoneDir + "/quests.json"; - if (!std::filesystem::exists(path)) { - std::fprintf(stderr, "remove-quest-objective: %s not found\n", path.c_str()); - return 1; - } - int qIdx, oIdx; - try { - qIdx = std::stoi(qIdxStr); - oIdx = std::stoi(oIdxStr); - } catch (...) { - std::fprintf(stderr, "remove-quest-objective: bad index\n"); - return 1; - } - wowee::editor::QuestEditor qe; - if (!qe.loadFromFile(path)) { - std::fprintf(stderr, "remove-quest-objective: failed to load %s\n", - path.c_str()); - return 1; - } - if (qIdx < 0 || qIdx >= static_cast<int>(qe.questCount())) { - std::fprintf(stderr, - "remove-quest-objective: questIdx %d out of range [0, %zu)\n", - qIdx, qe.questCount()); - return 1; - } - wowee::editor::Quest* q = qe.getQuest(qIdx); - if (!q) return 1; - if (oIdx < 0 || oIdx >= static_cast<int>(q->objectives.size())) { - std::fprintf(stderr, - "remove-quest-objective: objIdx %d out of range [0, %zu)\n", - oIdx, q->objectives.size()); - return 1; - } - std::string removedDesc = q->objectives[oIdx].description; - q->objectives.erase(q->objectives.begin() + oIdx); - if (!qe.saveToFile(path)) { - std::fprintf(stderr, "remove-quest-objective: failed to write %s\n", - path.c_str()); - return 1; - } - std::printf("Removed objective '%s' (was index %d) from quest %d ('%s'), now %zu remaining\n", - removedDesc.c_str(), oIdx, qIdx, q->title.c_str(), - q->objectives.size()); - return 0; } else if (std::strcmp(argv[i], "--clone-quest") == 0 && i + 2 < argc) { // Duplicate a quest. Useful for templating: create a base // quest with objectives + rewards once, then clone N times