Kelsidavis-WoWee/tools/editor/cli_content_info.cpp
Kelsi c519ee3e99
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
refactor(editor): extract creature/object/quest info into cli_content_info.cpp
Moves 16 contiguous content-inspection handlers out of main.cpp:

  --info-creatures              --info-creatures-by-faction
  --info-creatures-by-level     --info-objects-by-path
  --info-objects-by-type        --info-quests-by-level
  --info-quests-by-xp           --list-creatures
  --list-objects                --list-quests
  --list-quest-objectives       --list-quest-rewards
  --info-quest-graph-stats      --info-creature
  --info-quest                  --info-object

All read JSON sidecars (creatures.json, objects.json, quests.json)
via wowee::editor::{NpcSpawner, ObjectPlacer, QuestEditor} loaders.

main.cpp drops 17,505 → 16,564 lines (-941). Behavior verified
by running --info-creatures (correctly fails with same parse-
error message on a non-zone path).
2026-05-09 03:33:40 -07:00

1061 lines
40 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "cli_content_info.hpp"
#include "npc_spawner.hpp"
#include "npc_presets.hpp"
#include "object_placer.hpp"
#include "quest_editor.hpp"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <map>
#include <set>
#include <string>
#include <unordered_set>
#include <vector>
namespace wowee {
namespace editor {
namespace cli {
namespace {
int handleInfoCreatures(int& i, int argc, char** argv) {
std::string path = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
wowee::editor::NpcSpawner spawner;
if (!spawner.loadFromFile(path)) {
std::fprintf(stderr, "Failed to load creatures.json: %s\n", path.c_str());
return 1;
}
const auto& spawns = spawner.getSpawns();
int hostile = 0, vendor = 0, questgiver = 0, trainer = 0;
int patrol = 0, wander = 0, stationary = 0;
std::unordered_map<uint32_t, int> displayIdHist;
for (const auto& s : spawns) {
if (s.hostile) hostile++;
if (s.vendor) vendor++;
if (s.questgiver) questgiver++;
if (s.trainer) trainer++;
using B = wowee::editor::CreatureBehavior;
if (s.behavior == B::Patrol) patrol++;
else if (s.behavior == B::Wander) wander++;
else if (s.behavior == B::Stationary) stationary++;
displayIdHist[s.displayId]++;
}
if (jsonOut) {
nlohmann::json j;
j["file"] = path;
j["total"] = spawns.size();
j["hostile"] = hostile;
j["questgiver"] = questgiver;
j["vendor"] = vendor;
j["trainer"] = trainer;
j["behavior"] = {{"stationary", stationary},
{"wander", wander},
{"patrol", patrol}};
j["uniqueDisplayIds"] = displayIdHist.size();
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("creatures.json: %s\n", path.c_str());
std::printf(" total : %zu\n", spawns.size());
std::printf(" hostile : %d\n", hostile);
std::printf(" questgiver : %d\n", questgiver);
std::printf(" vendor : %d\n", vendor);
std::printf(" trainer : %d\n", trainer);
std::printf(" behavior : %d stationary, %d wander, %d patrol\n",
stationary, wander, patrol);
std::printf(" unique displayIds: %zu\n", displayIdHist.size());
return 0;
}
int handleInfoCreaturesByFaction(int& i, int argc, char** argv) {
// Faction histogram for combat balance analysis. AzerothCore
// factions: 7=human, 14=monster, 16=alliance-friendly, 35=neutral,
// etc. A zone with all faction=14 is going to be one giant
// free-for-all; a mixed-faction zone needs combat-tuning.
std::string path = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
wowee::editor::NpcSpawner sp;
if (!sp.loadFromFile(path)) {
std::fprintf(stderr,
"info-creatures-by-faction: failed to load %s\n", path.c_str());
return 1;
}
std::map<uint32_t, int> hist;
for (const auto& s : sp.getSpawns()) hist[s.faction]++;
if (jsonOut) {
nlohmann::json j;
j["file"] = path;
j["totalCreatures"] = sp.spawnCount();
j["uniqueFactions"] = hist.size();
nlohmann::json arr = nlohmann::json::array();
for (const auto& [f, c] : hist) {
arr.push_back({{"faction", f}, {"count", c}});
}
j["factions"] = arr;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("Creatures by faction: %s (%zu total)\n",
path.c_str(), sp.spawnCount());
std::printf(" faction count share\n");
for (const auto& [f, c] : hist) {
double pct = sp.spawnCount() > 0 ? 100.0 * c / sp.spawnCount() : 0.0;
std::printf(" %7u %5d %5.1f%%\n", f, c, pct);
}
std::printf(" (factions: 7=human, 14=monster, 35=neutral, etc.)\n");
return 0;
}
int handleInfoCreaturesByLevel(int& i, int argc, char** argv) {
// Level distribution for difficulty-curve analysis. Min/max/
// avg + per-level histogram. A zone with all level-1 spawns
// is a starter area; one with all 60s is endgame; spikes in
// the middle suggest content-tuning issues.
std::string path = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
wowee::editor::NpcSpawner sp;
if (!sp.loadFromFile(path)) {
std::fprintf(stderr,
"info-creatures-by-level: failed to load %s\n", path.c_str());
return 1;
}
std::map<uint32_t, int> hist;
uint32_t minL = std::numeric_limits<uint32_t>::max();
uint32_t maxL = 0;
uint64_t sumL = 0;
for (const auto& s : sp.getSpawns()) {
hist[s.level]++;
if (s.level < minL) minL = s.level;
if (s.level > maxL) maxL = s.level;
sumL += s.level;
}
double avgL = sp.spawnCount() > 0 ? double(sumL) / sp.spawnCount() : 0.0;
if (sp.spawnCount() == 0) minL = 0;
if (jsonOut) {
nlohmann::json j;
j["file"] = path;
j["totalCreatures"] = sp.spawnCount();
j["minLevel"] = minL;
j["maxLevel"] = maxL;
j["avgLevel"] = avgL;
nlohmann::json arr = nlohmann::json::array();
for (const auto& [l, c] : hist) {
arr.push_back({{"level", l}, {"count", c}});
}
j["levels"] = arr;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("Creatures by level: %s (%zu total)\n",
path.c_str(), sp.spawnCount());
std::printf(" range : %u to %u (avg %.1f)\n", minL, maxL, avgL);
std::printf("\n level count bar\n");
int maxBarCount = 0;
for (const auto& [_, c] : hist) maxBarCount = std::max(maxBarCount, c);
for (const auto& [l, c] : hist) {
int barLen = maxBarCount > 0 ? (40 * c) / maxBarCount : 0;
std::printf(" %5u %5d ", l, c);
for (int b = 0; b < barLen; ++b) std::printf("");
std::printf("\n");
}
return 0;
}
int handleInfoObjectsByPath(int& i, int argc, char** argv) {
// Most-used model paths with counts. Designers can quickly
// spot which trees/lamps/walls dominate a zone — helps with
// both texture-budget audits and 'this looks repetitive,
// diversify the doodads' design feedback.
std::string path = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
wowee::editor::ObjectPlacer placer;
if (!placer.loadFromFile(path)) {
std::fprintf(stderr,
"info-objects-by-path: failed to load %s\n", path.c_str());
return 1;
}
std::map<std::string, int> hist;
for (const auto& o : placer.getObjects()) hist[o.path]++;
// Sort by count descending.
std::vector<std::pair<std::string, int>> sorted(hist.begin(), hist.end());
std::sort(sorted.begin(), sorted.end(),
[](const auto& a, const auto& b) { return a.second > b.second; });
int total = static_cast<int>(placer.getObjects().size());
if (jsonOut) {
nlohmann::json j;
j["file"] = path;
j["totalObjects"] = total;
j["uniquePaths"] = hist.size();
nlohmann::json arr = nlohmann::json::array();
for (const auto& [p, c] : sorted) {
arr.push_back({{"path", p}, {"count", c}});
}
j["paths"] = arr;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("Objects by path: %s (%d total, %zu unique)\n",
path.c_str(), total, hist.size());
std::printf(" count share path\n");
for (const auto& [p, c] : sorted) {
double pct = total > 0 ? 100.0 * c / total : 0.0;
std::printf(" %5d %5.1f%% %s\n", c, pct, p.c_str());
}
return 0;
}
int handleInfoObjectsByType(int& i, int argc, char** argv) {
// M2 vs WMO split + per-type scale stats. Catches scale
// outliers ('this WMO is at 0.001 scale, did you mean 1.0?')
// and gives a sense of zone composition (mostly props vs
// mostly buildings).
std::string path = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
wowee::editor::ObjectPlacer placer;
if (!placer.loadFromFile(path)) {
std::fprintf(stderr,
"info-objects-by-type: failed to load %s\n", path.c_str());
return 1;
}
int m2Count = 0, wmoCount = 0;
float m2Min = 1e30f, m2Max = -1e30f;
float wmoMin = 1e30f, wmoMax = -1e30f;
double m2SumScale = 0, wmoSumScale = 0;
for (const auto& o : placer.getObjects()) {
if (o.type == wowee::editor::PlaceableType::M2) {
m2Count++;
m2Min = std::min(m2Min, o.scale);
m2Max = std::max(m2Max, o.scale);
m2SumScale += o.scale;
} else {
wmoCount++;
wmoMin = std::min(wmoMin, o.scale);
wmoMax = std::max(wmoMax, o.scale);
wmoSumScale += o.scale;
}
}
double m2Avg = m2Count > 0 ? m2SumScale / m2Count : 0.0;
double wmoAvg = wmoCount > 0 ? wmoSumScale / wmoCount : 0.0;
if (m2Count == 0) { m2Min = 0; m2Max = 0; }
if (wmoCount == 0) { wmoMin = 0; wmoMax = 0; }
if (jsonOut) {
nlohmann::json j;
j["file"] = path;
j["totalObjects"] = m2Count + wmoCount;
j["m2"] = {{"count", m2Count},
{"scaleMin", m2Min}, {"scaleMax", m2Max},
{"scaleAvg", m2Avg}};
j["wmo"] = {{"count", wmoCount},
{"scaleMin", wmoMin}, {"scaleMax", wmoMax},
{"scaleAvg", wmoAvg}};
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("Objects by type: %s\n", path.c_str());
std::printf(" M2 : %d (scale %.2f-%.2f, avg %.2f)\n",
m2Count, m2Min, m2Max, m2Avg);
std::printf(" WMO : %d (scale %.2f-%.2f, avg %.2f)\n",
wmoCount, wmoMin, wmoMax, wmoAvg);
return 0;
}
int handleInfoQuestsByLevel(int& i, int argc, char** argv) {
// Required-level distribution. Catches difficulty-curve
// issues where every quest is requiredLevel=1 (player skips
// the chain) or every quest is requiredLevel=60 (no early
// game), and outliers (a level-30 quest dropped into a
// starter zone).
std::string path = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
wowee::editor::QuestEditor qe;
if (!qe.loadFromFile(path)) {
std::fprintf(stderr,
"info-quests-by-level: failed to load %s\n", path.c_str());
return 1;
}
std::map<uint32_t, int> hist;
uint32_t minL = std::numeric_limits<uint32_t>::max();
uint32_t maxL = 0;
uint64_t sumL = 0;
for (const auto& q : qe.getQuests()) {
hist[q.requiredLevel]++;
if (q.requiredLevel < minL) minL = q.requiredLevel;
if (q.requiredLevel > maxL) maxL = q.requiredLevel;
sumL += q.requiredLevel;
}
double avgL = qe.questCount() > 0 ?
double(sumL) / qe.questCount() : 0.0;
if (qe.questCount() == 0) minL = 0;
if (jsonOut) {
nlohmann::json j;
j["file"] = path;
j["totalQuests"] = qe.questCount();
j["minLevel"] = minL;
j["maxLevel"] = maxL;
j["avgLevel"] = avgL;
nlohmann::json arr = nlohmann::json::array();
for (const auto& [l, c] : hist) {
arr.push_back({{"level", l}, {"count", c}});
}
j["levels"] = arr;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("Quests by required level: %s (%zu total)\n",
path.c_str(), qe.questCount());
std::printf(" range : %u to %u (avg %.1f)\n", minL, maxL, avgL);
std::printf("\n level count bar\n");
int maxBarCount = 0;
for (const auto& [_, c] : hist) maxBarCount = std::max(maxBarCount, c);
for (const auto& [l, c] : hist) {
int barLen = maxBarCount > 0 ? (40 * c) / maxBarCount : 0;
std::printf(" %5u %5d ", l, c);
for (int b = 0; b < barLen; ++b) std::printf("");
std::printf("\n");
}
return 0;
}
int handleInfoQuestsByXp(int& i, int argc, char** argv) {
// XP reward distribution. Bucket into 100-XP groups so a
// 10000-XP quest doesn't make the histogram unreadable.
// Catches no-reward quests + cluster analysis (mostly
// 100-XP smalls vs mostly 5000-XP boss kills).
std::string path = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
wowee::editor::QuestEditor qe;
if (!qe.loadFromFile(path)) {
std::fprintf(stderr,
"info-quests-by-xp: failed to load %s\n", path.c_str());
return 1;
}
uint32_t minXp = std::numeric_limits<uint32_t>::max();
uint32_t maxXp = 0;
uint64_t sumXp = 0;
int zeroXp = 0;
// Bucket size grows with max — keeps the histogram readable
// for both starter zones (10-100 XP) and endgame (5000+).
std::map<uint32_t, int> buckets;
for (const auto& q : qe.getQuests()) {
if (q.reward.xp < minXp) minXp = q.reward.xp;
if (q.reward.xp > maxXp) maxXp = q.reward.xp;
sumXp += q.reward.xp;
if (q.reward.xp == 0) zeroXp++;
}
uint32_t bucketSize = 100;
if (maxXp > 1000) bucketSize = 250;
if (maxXp > 5000) bucketSize = 500;
if (maxXp > 20000) bucketSize = 1000;
for (const auto& q : qe.getQuests()) {
buckets[(q.reward.xp / bucketSize) * bucketSize]++;
}
double avgXp = qe.questCount() > 0 ?
double(sumXp) / qe.questCount() : 0.0;
if (qe.questCount() == 0) minXp = 0;
if (jsonOut) {
nlohmann::json j;
j["file"] = path;
j["totalQuests"] = qe.questCount();
j["minXp"] = minXp;
j["maxXp"] = maxXp;
j["avgXp"] = avgXp;
j["zeroXpQuests"] = zeroXp;
j["bucketSize"] = bucketSize;
nlohmann::json arr = nlohmann::json::array();
for (const auto& [b, c] : buckets) {
arr.push_back({{"bucket", b}, {"count", c}});
}
j["buckets"] = arr;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("Quests by XP reward: %s (%zu total)\n",
path.c_str(), qe.questCount());
std::printf(" range : %u to %u (avg %.0f, %d with 0 XP)\n",
minXp, maxXp, avgXp, zeroXp);
std::printf("\n bucket (≥XP) count bar\n");
int maxBarCount = 0;
for (const auto& [_, c] : buckets) maxBarCount = std::max(maxBarCount, c);
for (const auto& [b, c] : buckets) {
int barLen = maxBarCount > 0 ? (40 * c) / maxBarCount : 0;
std::printf(" %12u %5d ", b, c);
for (int x = 0; x < barLen; ++x) std::printf("");
std::printf("\n");
}
std::printf(" (bucket size: %u XP)\n", bucketSize);
return 0;
}
int handleListCreatures(int& i, int argc, char** argv) {
// Verbose enumeration of every spawn — needed because
// --remove-creature takes a 0-based index but --info-creatures
// only shows aggregate counts.
std::string path = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
wowee::editor::NpcSpawner spawner;
if (!spawner.loadFromFile(path)) {
std::fprintf(stderr, "Failed to load creatures.json: %s\n", path.c_str());
return 1;
}
const auto& spawns = spawner.getSpawns();
if (jsonOut) {
nlohmann::json j;
j["file"] = path;
j["total"] = spawns.size();
nlohmann::json arr = nlohmann::json::array();
for (size_t k = 0; k < spawns.size(); ++k) {
const auto& s = spawns[k];
arr.push_back({
{"index", k},
{"name", s.name},
{"displayId", s.displayId},
{"level", s.level},
{"position", {s.position.x, s.position.y, s.position.z}},
{"hostile", s.hostile},
});
}
j["spawns"] = arr;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("creatures.json: %s (%zu total)\n", path.c_str(), spawns.size());
std::printf(" idx name lvl display pos (x, y, z)\n");
for (size_t k = 0; k < spawns.size(); ++k) {
const auto& s = spawns[k];
std::printf(" %3zu %-30s %3u %7u (%.1f, %.1f, %.1f)%s\n",
k, s.name.substr(0, 30).c_str(), s.level, s.displayId,
s.position.x, s.position.y, s.position.z,
s.hostile ? " [hostile]" : "");
}
return 0;
}
int handleListObjects(int& i, int argc, char** argv) {
std::string path = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
wowee::editor::ObjectPlacer placer;
if (!placer.loadFromFile(path)) {
std::fprintf(stderr, "Failed to load objects.json: %s\n", path.c_str());
return 1;
}
const auto& objs = placer.getObjects();
auto typeStr = [](wowee::editor::PlaceableType t) {
return t == wowee::editor::PlaceableType::M2 ? "m2" : "wmo";
};
if (jsonOut) {
nlohmann::json j;
j["file"] = path;
j["total"] = objs.size();
nlohmann::json arr = nlohmann::json::array();
for (size_t k = 0; k < objs.size(); ++k) {
const auto& o = objs[k];
arr.push_back({
{"index", k},
{"type", typeStr(o.type)},
{"path", o.path},
{"position", {o.position.x, o.position.y, o.position.z}},
{"scale", o.scale},
});
}
j["objects"] = arr;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("objects.json: %s (%zu total)\n", path.c_str(), objs.size());
std::printf(" idx type scale path pos (x, y, z)\n");
for (size_t k = 0; k < objs.size(); ++k) {
const auto& o = objs[k];
std::printf(" %3zu %-4s %5.2f %-38s (%.1f, %.1f, %.1f)\n",
k, typeStr(o.type), o.scale,
o.path.substr(0, 38).c_str(),
o.position.x, o.position.y, o.position.z);
}
return 0;
}
int handleListQuests(int& i, int argc, char** argv) {
std::string path = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
wowee::editor::QuestEditor qe;
if (!qe.loadFromFile(path)) {
std::fprintf(stderr, "Failed to load quests.json: %s\n", path.c_str());
return 1;
}
const auto& quests = qe.getQuests();
if (jsonOut) {
nlohmann::json j;
j["file"] = path;
j["total"] = quests.size();
nlohmann::json arr = nlohmann::json::array();
for (size_t k = 0; k < quests.size(); ++k) {
const auto& q = quests[k];
arr.push_back({
{"index", k},
{"title", q.title},
{"giver", q.questGiverNpcId},
{"turnIn", q.turnInNpcId},
{"requiredLevel", q.requiredLevel},
{"xp", q.reward.xp},
{"nextQuestId", q.nextQuestId},
});
}
j["quests"] = arr;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("quests.json: %s (%zu total)\n", path.c_str(), quests.size());
std::printf(" idx lvl giver turnIn xp title\n");
for (size_t k = 0; k < quests.size(); ++k) {
const auto& q = quests[k];
std::printf(" %3zu %3u %7u %7u %5u %s%s\n",
k, q.requiredLevel, q.questGiverNpcId, q.turnInNpcId,
q.reward.xp, q.title.c_str(),
q.nextQuestId ? " [chained]" : "");
}
return 0;
}
int handleListQuestObjectives(int& i, int argc, char** argv) {
// Per-quest objective listing — pairs with --remove-quest-objective
// (which takes objIdx). Tabulates type, target, count, description.
std::string path = argv[++i];
std::string idxStr = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
int qIdx;
try { qIdx = std::stoi(idxStr); }
catch (...) {
std::fprintf(stderr, "list-quest-objectives: bad questIdx '%s'\n", idxStr.c_str());
return 1;
}
wowee::editor::QuestEditor qe;
if (!qe.loadFromFile(path)) {
std::fprintf(stderr, "list-quest-objectives: failed to load %s\n", path.c_str());
return 1;
}
if (qIdx < 0 || qIdx >= static_cast<int>(qe.questCount())) {
std::fprintf(stderr,
"list-quest-objectives: questIdx %d out of range [0, %zu)\n",
qIdx, qe.questCount());
return 1;
}
const auto& q = qe.getQuests()[qIdx];
using OT = wowee::editor::QuestObjectiveType;
auto typeName = [](OT t) {
switch (t) {
case OT::KillCreature: return "kill";
case OT::CollectItem: return "collect";
case OT::TalkToNPC: return "talk";
case OT::ExploreArea: return "explore";
case OT::EscortNPC: return "escort";
case OT::UseObject: return "use";
}
return "?";
};
if (jsonOut) {
nlohmann::json j;
j["file"] = path;
j["questIdx"] = qIdx;
j["title"] = q.title;
j["count"] = q.objectives.size();
nlohmann::json arr = nlohmann::json::array();
for (size_t o = 0; o < q.objectives.size(); ++o) {
const auto& ob = q.objectives[o];
arr.push_back({
{"index", o},
{"type", typeName(ob.type)},
{"target", ob.targetName},
{"count", ob.targetCount},
{"description", ob.description},
});
}
j["objectives"] = arr;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("Quest %d ('%s'): %zu objective(s)\n",
qIdx, q.title.c_str(), q.objectives.size());
std::printf(" idx type count target description\n");
for (size_t o = 0; o < q.objectives.size(); ++o) {
const auto& ob = q.objectives[o];
std::printf(" %3zu %-7s %5u %-18s %s\n",
o, typeName(ob.type), ob.targetCount,
ob.targetName.substr(0, 18).c_str(),
ob.description.c_str());
}
return 0;
}
int handleListQuestRewards(int& i, int argc, char** argv) {
// Per-quest reward listing. Shows XP/coin breakdown plus the
// full itemRewards list (which --info-quests only counts).
std::string path = argv[++i];
std::string idxStr = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
int qIdx;
try { qIdx = std::stoi(idxStr); }
catch (...) {
std::fprintf(stderr, "list-quest-rewards: bad questIdx '%s'\n", idxStr.c_str());
return 1;
}
wowee::editor::QuestEditor qe;
if (!qe.loadFromFile(path)) {
std::fprintf(stderr, "list-quest-rewards: failed to load %s\n", path.c_str());
return 1;
}
if (qIdx < 0 || qIdx >= static_cast<int>(qe.questCount())) {
std::fprintf(stderr,
"list-quest-rewards: questIdx %d out of range [0, %zu)\n",
qIdx, qe.questCount());
return 1;
}
const auto& q = qe.getQuests()[qIdx];
const auto& r = q.reward;
if (jsonOut) {
nlohmann::json j;
j["file"] = path;
j["questIdx"] = qIdx;
j["title"] = q.title;
j["xp"] = r.xp;
j["gold"] = r.gold;
j["silver"] = r.silver;
j["copper"] = r.copper;
j["items"] = r.itemRewards;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("Quest %d ('%s') rewards:\n", qIdx, q.title.c_str());
std::printf(" xp : %u\n", r.xp);
std::printf(" coin : %ug %us %uc\n", r.gold, r.silver, r.copper);
std::printf(" items : %zu\n", r.itemRewards.size());
for (size_t k = 0; k < r.itemRewards.size(); ++k) {
std::printf(" [%zu] %s\n", k, r.itemRewards[k].c_str());
}
return 0;
}
int handleInfoQuestGraphStats(int& i, int argc, char** argv) {
// Topology analysis of the quest dependency graph. Where
// --export-quest-graph visualizes it, this quantifies it:
// roots = quests no one chains TO (entry points)
// leaves = quests with no nextQuestId (terminal)
// orphans = roots that are also leaves (one-shot quests)
// cycles = circular chain detected
// maxDepth = longest path from any root
// avgDepth = mean path length across all roots
std::string path = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
wowee::editor::QuestEditor qe;
if (!qe.loadFromFile(path)) {
std::fprintf(stderr,
"info-quest-graph-stats: failed to load %s\n", path.c_str());
return 1;
}
const auto& quests = qe.getQuests();
// Build id -> nextId and reverse adjacency.
std::unordered_map<uint32_t, uint32_t> nextOf;
std::unordered_set<uint32_t> hasInbound;
std::unordered_set<uint32_t> validIds;
for (const auto& q : quests) {
validIds.insert(q.id);
nextOf[q.id] = q.nextQuestId;
}
for (const auto& q : quests) {
if (q.nextQuestId != 0 && validIds.count(q.nextQuestId)) {
hasInbound.insert(q.nextQuestId);
}
}
int roots = 0, leaves = 0, orphans = 0;
int cycles = 0;
int maxDepth = 0;
int sumDepths = 0;
for (const auto& q : quests) {
bool isRoot = (hasInbound.count(q.id) == 0);
bool isLeaf = (q.nextQuestId == 0 ||
validIds.count(q.nextQuestId) == 0);
if (isRoot) roots++;
if (isLeaf) leaves++;
if (isRoot && isLeaf) orphans++;
if (isRoot) {
// Walk the chain forward, counting depth + cycle-guarding.
std::unordered_set<uint32_t> visited;
int depth = 1;
uint32_t current = q.id;
while (current != 0 && validIds.count(current)) {
if (!visited.insert(current).second) {
cycles++;
break;
}
auto it = nextOf.find(current);
if (it == nextOf.end() || it->second == 0) break;
current = it->second;
depth++;
}
if (depth > maxDepth) maxDepth = depth;
sumDepths += depth;
}
}
double avgDepth = (roots > 0) ? double(sumDepths) / roots : 0.0;
if (jsonOut) {
nlohmann::json j;
j["file"] = path;
j["totalQuests"] = quests.size();
j["roots"] = roots;
j["leaves"] = leaves;
j["orphans"] = orphans;
j["cycles"] = cycles;
j["maxDepth"] = maxDepth;
j["avgDepth"] = avgDepth;
std::printf("%s\n", j.dump(2).c_str());
return cycles == 0 ? 0 : 1;
}
std::printf("Quest graph: %s\n", path.c_str());
std::printf(" total quests : %zu\n", quests.size());
std::printf(" roots : %d (no inbound chain — entry points)\n", roots);
std::printf(" leaves : %d (no outbound chain — terminal)\n", leaves);
std::printf(" orphans : %d (root AND leaf — one-shot)\n", orphans);
std::printf(" cycles : %d %s\n", cycles,
cycles == 0 ? "" : "(BROKEN — chains loop back)");
std::printf(" max depth : %d\n", maxDepth);
std::printf(" avg depth : %.2f (chain length per root)\n", avgDepth);
return cycles == 0 ? 0 : 1;
}
int handleInfoCreature(int& i, int argc, char** argv) {
// Single-creature deep dive — every CreatureSpawn field for
// one entry. Companion to --list-creatures (which is a
// table view); useful for digging into 'why is this NPC
// not behaving like I expect?'.
std::string path = argv[++i];
std::string idxStr = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
int idx;
try { idx = std::stoi(idxStr); }
catch (...) {
std::fprintf(stderr, "info-creature: bad idx '%s'\n", idxStr.c_str());
return 1;
}
wowee::editor::NpcSpawner sp;
if (!sp.loadFromFile(path)) {
std::fprintf(stderr, "info-creature: failed to load %s\n", path.c_str());
return 1;
}
if (idx < 0 || idx >= static_cast<int>(sp.spawnCount())) {
std::fprintf(stderr,
"info-creature: idx %d out of range [0, %zu)\n",
idx, sp.spawnCount());
return 1;
}
const auto& s = sp.getSpawns()[idx];
using B = wowee::editor::CreatureBehavior;
const char* behavior =
s.behavior == B::Patrol ? "patrol" :
s.behavior == B::Wander ? "wander" : "stationary";
if (jsonOut) {
nlohmann::json j;
j["index"] = idx;
j["id"] = s.id;
j["name"] = s.name;
j["modelPath"] = s.modelPath;
j["displayId"] = s.displayId;
j["position"] = {s.position.x, s.position.y, s.position.z};
j["orientation"] = s.orientation;
j["level"] = s.level;
j["health"] = s.health;
j["mana"] = s.mana;
j["minDamage"] = s.minDamage;
j["maxDamage"] = s.maxDamage;
j["armor"] = s.armor;
j["faction"] = s.faction;
j["scale"] = s.scale;
j["behavior"] = behavior;
j["wanderRadius"] = s.wanderRadius;
j["aggroRadius"] = s.aggroRadius;
j["leashRadius"] = s.leashRadius;
j["respawnTimeMs"] = s.respawnTimeMs;
j["patrolPoints"] = s.patrolPath.size();
j["hostile"] = s.hostile;
j["questgiver"] = s.questgiver;
j["vendor"] = s.vendor;
j["trainer"] = s.trainer;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("Creature [%d] '%s'\n", idx, s.name.c_str());
std::printf(" id : %u\n", s.id);
std::printf(" displayId : %u\n", s.displayId);
std::printf(" modelPath : %s\n",
s.modelPath.empty() ? "(uses displayId)" : s.modelPath.c_str());
std::printf(" position : (%.2f, %.2f, %.2f)\n",
s.position.x, s.position.y, s.position.z);
std::printf(" orientation : %.2f deg\n", s.orientation);
std::printf(" scale : %.2f\n", s.scale);
std::printf(" level : %u\n", s.level);
std::printf(" health/mana : %u / %u\n", s.health, s.mana);
std::printf(" damage : %u-%u\n", s.minDamage, s.maxDamage);
std::printf(" armor : %u\n", s.armor);
std::printf(" faction : %u\n", s.faction);
std::printf(" behavior : %s\n", behavior);
std::printf(" wander rad : %.1f\n", s.wanderRadius);
std::printf(" aggro rad : %.1f\n", s.aggroRadius);
std::printf(" leash rad : %.1f\n", s.leashRadius);
std::printf(" respawn ms : %u\n", s.respawnTimeMs);
std::printf(" patrol points : %zu\n", s.patrolPath.size());
std::printf(" flags : %s%s%s%s\n",
s.hostile ? "hostile " : "",
s.questgiver ? "questgiver " : "",
s.vendor ? "vendor " : "",
s.trainer ? "trainer " : "");
return 0;
}
int handleInfoQuest(int& i, int argc, char** argv) {
// Single-quest deep dive — combines what --list-quest-objectives
// and --list-quest-rewards show into one view, plus the chain
// pointer + descriptions that neither covers.
std::string path = argv[++i];
std::string idxStr = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
int idx;
try { idx = std::stoi(idxStr); }
catch (...) {
std::fprintf(stderr, "info-quest: bad idx '%s'\n", idxStr.c_str());
return 1;
}
wowee::editor::QuestEditor qe;
if (!qe.loadFromFile(path)) {
std::fprintf(stderr, "info-quest: failed to load %s\n", path.c_str());
return 1;
}
if (idx < 0 || idx >= static_cast<int>(qe.questCount())) {
std::fprintf(stderr,
"info-quest: idx %d out of range [0, %zu)\n",
idx, qe.questCount());
return 1;
}
const auto& q = qe.getQuests()[idx];
using OT = wowee::editor::QuestObjectiveType;
auto typeName = [](OT t) {
switch (t) {
case OT::KillCreature: return "kill";
case OT::CollectItem: return "collect";
case OT::TalkToNPC: return "talk";
case OT::ExploreArea: return "explore";
case OT::EscortNPC: return "escort";
case OT::UseObject: return "use";
}
return "?";
};
if (jsonOut) {
nlohmann::json j;
j["index"] = idx;
j["id"] = q.id;
j["title"] = q.title;
j["description"] = q.description;
j["completionText"] = q.completionText;
j["requiredLevel"] = q.requiredLevel;
j["questGiverNpcId"] = q.questGiverNpcId;
j["turnInNpcId"] = q.turnInNpcId;
j["nextQuestId"] = q.nextQuestId;
j["reward"] = {
{"xp", q.reward.xp},
{"gold", q.reward.gold},
{"silver", q.reward.silver},
{"copper", q.reward.copper},
{"items", q.reward.itemRewards}
};
nlohmann::json objs = nlohmann::json::array();
for (const auto& obj : q.objectives) {
objs.push_back({
{"type", typeName(obj.type)},
{"target", obj.targetName},
{"count", obj.targetCount},
{"description", obj.description}
});
}
j["objectives"] = objs;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("Quest [%d] '%s'\n", idx, q.title.c_str());
std::printf(" id : %u\n", q.id);
std::printf(" required level : %u\n", q.requiredLevel);
std::printf(" giver NPC id : %u\n", q.questGiverNpcId);
std::printf(" turn-in NPC id : %u\n", q.turnInNpcId);
std::printf(" next quest id : %u%s\n", q.nextQuestId,
q.nextQuestId == 0 ? " (terminal)" : "");
if (!q.description.empty()) {
std::printf(" description : %s\n", q.description.c_str());
}
if (!q.completionText.empty()) {
std::printf(" completion text : %s\n", q.completionText.c_str());
}
std::printf(" reward : %u XP, %ug %us %uc, %zu item(s)\n",
q.reward.xp, q.reward.gold, q.reward.silver,
q.reward.copper, q.reward.itemRewards.size());
for (size_t k = 0; k < q.reward.itemRewards.size(); ++k) {
std::printf(" item[%zu] : %s\n", k,
q.reward.itemRewards[k].c_str());
}
std::printf(" objectives : %zu\n", q.objectives.size());
for (size_t k = 0; k < q.objectives.size(); ++k) {
const auto& o = q.objectives[k];
std::printf(" [%zu] %-7s ×%u %s%s%s\n",
k, typeName(o.type), o.targetCount,
o.targetName.c_str(),
o.description.empty() ? "" : "",
o.description.c_str());
}
return 0;
}
int handleInfoObject(int& i, int argc, char** argv) {
// Single-object deep dive — every PlacedObject field for one
// entry. Completes the single-entity inspector trio
// (creature/quest/object).
std::string path = argv[++i];
std::string idxStr = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
int idx;
try { idx = std::stoi(idxStr); }
catch (...) {
std::fprintf(stderr, "info-object: bad idx '%s'\n", idxStr.c_str());
return 1;
}
wowee::editor::ObjectPlacer placer;
if (!placer.loadFromFile(path)) {
std::fprintf(stderr, "info-object: failed to load %s\n", path.c_str());
return 1;
}
const auto& objs = placer.getObjects();
if (idx < 0 || idx >= static_cast<int>(objs.size())) {
std::fprintf(stderr,
"info-object: idx %d out of range [0, %zu)\n",
idx, objs.size());
return 1;
}
const auto& o = objs[idx];
const char* typeStr =
o.type == wowee::editor::PlaceableType::M2 ? "m2" : "wmo";
if (jsonOut) {
nlohmann::json j;
j["index"] = idx;
j["type"] = typeStr;
j["path"] = o.path;
j["nameId"] = o.nameId;
j["uniqueId"] = o.uniqueId;
j["position"] = {o.position.x, o.position.y, o.position.z};
j["rotation"] = {o.rotation.x, o.rotation.y, o.rotation.z};
j["scale"] = o.scale;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("Object [%d]\n", idx);
std::printf(" type : %s\n", typeStr);
std::printf(" path : %s\n", o.path.c_str());
std::printf(" nameId : %u\n", o.nameId);
std::printf(" uniqueId : %u%s\n", o.uniqueId,
o.uniqueId == 0 ? " (unassigned)" : "");
std::printf(" position : (%.3f, %.3f, %.3f)\n",
o.position.x, o.position.y, o.position.z);
std::printf(" rotation : (%.2f, %.2f, %.2f) deg\n",
o.rotation.x, o.rotation.y, o.rotation.z);
std::printf(" scale : %.3f\n", o.scale);
return 0;
}
} // namespace
bool handleContentInfo(int& i, int argc, char** argv, int& outRc) {
if (std::strcmp(argv[i], "--info-creatures") == 0 && i + 1 < argc) {
outRc = handleInfoCreatures(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-creatures-by-faction") == 0 && i + 1 < argc) {
outRc = handleInfoCreaturesByFaction(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-creatures-by-level") == 0 && i + 1 < argc) {
outRc = handleInfoCreaturesByLevel(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-objects-by-path") == 0 && i + 1 < argc) {
outRc = handleInfoObjectsByPath(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-objects-by-type") == 0 && i + 1 < argc) {
outRc = handleInfoObjectsByType(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-quests-by-level") == 0 && i + 1 < argc) {
outRc = handleInfoQuestsByLevel(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-quests-by-xp") == 0 && i + 1 < argc) {
outRc = handleInfoQuestsByXp(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--list-creatures") == 0 && i + 1 < argc) {
outRc = handleListCreatures(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--list-objects") == 0 && i + 1 < argc) {
outRc = handleListObjects(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--list-quests") == 0 && i + 1 < argc) {
outRc = handleListQuests(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--list-quest-objectives") == 0 && i + 2 < argc) {
outRc = handleListQuestObjectives(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--list-quest-rewards") == 0 && i + 2 < argc) {
outRc = handleListQuestRewards(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-quest-graph-stats") == 0 && i + 1 < argc) {
outRc = handleInfoQuestGraphStats(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-creature") == 0 && i + 2 < argc) {
outRc = handleInfoCreature(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-quest") == 0 && i + 2 < argc) {
outRc = handleInfoQuest(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-object") == 0 && i + 2 < argc) {
outRc = handleInfoObject(i, argc, argv); return true;
}
return false;
}
} // namespace cli
} // namespace editor
} // namespace wowee