From 2048496aaf5f19600605000c13b9e4d45281a4e4 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 12:18:20 -0700 Subject: [PATCH] feat(editor): add --add-quest-reward-item and --set-quest-reward MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the quest-authoring CLI: --add-quest creates a quest, then --add-quest-objective adds completion conditions, and now these add the rewards. wowee_editor --add-quest-reward-item $Z 0 'Item:Sword' 'Item:Shield' 'Item:Potion' wowee_editor --set-quest-reward $Z 0 --xp 999 --gold 5 --silver 30 --add-quest-reward-item greedy-consumes any trailing positional args that don't start with '-', so a whole loot table can be added in one invocation rather than N round-trips through load+save. --set-quest-reward updates only the fields explicitly passed (--xp / --gold / --silver / --copper) — avoids the round-trip-and-clobber footgun of a 'replace whole reward' command. Field flags are order-independent. At least one flag is required (else error). Both validate questIdx against quest count and bail with exit 1 on out-of-range / missing file / bad numeric value. Verified: scaffolded zone, added quest with default xp=100, batch- added 3 item rewards in one shot, then set xp=999/gold=5/silver=30 in one shot. --info-quests reports total XP=999, with-items=1. --- tools/editor/main.cpp | 134 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 1 deletion(-) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 18c9848b..bfe172e4 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -413,6 +413,10 @@ static void printUsage(const char* argv0) { std::printf(" Append one quest to /quests.json and exit\n"); std::printf(" --add-quest-objective [count]\n"); std::printf(" Append one objective to a quest by index\n"); + std::printf(" --add-quest-reward-item [more...]\n"); + std::printf(" Append item reward(s) to a quest's reward.itemRewards list\n"); + std::printf(" --set-quest-reward [--xp N] [--gold N] [--silver N] [--copper N]\n"); + std::printf(" Update XP/coin reward fields on a quest by index\n"); std::printf(" --remove-creature \n"); std::printf(" Remove creature at given 0-based index from /creatures.json\n"); std::printf(" --remove-object \n"); @@ -507,7 +511,7 @@ int main(int argc, char* argv[]) { "--validate", "--validate-wom", "--validate-wob", "--validate-woc", "--validate-whm", "--validate-all", "--zone-summary", "--scaffold-zone", "--add-creature", "--add-object", "--add-quest", - "--add-quest-objective", + "--add-quest-objective", "--add-quest-reward-item", "--set-quest-reward", "--remove-creature", "--remove-object", "--remove-quest", "--copy-zone", "--build-woc", "--regen-collision", "--fix-zone", @@ -554,6 +558,16 @@ int main(int argc, char* argv[]) { "--add-quest-objective requires \n"); return 1; } + if (std::strcmp(argv[i], "--add-quest-reward-item") == 0 && i + 3 >= argc) { + std::fprintf(stderr, + "--add-quest-reward-item requires \n"); + return 1; + } + if (std::strcmp(argv[i], "--set-quest-reward") == 0 && i + 2 >= argc) { + std::fprintf(stderr, + "--set-quest-reward requires [--xp N] [--gold N] [--silver N] [--copper N]\n"); + return 1; + } if (std::strcmp(argv[i], "--copy-zone") == 0 && i + 2 >= argc) { std::fprintf(stderr, "--copy-zone requires \n"); @@ -2794,6 +2808,124 @@ int main(int argc, char* argv[]) { obj.description.c_str(), idx, q->title.c_str(), q->objectives.size()); return 0; + } else if (std::strcmp(argv[i], "--add-quest-reward-item") == 0 && i + 3 < argc) { + // Append one or more item rewards to a quest. Multiple paths + // can be passed in a single invocation: + // --add-quest-reward-item zone 0 'Item:Sword' 'Item:Shield' + std::string zoneDir = argv[++i]; + std::string idxStr = argv[++i]; + std::string path = zoneDir + "/quests.json"; + if (!std::filesystem::exists(path)) { + std::fprintf(stderr, "add-quest-reward-item: %s not found\n", path.c_str()); + return 1; + } + int idx; + try { idx = std::stoi(idxStr); } + catch (...) { + std::fprintf(stderr, "add-quest-reward-item: bad questIdx '%s'\n", idxStr.c_str()); + return 1; + } + wowee::editor::QuestEditor qe; + if (!qe.loadFromFile(path)) { + std::fprintf(stderr, "add-quest-reward-item: failed to load %s\n", path.c_str()); + return 1; + } + if (idx < 0 || idx >= static_cast(qe.questCount())) { + std::fprintf(stderr, + "add-quest-reward-item: questIdx %d out of range [0, %zu)\n", + idx, qe.questCount()); + return 1; + } + wowee::editor::Quest* q = qe.getQuest(idx); + if (!q) return 1; + int added = 0; + // Greedy-consume any remaining args that don't start with '-' + // so the caller can batch-add a whole loot table in one shot. + while (i + 1 < argc && argv[i + 1][0] != '-') { + q->reward.itemRewards.push_back(argv[++i]); + added++; + } + if (added == 0) { + std::fprintf(stderr, "add-quest-reward-item: need at least one itemPath\n"); + return 1; + } + if (!qe.saveToFile(path)) { + std::fprintf(stderr, "add-quest-reward-item: failed to write %s\n", path.c_str()); + return 1; + } + std::printf("Added %d item reward(s) to quest %d ('%s'), now %zu total\n", + added, idx, q->title.c_str(), q->reward.itemRewards.size()); + return 0; + } else if (std::strcmp(argv[i], "--set-quest-reward") == 0 && i + 2 < argc) { + // Update XP / coin reward fields on an existing quest. Each + // field is optional — only the ones explicitly passed are + // changed. This avoids the round-trip-and-clobber footgun of + // a "replace whole reward" command. + std::string zoneDir = argv[++i]; + std::string idxStr = argv[++i]; + std::string path = zoneDir + "/quests.json"; + if (!std::filesystem::exists(path)) { + std::fprintf(stderr, "set-quest-reward: %s not found\n", path.c_str()); + return 1; + } + int idx; + try { idx = std::stoi(idxStr); } + catch (...) { + std::fprintf(stderr, "set-quest-reward: bad questIdx '%s'\n", idxStr.c_str()); + return 1; + } + wowee::editor::QuestEditor qe; + if (!qe.loadFromFile(path)) { + std::fprintf(stderr, "set-quest-reward: failed to load %s\n", path.c_str()); + return 1; + } + if (idx < 0 || idx >= static_cast(qe.questCount())) { + std::fprintf(stderr, + "set-quest-reward: questIdx %d out of range [0, %zu)\n", + idx, qe.questCount()); + return 1; + } + wowee::editor::Quest* q = qe.getQuest(idx); + if (!q) return 1; + int changed = 0; + auto consumeUint = [&](const char* flag, uint32_t& target) { + if (i + 2 < argc && std::strcmp(argv[i + 1], flag) == 0) { + try { + target = static_cast(std::stoul(argv[i + 2])); + i += 2; + changed++; + return true; + } catch (...) { + std::fprintf(stderr, "set-quest-reward: bad %s value '%s'\n", + flag, argv[i + 2]); + } + } + return false; + }; + // Loop until no more recognised flags consume their value — + // order-independent, so callers can pass --gold then --xp. + bool any = true; + while (any) { + any = false; + if (consumeUint("--xp", q->reward.xp)) any = true; + if (consumeUint("--gold", q->reward.gold)) any = true; + if (consumeUint("--silver", q->reward.silver)) any = true; + if (consumeUint("--copper", q->reward.copper)) any = true; + } + if (changed == 0) { + std::fprintf(stderr, + "set-quest-reward: no fields changed — pass --xp / --gold / --silver / --copper\n"); + return 1; + } + if (!qe.saveToFile(path)) { + std::fprintf(stderr, "set-quest-reward: failed to write %s\n", path.c_str()); + return 1; + } + std::printf("Updated %d field(s) on quest %d ('%s'): xp=%u gold=%u silver=%u copper=%u\n", + changed, idx, q->title.c_str(), + q->reward.xp, q->reward.gold, + q->reward.silver, q->reward.copper); + return 0; } else if (std::strcmp(argv[i], "--remove-creature") == 0 && i + 2 < argc) { // Remove a creature spawn by 0-based index. Pair with // --info-creatures (or your editor) to find the right index