diff --git a/CMakeLists.txt b/CMakeLists.txt index 69061d2f..50a8f497 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1342,6 +1342,7 @@ add_executable(wowee_editor tools/editor/cli_remove.cpp tools/editor/cli_add.cpp tools/editor/cli_random.cpp + tools/editor/cli_items_export.cpp tools/editor/editor_app.cpp tools/editor/editor_camera.cpp tools/editor/editor_viewport.cpp diff --git a/tools/editor/cli_items_export.cpp b/tools/editor/cli_items_export.cpp new file mode 100644 index 00000000..e83f852f --- /dev/null +++ b/tools/editor/cli_items_export.cpp @@ -0,0 +1,312 @@ +#include "cli_items_export.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +int handleExportZoneItemsMd(int& i, int argc, char** argv) { + // Render items.json as a Markdown table grouped by + // quality. Useful for design docs, PR descriptions, and + // GitHub Pages — one rendered page communicates the loot + // landscape better than scrolling through JSON. + std::string zoneDir = argv[++i]; + std::string outPath; + if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; + namespace fs = std::filesystem; + std::string path = zoneDir + "/items.json"; + if (!fs::exists(path)) { + std::fprintf(stderr, + "export-zone-items-md: %s has no items.json\n", + zoneDir.c_str()); + return 1; + } + nlohmann::json doc; + try { + std::ifstream in(path); + in >> doc; + } catch (...) { + std::fprintf(stderr, + "export-zone-items-md: %s is not valid JSON\n", + path.c_str()); + return 1; + } + if (!doc.contains("items") || !doc["items"].is_array()) { + std::fprintf(stderr, + "export-zone-items-md: %s has no 'items' array\n", + path.c_str()); + return 1; + } + if (outPath.empty()) outPath = zoneDir + "/ITEMS.md"; + const auto& items = doc["items"]; + static const char* qualityNames[] = { + "Poor", "Common", "Uncommon", "Rare", "Epic", + "Legendary", "Artifact" + }; + // Bucket by quality so the report reads top-down from + // best loot to filler. Reverse iteration over the buckets. + std::map> byQuality; + for (size_t k = 0; k < items.size(); ++k) { + uint32_t q = items[k].value("quality", 1u); + if (q > 6) q = 0; + byQuality[q].push_back(k); + } + std::ofstream out(outPath); + if (!out) { + std::fprintf(stderr, + "export-zone-items-md: cannot write %s\n", outPath.c_str()); + return 1; + } + std::string zoneName = fs::path(zoneDir).filename().string(); + out << "# Items: " << zoneName << "\n\n"; + out << "Source: `" << path << "` \n"; + out << "Total items: **" << items.size() << "**\n\n"; + // Quality histogram up top. + out << "## Quality breakdown\n\n"; + out << "| Quality | Count |\n|---|---:|\n"; + for (int q = 6; q >= 0; --q) { + auto it = byQuality.find(q); + if (it == byQuality.end()) continue; + out << "| " << qualityNames[q] << " | " + << it->second.size() << " |\n"; + } + out << "\n"; + // Per-quality sections, best first. + for (int q = 6; q >= 0; --q) { + auto qit = byQuality.find(q); + if (qit == byQuality.end()) continue; + out << "## " << qualityNames[q] << "\n\n"; + out << "| ID | Name | iLvl | Display | Stack |\n"; + out << "|---:|---|---:|---:|---:|\n"; + for (size_t k : qit->second) { + const auto& it = items[k]; + std::string name = it.value("name", std::string("(unnamed)")); + out << "| " << it.value("id", 0u) << " | " + << name << " | " + << it.value("itemLevel", 1u) << " | " + << it.value("displayId", 0u) << " | " + << it.value("stackable", 1u) << " |\n"; + } + out << "\n"; + } + out.close(); + std::printf("Wrote %s\n", outPath.c_str()); + std::printf(" total items : %zu\n", items.size()); + std::printf(" qualities : %zu (used)\n", byQuality.size()); + return 0; +} + +int handleExportProjectItemsMd(int& i, int argc, char** argv) { + // Project-wide items markdown. Walks every zone in + // and emits one document with: project-wide + // header + total + quality histogram, then per-zone + // sections each containing a table (ID/name/quality/ + // ilvl/displayId/stack). Easier to scan than running + // --export-zone-items-md N times. + std::string projectDir = argv[++i]; + std::string outPath; + if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; + namespace fs = std::filesystem; + if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) { + std::fprintf(stderr, + "export-project-items-md: %s is not a directory\n", + projectDir.c_str()); + return 1; + } + if (outPath.empty()) outPath = projectDir + "/ITEMS.md"; + std::vector zones; + for (const auto& entry : fs::directory_iterator(projectDir)) { + if (!entry.is_directory()) continue; + if (!fs::exists(entry.path() / "zone.json")) continue; + if (!fs::exists(entry.path() / "items.json")) continue; + zones.push_back(entry.path().string()); + } + std::sort(zones.begin(), zones.end()); + static const char* qualityNames[] = { + "Poor", "Common", "Uncommon", "Rare", "Epic", + "Legendary", "Artifact" + }; + int totalItems = 0; + std::map globalQ; + // Per-zone collected items so we don't have to re-read + // each items.json twice. + struct ZItems { + std::string name; + nlohmann::json items; + }; + std::vector zoneItems; + for (const auto& zoneDir : zones) { + std::string ipath = zoneDir + "/items.json"; + nlohmann::json doc; + try { + std::ifstream in(ipath); + in >> doc; + } catch (...) { continue; } + if (!doc.contains("items") || !doc["items"].is_array()) continue; + ZItems z; + z.name = fs::path(zoneDir).filename().string(); + z.items = doc["items"]; + for (const auto& it : z.items) { + int q = static_cast(it.value("quality", 1u)); + if (q < 0 || q > 6) q = 0; + globalQ[q]++; + totalItems++; + } + zoneItems.push_back(std::move(z)); + } + std::ofstream out(outPath); + if (!out) { + std::fprintf(stderr, + "export-project-items-md: cannot write %s\n", + outPath.c_str()); + return 1; + } + out << "# Project Items: " + << fs::path(projectDir).filename().string() << "\n\n"; + out << "Source: `" << projectDir << "` \n"; + out << "Zones with items: **" << zoneItems.size() << "** \n"; + out << "Total items: **" << totalItems << "**\n\n"; + out << "## Project quality breakdown\n\n"; + out << "| Quality | Count |\n|---|---:|\n"; + for (int q = 6; q >= 0; --q) { + auto it = globalQ.find(q); + if (it == globalQ.end()) continue; + out << "| " << qualityNames[q] << " | " + << it->second << " |\n"; + } + out << "\n"; + for (const auto& z : zoneItems) { + out << "## Zone: " << z.name << "\n\n"; + out << "Items: **" << z.items.size() << "**\n\n"; + out << "| ID | Name | Quality | iLvl | Display | Stack |\n"; + out << "|---:|---|---|---:|---:|---:|\n"; + for (const auto& it : z.items) { + int q = static_cast(it.value("quality", 1u)); + if (q < 0 || q > 6) q = 0; + std::string name = it.value("name", std::string("(unnamed)")); + out << "| " << it.value("id", 0u) << " | " + << name << " | " + << qualityNames[q] << " | " + << it.value("itemLevel", 1u) << " | " + << it.value("displayId", 0u) << " | " + << it.value("stackable", 1u) << " |\n"; + } + out << "\n"; + } + out.close(); + std::printf("Wrote %s\n", outPath.c_str()); + std::printf(" zones with items : %zu\n", zoneItems.size()); + std::printf(" total items : %d\n", totalItems); + return 0; +} + +int handleExportProjectItemsCsv(int& i, int argc, char** argv) { + // Single CSV with every item across every zone. The + // zone name is the first column so a pivot table can + // group by it; everything else mirrors --export-zone-csv + // items columns. Saves running the per-zone CSV exporter + // N times and concatenating manually. + std::string projectDir = argv[++i]; + std::string outPath; + if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; + namespace fs = std::filesystem; + if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) { + std::fprintf(stderr, + "export-project-items-csv: %s is not a directory\n", + projectDir.c_str()); + return 1; + } + if (outPath.empty()) outPath = projectDir + "/items.csv"; + std::vector zones; + for (const auto& entry : fs::directory_iterator(projectDir)) { + if (!entry.is_directory()) continue; + if (!fs::exists(entry.path() / "zone.json")) continue; + if (!fs::exists(entry.path() / "items.json")) continue; + zones.push_back(entry.path().string()); + } + std::sort(zones.begin(), zones.end()); + // CSV-escape the same way --export-zone-csv does. + auto csvEsc = [](const std::string& s) { + bool needs = s.find(',') != std::string::npos || + s.find('"') != std::string::npos || + s.find('\n') != std::string::npos; + if (!needs) return s; + std::string out = "\""; + for (char c : s) { + if (c == '"') out += "\"\""; + else out += c; + } + out += "\""; + return out; + }; + std::ofstream out(outPath); + if (!out) { + std::fprintf(stderr, + "export-project-items-csv: cannot write %s\n", + outPath.c_str()); + return 1; + } + out << "zone,index,id,name,quality,itemLevel,displayId,stackable\n"; + int totalRows = 0; + for (const auto& zoneDir : zones) { + std::string zoneName = fs::path(zoneDir).filename().string(); + std::string ipath = zoneDir + "/items.json"; + nlohmann::json doc; + try { + std::ifstream in(ipath); + in >> doc; + } catch (...) { continue; } + if (!doc.contains("items") || !doc["items"].is_array()) continue; + const auto& items = doc["items"]; + for (size_t k = 0; k < items.size(); ++k) { + const auto& it = items[k]; + out << csvEsc(zoneName) << "," << k << "," + << it.value("id", 0u) << "," + << csvEsc(it.value("name", std::string())) << "," + << it.value("quality", 1u) << "," + << it.value("itemLevel", 1u) << "," + << it.value("displayId", 0u) << "," + << it.value("stackable", 1u) << "\n"; + totalRows++; + } + } + out.close(); + std::printf("Wrote %s\n", outPath.c_str()); + std::printf(" zones with items : %zu\n", zones.size()); + std::printf(" rows : %d\n", totalRows); + return 0; +} + + +} // namespace + +bool handleItemsExport(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--export-zone-items-md") == 0 && i + 1 < argc) { + outRc = handleExportZoneItemsMd(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--export-project-items-md") == 0 && i + 1 < argc) { + outRc = handleExportProjectItemsMd(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--export-project-items-csv") == 0 && i + 1 < argc) { + outRc = handleExportProjectItemsCsv(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_items_export.hpp b/tools/editor/cli_items_export.hpp new file mode 100644 index 00000000..4c435034 --- /dev/null +++ b/tools/editor/cli_items_export.hpp @@ -0,0 +1,19 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +// Dispatch the item-export handlers — render items.json as +// human-readable Markdown / CSV reports for design docs and +// pivot-table workflows. +// --export-zone-items-md per-zone Markdown by quality +// --export-project-items-md project-wide Markdown rollup +// --export-project-items-csv project-wide CSV (zone in col 1) +// +// Returns true if matched; outRc holds the exit code. +bool handleItemsExport(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 b2de4b84..a3781944 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -43,6 +43,7 @@ #include "cli_remove.hpp" #include "cli_add.hpp" #include "cli_random.hpp" +#include "cli_items_export.hpp" #include "content_pack.hpp" #include "npc_spawner.hpp" #include "object_placer.hpp" @@ -508,6 +509,9 @@ int main(int argc, char* argv[]) { if (wowee::editor::cli::handleRandom(i, argc, argv, outRc)) { return outRc; } + if (wowee::editor::cli::handleItemsExport(i, argc, argv, outRc)) { + return outRc; + } } if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) { dataPath = argv[++i]; @@ -1585,272 +1589,6 @@ int main(int argc, char* argv[]) { std::printf(" %s\n", c.c_str()); } return 0; - } else if (std::strcmp(argv[i], "--export-zone-items-md") == 0 && i + 1 < argc) { - // Render items.json as a Markdown table grouped by - // quality. Useful for design docs, PR descriptions, and - // GitHub Pages — one rendered page communicates the loot - // landscape better than scrolling through JSON. - std::string zoneDir = argv[++i]; - std::string outPath; - if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; - namespace fs = std::filesystem; - std::string path = zoneDir + "/items.json"; - if (!fs::exists(path)) { - std::fprintf(stderr, - "export-zone-items-md: %s has no items.json\n", - zoneDir.c_str()); - return 1; - } - nlohmann::json doc; - try { - std::ifstream in(path); - in >> doc; - } catch (...) { - std::fprintf(stderr, - "export-zone-items-md: %s is not valid JSON\n", - path.c_str()); - return 1; - } - if (!doc.contains("items") || !doc["items"].is_array()) { - std::fprintf(stderr, - "export-zone-items-md: %s has no 'items' array\n", - path.c_str()); - return 1; - } - if (outPath.empty()) outPath = zoneDir + "/ITEMS.md"; - const auto& items = doc["items"]; - static const char* qualityNames[] = { - "Poor", "Common", "Uncommon", "Rare", "Epic", - "Legendary", "Artifact" - }; - // Bucket by quality so the report reads top-down from - // best loot to filler. Reverse iteration over the buckets. - std::map> byQuality; - for (size_t k = 0; k < items.size(); ++k) { - uint32_t q = items[k].value("quality", 1u); - if (q > 6) q = 0; - byQuality[q].push_back(k); - } - std::ofstream out(outPath); - if (!out) { - std::fprintf(stderr, - "export-zone-items-md: cannot write %s\n", outPath.c_str()); - return 1; - } - std::string zoneName = fs::path(zoneDir).filename().string(); - out << "# Items: " << zoneName << "\n\n"; - out << "Source: `" << path << "` \n"; - out << "Total items: **" << items.size() << "**\n\n"; - // Quality histogram up top. - out << "## Quality breakdown\n\n"; - out << "| Quality | Count |\n|---|---:|\n"; - for (int q = 6; q >= 0; --q) { - auto it = byQuality.find(q); - if (it == byQuality.end()) continue; - out << "| " << qualityNames[q] << " | " - << it->second.size() << " |\n"; - } - out << "\n"; - // Per-quality sections, best first. - for (int q = 6; q >= 0; --q) { - auto qit = byQuality.find(q); - if (qit == byQuality.end()) continue; - out << "## " << qualityNames[q] << "\n\n"; - out << "| ID | Name | iLvl | Display | Stack |\n"; - out << "|---:|---|---:|---:|---:|\n"; - for (size_t k : qit->second) { - const auto& it = items[k]; - std::string name = it.value("name", std::string("(unnamed)")); - out << "| " << it.value("id", 0u) << " | " - << name << " | " - << it.value("itemLevel", 1u) << " | " - << it.value("displayId", 0u) << " | " - << it.value("stackable", 1u) << " |\n"; - } - out << "\n"; - } - out.close(); - std::printf("Wrote %s\n", outPath.c_str()); - std::printf(" total items : %zu\n", items.size()); - std::printf(" qualities : %zu (used)\n", byQuality.size()); - return 0; - } else if (std::strcmp(argv[i], "--export-project-items-md") == 0 && i + 1 < argc) { - // Project-wide items markdown. Walks every zone in - // and emits one document with: project-wide - // header + total + quality histogram, then per-zone - // sections each containing a table (ID/name/quality/ - // ilvl/displayId/stack). Easier to scan than running - // --export-zone-items-md N times. - std::string projectDir = argv[++i]; - std::string outPath; - if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; - namespace fs = std::filesystem; - if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) { - std::fprintf(stderr, - "export-project-items-md: %s is not a directory\n", - projectDir.c_str()); - return 1; - } - if (outPath.empty()) outPath = projectDir + "/ITEMS.md"; - std::vector zones; - for (const auto& entry : fs::directory_iterator(projectDir)) { - if (!entry.is_directory()) continue; - if (!fs::exists(entry.path() / "zone.json")) continue; - if (!fs::exists(entry.path() / "items.json")) continue; - zones.push_back(entry.path().string()); - } - std::sort(zones.begin(), zones.end()); - static const char* qualityNames[] = { - "Poor", "Common", "Uncommon", "Rare", "Epic", - "Legendary", "Artifact" - }; - int totalItems = 0; - std::map globalQ; - // Per-zone collected items so we don't have to re-read - // each items.json twice. - struct ZItems { - std::string name; - nlohmann::json items; - }; - std::vector zoneItems; - for (const auto& zoneDir : zones) { - std::string ipath = zoneDir + "/items.json"; - nlohmann::json doc; - try { - std::ifstream in(ipath); - in >> doc; - } catch (...) { continue; } - if (!doc.contains("items") || !doc["items"].is_array()) continue; - ZItems z; - z.name = fs::path(zoneDir).filename().string(); - z.items = doc["items"]; - for (const auto& it : z.items) { - int q = static_cast(it.value("quality", 1u)); - if (q < 0 || q > 6) q = 0; - globalQ[q]++; - totalItems++; - } - zoneItems.push_back(std::move(z)); - } - std::ofstream out(outPath); - if (!out) { - std::fprintf(stderr, - "export-project-items-md: cannot write %s\n", - outPath.c_str()); - return 1; - } - out << "# Project Items: " - << fs::path(projectDir).filename().string() << "\n\n"; - out << "Source: `" << projectDir << "` \n"; - out << "Zones with items: **" << zoneItems.size() << "** \n"; - out << "Total items: **" << totalItems << "**\n\n"; - out << "## Project quality breakdown\n\n"; - out << "| Quality | Count |\n|---|---:|\n"; - for (int q = 6; q >= 0; --q) { - auto it = globalQ.find(q); - if (it == globalQ.end()) continue; - out << "| " << qualityNames[q] << " | " - << it->second << " |\n"; - } - out << "\n"; - for (const auto& z : zoneItems) { - out << "## Zone: " << z.name << "\n\n"; - out << "Items: **" << z.items.size() << "**\n\n"; - out << "| ID | Name | Quality | iLvl | Display | Stack |\n"; - out << "|---:|---|---|---:|---:|---:|\n"; - for (const auto& it : z.items) { - int q = static_cast(it.value("quality", 1u)); - if (q < 0 || q > 6) q = 0; - std::string name = it.value("name", std::string("(unnamed)")); - out << "| " << it.value("id", 0u) << " | " - << name << " | " - << qualityNames[q] << " | " - << it.value("itemLevel", 1u) << " | " - << it.value("displayId", 0u) << " | " - << it.value("stackable", 1u) << " |\n"; - } - out << "\n"; - } - out.close(); - std::printf("Wrote %s\n", outPath.c_str()); - std::printf(" zones with items : %zu\n", zoneItems.size()); - std::printf(" total items : %d\n", totalItems); - return 0; - } else if (std::strcmp(argv[i], "--export-project-items-csv") == 0 && i + 1 < argc) { - // Single CSV with every item across every zone. The - // zone name is the first column so a pivot table can - // group by it; everything else mirrors --export-zone-csv - // items columns. Saves running the per-zone CSV exporter - // N times and concatenating manually. - std::string projectDir = argv[++i]; - std::string outPath; - if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; - namespace fs = std::filesystem; - if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) { - std::fprintf(stderr, - "export-project-items-csv: %s is not a directory\n", - projectDir.c_str()); - return 1; - } - if (outPath.empty()) outPath = projectDir + "/items.csv"; - std::vector zones; - for (const auto& entry : fs::directory_iterator(projectDir)) { - if (!entry.is_directory()) continue; - if (!fs::exists(entry.path() / "zone.json")) continue; - if (!fs::exists(entry.path() / "items.json")) continue; - zones.push_back(entry.path().string()); - } - std::sort(zones.begin(), zones.end()); - // CSV-escape the same way --export-zone-csv does. - auto csvEsc = [](const std::string& s) { - bool needs = s.find(',') != std::string::npos || - s.find('"') != std::string::npos || - s.find('\n') != std::string::npos; - if (!needs) return s; - std::string out = "\""; - for (char c : s) { - if (c == '"') out += "\"\""; - else out += c; - } - out += "\""; - return out; - }; - std::ofstream out(outPath); - if (!out) { - std::fprintf(stderr, - "export-project-items-csv: cannot write %s\n", - outPath.c_str()); - return 1; - } - out << "zone,index,id,name,quality,itemLevel,displayId,stackable\n"; - int totalRows = 0; - for (const auto& zoneDir : zones) { - std::string zoneName = fs::path(zoneDir).filename().string(); - std::string ipath = zoneDir + "/items.json"; - nlohmann::json doc; - try { - std::ifstream in(ipath); - in >> doc; - } catch (...) { continue; } - if (!doc.contains("items") || !doc["items"].is_array()) continue; - const auto& items = doc["items"]; - for (size_t k = 0; k < items.size(); ++k) { - const auto& it = items[k]; - out << csvEsc(zoneName) << "," << k << "," - << it.value("id", 0u) << "," - << csvEsc(it.value("name", std::string())) << "," - << it.value("quality", 1u) << "," - << it.value("itemLevel", 1u) << "," - << it.value("displayId", 0u) << "," - << it.value("stackable", 1u) << "\n"; - totalRows++; - } - } - out.close(); - std::printf("Wrote %s\n", outPath.c_str()); - std::printf(" zones with items : %zu\n", zones.size()); - std::printf(" rows : %d\n", totalRows); - return 0; } else if (std::strcmp(argv[i], "--copy-zone-items") == 0 && i + 2 < argc) { // Copy items from one zone to another. Default mode // replaces the destination items.json wholesale; --merge