diff --git a/CMakeLists.txt b/CMakeLists.txt index 9bfdf05c..e298d0bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1313,6 +1313,7 @@ add_executable(wowee_editor tools/editor/cli_format_validate.cpp tools/editor/cli_convert.cpp tools/editor/cli_format_info.cpp + tools/editor/cli_pack.cpp tools/editor/editor_app.cpp tools/editor/editor_camera.cpp tools/editor/editor_viewport.cpp diff --git a/tools/editor/cli_pack.cpp b/tools/editor/cli_pack.cpp new file mode 100644 index 00000000..e4a2d992 --- /dev/null +++ b/tools/editor/cli_pack.cpp @@ -0,0 +1,327 @@ +#include "cli_pack.hpp" + +#include "content_pack.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +int handleListWcp(int& i, int argc, char** argv) { + // Like --info-wcp but prints every file path. Useful for spotting + // missing or unexpected entries before unpacking. + std::string path = argv[++i]; + wowee::editor::ContentPackInfo info; + if (!wowee::editor::ContentPacker::readInfo(path, info)) { + std::fprintf(stderr, "Failed to read WCP: %s\n", path.c_str()); + return 1; + } + std::printf("WCP: %s — %zu files\n", path.c_str(), info.files.size()); + // Sort by path so identical packs produce identical output (the + // packer order depends on the directory_iterator implementation). + auto files = info.files; + std::sort(files.begin(), files.end(), + [](const auto& a, const auto& b) { return a.path < b.path; }); + for (const auto& f : files) { + std::printf(" %-10s %10llu %s\n", + f.category.c_str(), + static_cast(f.size), + f.path.c_str()); + } + return 0; +} + +int handleInfoWcp(int& i, int argc, char** argv) { + std::string path = argv[++i]; + // Optional --json after the path for machine-readable output. + bool jsonOut = (i + 1 < argc && + std::strcmp(argv[i + 1], "--json") == 0); + if (jsonOut) i++; + wowee::editor::ContentPackInfo info; + if (!wowee::editor::ContentPacker::readInfo(path, info)) { + std::fprintf(stderr, "Failed to read WCP: %s\n", path.c_str()); + return 1; + } + // Per-category file totals + std::unordered_map byCat; + uint64_t totalSize = 0; + for (const auto& f : info.files) { + byCat[f.category]++; + totalSize += f.size; + } + if (jsonOut) { + nlohmann::json j; + j["wcp"] = path; + j["name"] = info.name; + j["author"] = info.author; + j["description"] = info.description; + j["version"] = info.version; + j["format"] = info.format; + j["mapId"] = info.mapId; + j["fileCount"] = info.files.size(); + j["totalBytes"] = totalSize; + nlohmann::json categories = nlohmann::json::object(); + for (const auto& [cat, count] : byCat) categories[cat] = count; + j["categories"] = categories; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WCP: %s\n", path.c_str()); + std::printf(" name : %s\n", info.name.c_str()); + std::printf(" author : %s\n", info.author.c_str()); + std::printf(" description : %s\n", info.description.c_str()); + std::printf(" version : %s\n", info.version.c_str()); + std::printf(" format : %s\n", info.format.c_str()); + std::printf(" mapId : %u\n", info.mapId); + std::printf(" files : %zu\n", info.files.size()); + for (const auto& [cat, count] : byCat) { + std::printf(" %-10s : %zu\n", cat.c_str(), count); + } + std::printf(" total bytes : %.2f MB\n", totalSize / (1024.0 * 1024.0)); + return 0; +} + +int handleInfoPackBudget(int& i, int argc, char** argv) { + // Per-extension byte breakdown of a WCP archive. --info-wcp + // gives counts per category; this gives bytes per extension + // so users can spot what's bloating an archive before + // shipping. ('Why is my pack 80MB? Oh, the .glb baked + // outputs got included.') + std::string path = argv[++i]; + bool jsonOut = (i + 1 < argc && + std::strcmp(argv[i + 1], "--json") == 0); + if (jsonOut) i++; + wowee::editor::ContentPackInfo info; + if (!wowee::editor::ContentPacker::readInfo(path, info)) { + std::fprintf(stderr, + "info-pack-budget: failed to read %s\n", path.c_str()); + return 1; + } + // Sum bytes per extension (lower-cased). + std::map> byExt; + uint64_t totalBytes = 0; + for (const auto& f : info.files) { + std::string ext; + auto dot = f.path.find_last_of('.'); + if (dot != std::string::npos) ext = f.path.substr(dot); + else ext = "(no-ext)"; + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) { return std::tolower(c); }); + byExt[ext].first++; + byExt[ext].second += f.size; + totalBytes += f.size; + } + // Sort by bytes descending. + std::vector>> sorted( + byExt.begin(), byExt.end()); + std::sort(sorted.begin(), sorted.end(), + [](const auto& a, const auto& b) { + return a.second.second > b.second.second; + }); + if (jsonOut) { + nlohmann::json j; + j["wcp"] = path; + j["totalFiles"] = info.files.size(); + j["totalBytes"] = totalBytes; + nlohmann::json arr = nlohmann::json::array(); + for (const auto& [ext, cb] : sorted) { + arr.push_back({{"ext", ext}, + {"count", cb.first}, + {"bytes", cb.second}}); + } + j["byExtension"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WCP budget: %s\n", path.c_str()); + std::printf(" total: %zu file(s), %.2f MB\n", + info.files.size(), totalBytes / (1024.0 * 1024.0)); + std::printf("\n ext count bytes KB share\n"); + for (const auto& [ext, cb] : sorted) { + double pct = totalBytes > 0 + ? 100.0 * cb.second / totalBytes : 0.0; + std::printf(" %-12s %6d %11llu %6.1f %5.1f%%\n", + ext.c_str(), cb.first, + static_cast(cb.second), + cb.second / 1024.0, pct); + } + return 0; +} + +int handleInfoPackTree(int& i, int argc, char** argv) { + // Tree view of a WCP's directory layout with per-file byte + // sizes. --list-wcp shows the flat sorted file list; + // --info-pack-tree gives the hierarchical view that's + // easier to read for archives with subdirectories (textures + // under data/, models under buildings/, etc.). + std::string path = argv[++i]; + wowee::editor::ContentPackInfo info; + if (!wowee::editor::ContentPacker::readInfo(path, info)) { + std::fprintf(stderr, + "info-pack-tree: failed to read %s\n", path.c_str()); + return 1; + } + // Build a directory tree from flat file paths. Sub-tree + // children are sorted alphabetically with files before dirs + // (by-convention filesystem-tree look). + struct Node { + std::map> children; + bool isFile = false; + uint64_t bytes = 0; + }; + auto root = std::make_shared(); + auto split = [](const std::string& p) { + std::vector parts; + std::string cur; + for (char c : p) { + if (c == '/' || c == '\\') { + if (!cur.empty()) { parts.push_back(cur); cur.clear(); } + } else cur += c; + } + if (!cur.empty()) parts.push_back(cur); + return parts; + }; + uint64_t totalBytes = 0; + for (const auto& f : info.files) { + auto parts = split(f.path); + if (parts.empty()) continue; + Node* cur = root.get(); + for (size_t k = 0; k < parts.size(); ++k) { + auto& child = cur->children[parts[k]]; + if (!child) child = std::make_shared(); + if (k == parts.size() - 1) { + child->isFile = true; + child->bytes = f.size; + } + cur = child.get(); + } + totalBytes += f.size; + } + // Recursive renderer with box-drawing connectors. Aggregates + // child bytes up so directories show their subtotal. + std::function render = + [&](const Node* n, const std::string& prefix) -> uint64_t { + size_t i = 0; + size_t total = n->children.size(); + uint64_t subtotal = 0; + for (const auto& [name, child] : n->children) { + bool last = (++i == total); + const char* branch = last ? "└─ " : "├─ "; + const char* cont = last ? " " : "│ "; + if (child->isFile) { + std::printf("%s%s%s (%llu bytes)\n", + prefix.c_str(), branch, name.c_str(), + static_cast(child->bytes)); + subtotal += child->bytes; + } else { + // Directory — recurse, then print header with subtotal. + std::printf("%s%s%s/\n", + prefix.c_str(), branch, name.c_str()); + subtotal += render(child.get(), prefix + cont); + } + } + return subtotal; + }; + std::printf("%s (%zu files, %.2f KB)\n", + path.c_str(), info.files.size(), totalBytes / 1024.0); + render(root.get(), ""); + return 0; +} + +int handlePackWcp(int& i, int argc, char** argv) { + // Pack a zone directory into a .wcp archive. + // Usage: --pack-wcp [destPath] + // If looks like a path (contains '/' or starts + // with '.'), use it directly; otherwise resolve under + // custom_zones/ then output/ (matching the discovery search + // order). + std::string nameOrDir = argv[++i]; + std::string destPath; + if (i + 1 < argc && argv[i + 1][0] != '-') { + destPath = argv[++i]; + } + namespace fs = std::filesystem; + std::string outputDir, mapName; + if (nameOrDir.find('/') != std::string::npos || nameOrDir[0] == '.') { + fs::path p = fs::absolute(nameOrDir); + outputDir = p.parent_path().string(); + mapName = p.filename().string(); + } else { + mapName = nameOrDir; + if (fs::exists("custom_zones/" + mapName)) outputDir = "custom_zones"; + else if (fs::exists("output/" + mapName)) outputDir = "output"; + else { + std::fprintf(stderr, + "--pack-wcp: zone '%s' not found in custom_zones/ or output/\n", + mapName.c_str()); + return 1; + } + } + if (destPath.empty()) destPath = mapName + ".wcp"; + wowee::editor::ContentPackInfo info; + info.name = mapName; + info.format = "wcp-1.0"; + if (!wowee::editor::ContentPacker::packZone(outputDir, mapName, destPath, info)) { + std::fprintf(stderr, "WCP pack failed for %s/%s\n", + outputDir.c_str(), mapName.c_str()); + return 1; + } + std::printf("WCP packed: %s\n", destPath.c_str()); + return 0; +} + +int handleUnpackWcp(int& i, int argc, char** argv) { + std::string wcpPath = argv[++i]; + std::string destDir = "custom_zones"; + if (i + 1 < argc && argv[i + 1][0] != '-') { + destDir = argv[++i]; + } + if (!wowee::editor::ContentPacker::unpackZone(wcpPath, destDir)) { + std::fprintf(stderr, "WCP unpack failed: %s\n", wcpPath.c_str()); + return 1; + } + std::printf("WCP unpacked to: %s\n", destDir.c_str()); + return 0; +} + + +} // namespace + +bool handlePack(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--list-wcp") == 0 && i + 1 < argc) { + outRc = handleListWcp(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wcp") == 0 && i + 1 < argc) { + outRc = handleInfoWcp(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-pack-budget") == 0 && i + 1 < argc) { + outRc = handleInfoPackBudget(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-pack-tree") == 0 && i + 1 < argc) { + outRc = handleInfoPackTree(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--pack-wcp") == 0 && i + 1 < argc) { + outRc = handlePackWcp(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--unpack-wcp") == 0 && i + 1 < argc) { + outRc = handleUnpackWcp(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_pack.hpp b/tools/editor/cli_pack.hpp new file mode 100644 index 00000000..84d5886a --- /dev/null +++ b/tools/editor/cli_pack.hpp @@ -0,0 +1,20 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +// Dispatch the WoWee Content Pack (.wcp) handlers: +// --list-wcp --info-wcp +// --info-pack-budget --info-pack-tree +// --pack-wcp --unpack-wcp +// +// All defer to wowee::editor::ContentPacker for actual pack +// I/O; the handlers just parse args and format output. +// +// Returns true if matched; outRc holds the exit code. +bool handlePack(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 055392ff..837f544d 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -14,6 +14,7 @@ #include "cli_format_validate.hpp" #include "cli_convert.hpp" #include "cli_format_info.hpp" +#include "cli_pack.hpp" #include "content_pack.hpp" #include "npc_spawner.hpp" #include "object_placer.hpp" @@ -465,6 +466,9 @@ int main(int argc, char* argv[]) { if (wowee::editor::cli::handleFormatInfo(i, argc, argv, outRc)) { return outRc; } + if (wowee::editor::cli::handlePack(i, argc, argv, outRc)) { + return outRc; + } } if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) { dataPath = argv[++i]; @@ -3693,219 +3697,6 @@ int main(int argc, char* argv[]) { return 0; } return 1; - } else if (std::strcmp(argv[i], "--list-wcp") == 0 && i + 1 < argc) { - // Like --info-wcp but prints every file path. Useful for spotting - // missing or unexpected entries before unpacking. - std::string path = argv[++i]; - wowee::editor::ContentPackInfo info; - if (!wowee::editor::ContentPacker::readInfo(path, info)) { - std::fprintf(stderr, "Failed to read WCP: %s\n", path.c_str()); - return 1; - } - std::printf("WCP: %s — %zu files\n", path.c_str(), info.files.size()); - // Sort by path so identical packs produce identical output (the - // packer order depends on the directory_iterator implementation). - auto files = info.files; - std::sort(files.begin(), files.end(), - [](const auto& a, const auto& b) { return a.path < b.path; }); - for (const auto& f : files) { - std::printf(" %-10s %10llu %s\n", - f.category.c_str(), - static_cast(f.size), - f.path.c_str()); - } - return 0; - } else if (std::strcmp(argv[i], "--info-wcp") == 0 && i + 1 < argc) { - std::string path = argv[++i]; - // Optional --json after the path for machine-readable output. - bool jsonOut = (i + 1 < argc && - std::strcmp(argv[i + 1], "--json") == 0); - if (jsonOut) i++; - wowee::editor::ContentPackInfo info; - if (!wowee::editor::ContentPacker::readInfo(path, info)) { - std::fprintf(stderr, "Failed to read WCP: %s\n", path.c_str()); - return 1; - } - // Per-category file totals - std::unordered_map byCat; - uint64_t totalSize = 0; - for (const auto& f : info.files) { - byCat[f.category]++; - totalSize += f.size; - } - if (jsonOut) { - nlohmann::json j; - j["wcp"] = path; - j["name"] = info.name; - j["author"] = info.author; - j["description"] = info.description; - j["version"] = info.version; - j["format"] = info.format; - j["mapId"] = info.mapId; - j["fileCount"] = info.files.size(); - j["totalBytes"] = totalSize; - nlohmann::json categories = nlohmann::json::object(); - for (const auto& [cat, count] : byCat) categories[cat] = count; - j["categories"] = categories; - std::printf("%s\n", j.dump(2).c_str()); - return 0; - } - std::printf("WCP: %s\n", path.c_str()); - std::printf(" name : %s\n", info.name.c_str()); - std::printf(" author : %s\n", info.author.c_str()); - std::printf(" description : %s\n", info.description.c_str()); - std::printf(" version : %s\n", info.version.c_str()); - std::printf(" format : %s\n", info.format.c_str()); - std::printf(" mapId : %u\n", info.mapId); - std::printf(" files : %zu\n", info.files.size()); - for (const auto& [cat, count] : byCat) { - std::printf(" %-10s : %zu\n", cat.c_str(), count); - } - std::printf(" total bytes : %.2f MB\n", totalSize / (1024.0 * 1024.0)); - return 0; - } else if (std::strcmp(argv[i], "--info-pack-budget") == 0 && i + 1 < argc) { - // Per-extension byte breakdown of a WCP archive. --info-wcp - // gives counts per category; this gives bytes per extension - // so users can spot what's bloating an archive before - // shipping. ('Why is my pack 80MB? Oh, the .glb baked - // outputs got included.') - std::string path = argv[++i]; - bool jsonOut = (i + 1 < argc && - std::strcmp(argv[i + 1], "--json") == 0); - if (jsonOut) i++; - wowee::editor::ContentPackInfo info; - if (!wowee::editor::ContentPacker::readInfo(path, info)) { - std::fprintf(stderr, - "info-pack-budget: failed to read %s\n", path.c_str()); - return 1; - } - // Sum bytes per extension (lower-cased). - std::map> byExt; - uint64_t totalBytes = 0; - for (const auto& f : info.files) { - std::string ext; - auto dot = f.path.find_last_of('.'); - if (dot != std::string::npos) ext = f.path.substr(dot); - else ext = "(no-ext)"; - std::transform(ext.begin(), ext.end(), ext.begin(), - [](unsigned char c) { return std::tolower(c); }); - byExt[ext].first++; - byExt[ext].second += f.size; - totalBytes += f.size; - } - // Sort by bytes descending. - std::vector>> sorted( - byExt.begin(), byExt.end()); - std::sort(sorted.begin(), sorted.end(), - [](const auto& a, const auto& b) { - return a.second.second > b.second.second; - }); - if (jsonOut) { - nlohmann::json j; - j["wcp"] = path; - j["totalFiles"] = info.files.size(); - j["totalBytes"] = totalBytes; - nlohmann::json arr = nlohmann::json::array(); - for (const auto& [ext, cb] : sorted) { - arr.push_back({{"ext", ext}, - {"count", cb.first}, - {"bytes", cb.second}}); - } - j["byExtension"] = arr; - std::printf("%s\n", j.dump(2).c_str()); - return 0; - } - std::printf("WCP budget: %s\n", path.c_str()); - std::printf(" total: %zu file(s), %.2f MB\n", - info.files.size(), totalBytes / (1024.0 * 1024.0)); - std::printf("\n ext count bytes KB share\n"); - for (const auto& [ext, cb] : sorted) { - double pct = totalBytes > 0 - ? 100.0 * cb.second / totalBytes : 0.0; - std::printf(" %-12s %6d %11llu %6.1f %5.1f%%\n", - ext.c_str(), cb.first, - static_cast(cb.second), - cb.second / 1024.0, pct); - } - return 0; - } else if (std::strcmp(argv[i], "--info-pack-tree") == 0 && i + 1 < argc) { - // Tree view of a WCP's directory layout with per-file byte - // sizes. --list-wcp shows the flat sorted file list; - // --info-pack-tree gives the hierarchical view that's - // easier to read for archives with subdirectories (textures - // under data/, models under buildings/, etc.). - std::string path = argv[++i]; - wowee::editor::ContentPackInfo info; - if (!wowee::editor::ContentPacker::readInfo(path, info)) { - std::fprintf(stderr, - "info-pack-tree: failed to read %s\n", path.c_str()); - return 1; - } - // Build a directory tree from flat file paths. Sub-tree - // children are sorted alphabetically with files before dirs - // (by-convention filesystem-tree look). - struct Node { - std::map> children; - bool isFile = false; - uint64_t bytes = 0; - }; - auto root = std::make_shared(); - auto split = [](const std::string& p) { - std::vector parts; - std::string cur; - for (char c : p) { - if (c == '/' || c == '\\') { - if (!cur.empty()) { parts.push_back(cur); cur.clear(); } - } else cur += c; - } - if (!cur.empty()) parts.push_back(cur); - return parts; - }; - uint64_t totalBytes = 0; - for (const auto& f : info.files) { - auto parts = split(f.path); - if (parts.empty()) continue; - Node* cur = root.get(); - for (size_t k = 0; k < parts.size(); ++k) { - auto& child = cur->children[parts[k]]; - if (!child) child = std::make_shared(); - if (k == parts.size() - 1) { - child->isFile = true; - child->bytes = f.size; - } - cur = child.get(); - } - totalBytes += f.size; - } - // Recursive renderer with box-drawing connectors. Aggregates - // child bytes up so directories show their subtotal. - std::function render = - [&](const Node* n, const std::string& prefix) -> uint64_t { - size_t i = 0; - size_t total = n->children.size(); - uint64_t subtotal = 0; - for (const auto& [name, child] : n->children) { - bool last = (++i == total); - const char* branch = last ? "└─ " : "├─ "; - const char* cont = last ? " " : "│ "; - if (child->isFile) { - std::printf("%s%s%s (%llu bytes)\n", - prefix.c_str(), branch, name.c_str(), - static_cast(child->bytes)); - subtotal += child->bytes; - } else { - // Directory — recurse, then print header with subtotal. - std::printf("%s%s%s/\n", - prefix.c_str(), branch, name.c_str()); - subtotal += render(child.get(), prefix + cont); - } - } - return subtotal; - }; - std::printf("%s (%zu files, %.2f KB)\n", - path.c_str(), info.files.size(), totalBytes / 1024.0); - render(root.get(), ""); - return 0; } else if (std::strcmp(argv[i], "--info-wot") == 0 && i + 1 < argc) { std::string base = argv[++i]; bool jsonOut = (i + 1 < argc && @@ -15134,58 +14925,6 @@ int main(int argc, char* argv[]) { std::printf(" %zu zone(s) wired up\n", zones.size()); std::printf(" next: make -C %s -j$(nproc)\n", projectDir.c_str()); return 0; - } else if (std::strcmp(argv[i], "--pack-wcp") == 0 && i + 1 < argc) { - // Pack a zone directory into a .wcp archive. - // Usage: --pack-wcp [destPath] - // If looks like a path (contains '/' or starts - // with '.'), use it directly; otherwise resolve under - // custom_zones/ then output/ (matching the discovery search - // order). - std::string nameOrDir = argv[++i]; - std::string destPath; - if (i + 1 < argc && argv[i + 1][0] != '-') { - destPath = argv[++i]; - } - namespace fs = std::filesystem; - std::string outputDir, mapName; - if (nameOrDir.find('/') != std::string::npos || nameOrDir[0] == '.') { - fs::path p = fs::absolute(nameOrDir); - outputDir = p.parent_path().string(); - mapName = p.filename().string(); - } else { - mapName = nameOrDir; - if (fs::exists("custom_zones/" + mapName)) outputDir = "custom_zones"; - else if (fs::exists("output/" + mapName)) outputDir = "output"; - else { - std::fprintf(stderr, - "--pack-wcp: zone '%s' not found in custom_zones/ or output/\n", - mapName.c_str()); - return 1; - } - } - if (destPath.empty()) destPath = mapName + ".wcp"; - wowee::editor::ContentPackInfo info; - info.name = mapName; - info.format = "wcp-1.0"; - if (!wowee::editor::ContentPacker::packZone(outputDir, mapName, destPath, info)) { - std::fprintf(stderr, "WCP pack failed for %s/%s\n", - outputDir.c_str(), mapName.c_str()); - return 1; - } - std::printf("WCP packed: %s\n", destPath.c_str()); - return 0; - } else if (std::strcmp(argv[i], "--unpack-wcp") == 0 && i + 1 < argc) { - std::string wcpPath = argv[++i]; - std::string destDir = "custom_zones"; - if (i + 1 < argc && argv[i + 1][0] != '-') { - destDir = argv[++i]; - } - if (!wowee::editor::ContentPacker::unpackZone(wcpPath, destDir)) { - std::fprintf(stderr, "WCP unpack failed: %s\n", wcpPath.c_str()); - return 1; - } - std::printf("WCP unpacked to: %s\n", destDir.c_str()); - return 0; } else if (std::strcmp(argv[i], "--list-zones") == 0) { // Optional --json after the flag for machine-readable output. bool jsonOut = (i + 1 < argc &&