feat(editor): add --info-objects-by-path + --info-objects-by-type analytics

Object-side counterparts to --info-creatures-by-faction/-level. Two
analytics commands for placement audits:

  wowee_editor --info-objects-by-path $Z/objects.json

  Objects by path: ... (47 total, 12 unique)
    count   share   path
       18    38.3%  World/Generic/Tree.m2
        9    19.1%  World/Generic/Lamp.m2
        4     8.5%  World/Building/Inn.wmo
        ...

  wowee_editor --info-objects-by-type $Z/objects.json

  Objects by type: ...
    M2  : 38  (scale 0.80-2.50, avg 1.12)
    WMO : 9   (scale 1.00-1.00, avg 1.00)

--info-objects-by-path: most-used model paths first. Catches
'this looks repetitive, diversify the doodads' design feedback
and surfaces texture-budget hot spots (one model used 50× pulls
its textures into the working set 50×).

--info-objects-by-type: 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 composition sense (mostly props vs mostly
buildings).

JSON modes emit per-path / per-type records for dashboards.
Verified on a 4-object seed (3 M2 + 1 WMO with mixed scales):
correctly reports Tree.m2 used 2× (50%), M2 scale range 0.80-1.50
avg 1.10.
This commit is contained in:
Kelsi 2026-05-06 17:46:51 -07:00
parent 1797ffd280
commit f4e8623d58

View file

@ -716,6 +716,10 @@ static void printUsage(const char* argv0) {
std::printf(" Histogram of creature counts grouped by faction id\n");
std::printf(" --info-creatures-by-level <p> [--json]\n");
std::printf(" Distribution of creature levels (min/max/avg + per-level counts)\n");
std::printf(" --info-objects-by-path <p> [--json]\n");
std::printf(" Histogram of object placements grouped by model path (most-used first)\n");
std::printf(" --info-objects-by-type <p> [--json]\n");
std::printf(" M2 vs WMO breakdown plus scale distribution (min/max/avg)\n");
std::printf(" --info-objects <p> [--json]\n");
std::printf(" Print objects.json summary (counts, types, scale range) and exit\n");
std::printf(" --info-quests <p> [--json]\n");
@ -801,6 +805,7 @@ int main(int argc, char* argv[]) {
"--info-creature", "--info-quest", "--info-object",
"--info-quest-graph-stats",
"--info-creatures-by-faction", "--info-creatures-by-level",
"--info-objects-by-path", "--info-objects-by-type",
"--unpack-wcp", "--pack-wcp",
"--validate", "--validate-wom", "--validate-wob", "--validate-woc",
"--validate-whm", "--validate-all", "--validate-project",
@ -2634,6 +2639,104 @@ int main(int argc, char* argv[]) {
std::printf("\n");
}
return 0;
} else if (std::strcmp(argv[i], "--info-objects-by-path") == 0 && i + 1 < argc) {
// 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;
} else if (std::strcmp(argv[i], "--info-objects-by-type") == 0 && i + 1 < argc) {
// 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;
} else if (std::strcmp(argv[i], "--list-creatures") == 0 && i + 1 < argc) {
// Verbose enumeration of every spawn — needed because
// --remove-creature takes a 0-based index but --info-creatures