diff --git a/CMakeLists.txt b/CMakeLists.txt index 84ae3268..7288a1cc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1440,6 +1440,7 @@ add_executable(wowee_editor tools/editor/cli_companions_catalog.cpp tools/editor/cli_spell_mechanics_catalog.cpp tools/editor/cli_keybindings_catalog.cpp + tools/editor/cli_tree_summary_md.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index d57e8b9a..adb6f1e6 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -135,7 +135,7 @@ const char* const kArgRequired[] = { "--info-wliq", "--validate-wliq", "--export-wliq-json", "--import-wliq-json", "--info-magic", "--summary-dir", "--rename-by-magic", - "--bulk-rename-by-magic", "--touch-tree", + "--bulk-rename-by-magic", "--touch-tree", "--tree-summary-md", "--gen-animations", "--gen-animations-combat", "--gen-animations-movement", "--info-wani", "--validate-wani", "--export-wani-json", "--import-wani-json", diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index c6dbdfff..b55d6570 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -89,6 +89,7 @@ #include "cli_companions_catalog.hpp" #include "cli_spell_mechanics_catalog.hpp" #include "cli_keybindings_catalog.hpp" +#include "cli_tree_summary_md.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -219,6 +220,7 @@ constexpr DispatchFn kDispatchTable[] = { handleCompanionsCatalog, handleSpellMechanicsCatalog, handleKeybindingsCatalog, + handleTreeSummaryMd, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index b21e5ceb..607ad528 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1359,6 +1359,8 @@ void printUsage(const char* argv0) { std::printf(" Apply --rename-by-magic recursively to every file in . Conflicts are skipped without --force; exits 1 if any rename failed\n"); std::printf(" --touch-tree [--json] [--quiet]\n"); std::printf(" CI integrity check: open every recognized .w* file in , parse standard header, report PASS/FAIL + extension mismatches. Exit 1 on any failure\n"); + std::printf(" --tree-summary-md [out.md]\n"); + std::printf(" Emit a Markdown report of a content tree (per-format counts + per-file detail with catalog name + entry count). Stdout if no out path\n"); std::printf(" --gen-animations [name]\n"); std::printf(" Emit .wani starter: 5 essential animations (Stand / Walk / Run / Death / AttackUnarmed) with fallback chains\n"); std::printf(" --gen-animations-combat [name]\n"); diff --git a/tools/editor/cli_tree_summary_md.cpp b/tools/editor/cli_tree_summary_md.cpp new file mode 100644 index 00000000..baf2d0ff --- /dev/null +++ b/tools/editor/cli_tree_summary_md.cpp @@ -0,0 +1,196 @@ +#include "cli_tree_summary_md.hpp" +#include "cli_arg_parse.hpp" +#include "cli_format_table.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +namespace fs = std::filesystem; + +struct FileRow { + fs::path relPath; + const FormatMagicEntry* fmt; + uint32_t version; + uint32_t entryCount; + std::string catalogName; + bool hasHeader; + uintmax_t bytes; +}; + +// Read the standard catalog header (magic+version+name+ +// entryCount). World/asset formats don't have this layout — +// for those we leave the header fields blank. +bool peekHeader(const fs::path& path, char magic[4], + uint32_t& version, std::string& catalogName, + uint32_t& entryCount) { + std::ifstream is(path, std::ios::binary); + if (!is) return false; + if (!is.read(magic, 4) || is.gcount() != 4) return false; + if (!is.read(reinterpret_cast(&version), 4)) return false; + uint32_t nameLen = 0; + if (!is.read(reinterpret_cast(&nameLen), 4)) return false; + if (nameLen > (1u << 20)) return false; + catalogName.resize(nameLen); + if (nameLen > 0) { + if (!is.read(catalogName.data(), nameLen)) return false; + } + if (!is.read(reinterpret_cast(&entryCount), 4)) return false; + return true; +} + +int handleSummary(int& i, int argc, char** argv) { + std::string dir = argv[++i]; + std::string outPath; + if (parseOptArg(i, argc, argv)) outPath = argv[++i]; + if (!fs::exists(dir) || !fs::is_directory(dir)) { + std::fprintf(stderr, + "tree-summary-md: not a directory: %s\n", dir.c_str()); + return 1; + } + std::vector rows; + std::map formatFileCounts; + std::map formatEntryCounts; + uint64_t totalFiles = 0; + uint64_t totalRecognized = 0; + uint64_t totalBytes = 0; + for (const auto& entry : fs::recursive_directory_iterator(dir)) { + if (!entry.is_regular_file()) continue; + ++totalFiles; + char magic[4]; + std::ifstream is(entry.path(), std::ios::binary); + if (!is.read(magic, 4) || is.gcount() != 4) continue; + const FormatMagicEntry* fmt = findFormatByMagic(magic); + if (!fmt) continue; + ++totalRecognized; + FileRow r; + r.relPath = fs::relative(entry.path(), dir); + r.fmt = fmt; + r.version = 0; r.entryCount = 0; + r.bytes = entry.file_size(); + // Re-open and parse standard header for catalog formats. + // Skip for world/asset formats (infoFlag == nullptr). + if (fmt->infoFlag != nullptr) { + char headerMagic[4]; + uint32_t v = 0, ec = 0; + std::string cn; + if (peekHeader(entry.path(), headerMagic, v, cn, ec)) { + r.version = v; r.catalogName = cn; + r.entryCount = ec; r.hasHeader = true; + formatEntryCounts[fmt->magic] += ec; + } else { + r.hasHeader = false; + } + } else { + r.hasHeader = false; + } + formatFileCounts[fmt->magic] += 1; + totalBytes += r.bytes; + rows.push_back(std::move(r)); + } + // Build the markdown. + std::ostringstream md; + md << "# Wowee open-format inventory: " << dir << "\n\n"; + md << "Generated by `--tree-summary-md`. " + << "Walks the directory recursively and identifies every " + << "Wowee open-format file by its 4-byte magic.\n\n"; + md << "## Summary\n\n"; + md << "| Stat | Value |\n"; + md << "|------|-------|\n"; + md << "| Total files scanned | " << totalFiles << " |\n"; + md << "| Recognized .w* files | " << totalRecognized << " |\n"; + md << "| Unrecognized files | " + << (totalFiles - totalRecognized) << " |\n"; + md << "| Total recognized bytes | " << totalBytes << " |\n\n"; + if (!formatFileCounts.empty()) { + md << "## Per-format breakdown\n\n"; + md << "| Magic | Extension | Files | Total entries | Description |\n"; + md << "|-------|-----------|-------|---------------|-------------|\n"; + for (const auto& kv : formatFileCounts) { + std::string magicStr = kv.first; + const FormatMagicEntry* fmt = nullptr; + for (const FormatMagicEntry* p = formatTableBegin(); + p != formatTableEnd(); ++p) { + if (std::memcmp(p->magic, magicStr.data(), 4) == 0) { + fmt = p; break; + } + } + uint64_t entries = formatEntryCounts.count(magicStr) + ? formatEntryCounts[magicStr] : 0; + md << "| `" << magicStr << "` | `" + << (fmt ? fmt->extension : "?") << "` | " + << kv.second << " | " << entries << " | " + << (fmt ? fmt->description : "?") << " |\n"; + } + md << "\n"; + } + if (!rows.empty()) { + md << "## Per-file detail\n\n"; + md << "| Path | Magic | Version | Catalog name | Entries | Bytes |\n"; + md << "|------|-------|---------|--------------|---------|-------|\n"; + for (const auto& r : rows) { + char ms[5] = {r.fmt->magic[0], r.fmt->magic[1], + r.fmt->magic[2], r.fmt->magic[3], 0}; + md << "| `" << r.relPath.string() << "` | `" + << ms << "` | "; + if (r.hasHeader) { + md << r.version << " | `" << r.catalogName << "` | " + << r.entryCount << " | "; + } else { + md << "- | - | - | "; + } + md << r.bytes << " |\n"; + } + md << "\n"; + } + md << "## How to inspect\n\n"; + md << "Use `--info-magic ` to identify any file by magic, " + << "or the per-format `--info-*` flag listed in `--list-formats`.\n"; + std::string output = md.str(); + if (outPath.empty()) { + std::printf("%s", output.c_str()); + } else { + std::ofstream out(outPath); + if (!out) { + std::fprintf(stderr, + "tree-summary-md: cannot write %s\n", + outPath.c_str()); + return 1; + } + out << output; + std::printf("Wrote %s\n", outPath.c_str()); + std::printf(" scanned files : %llu\n", + static_cast(totalFiles)); + std::printf(" recognized .w* : %llu\n", + static_cast(totalRecognized)); + std::printf(" unique formats : %zu\n", + formatFileCounts.size()); + } + return 0; +} + +} // namespace + +bool handleTreeSummaryMd(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--tree-summary-md") == 0 && + i + 1 < argc) { + outRc = handleSummary(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_tree_summary_md.hpp b/tools/editor/cli_tree_summary_md.hpp new file mode 100644 index 00000000..942bee0a --- /dev/null +++ b/tools/editor/cli_tree_summary_md.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleTreeSummaryMd(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee