diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp
index 1b9927b2..be00bd72 100644
--- a/tools/editor/main.cpp
+++ b/tools/editor/main.cpp
@@ -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
[--json]\n");
std::printf(" Distribution of creature levels (min/max/avg + per-level counts)\n");
+ std::printf(" --info-objects-by-path
[--json]\n");
+ std::printf(" Histogram of object placements grouped by model path (most-used first)\n");
+ std::printf(" --info-objects-by-type
[--json]\n");
+ std::printf(" M2 vs WMO breakdown plus scale distribution (min/max/avg)\n");
std::printf(" --info-objects
[--json]\n");
std::printf(" Print objects.json summary (counts, types, scale range) and exit\n");
std::printf(" --info-quests
[--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 hist;
+ for (const auto& o : placer.getObjects()) hist[o.path]++;
+ // Sort by count descending.
+ std::vector> 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(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