From 824a6c8cabba87d5b727fb7f2cf3462e29365b8e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 19:27:48 -0700 Subject: [PATCH] feat(editor): add --summary-dir bulk content inventory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recursively walks a directory, identifies every file by 4-byte magic, and reports per-format file count, total entries, and bytes. Useful for content audits ("how many cinematics did this asset bundle ship?") and for tracking migration progress ("what fraction of zones still lack a holiday catalog?"). Also extracts the format-magic table out of cli_info_magic.cpp into a shared cli_format_table.{hpp,cpp} so --info-magic and --summary-dir reuse the same source of truth — adding a new format now updates one row in one file instead of two. Both flags use the standard catalog header (magic + version + name + entryCount) for catalog formats; asset/world formats are counted but report 0 entries since their headers differ. Supports --json variant for tooling integration. --- CMakeLists.txt | 2 + tools/editor/cli_arg_required.cpp | 2 +- tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_format_table.cpp | 78 +++++++++++++ tools/editor/cli_format_table.hpp | 37 ++++++ tools/editor/cli_help.cpp | 2 + tools/editor/cli_info_magic.cpp | 74 +----------- tools/editor/cli_summary_dir.cpp | 181 ++++++++++++++++++++++++++++++ tools/editor/cli_summary_dir.hpp | 11 ++ 9 files changed, 316 insertions(+), 73 deletions(-) create mode 100644 tools/editor/cli_format_table.cpp create mode 100644 tools/editor/cli_format_table.hpp create mode 100644 tools/editor/cli_summary_dir.cpp create mode 100644 tools/editor/cli_summary_dir.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 67cbdd0e..08a03173 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1418,6 +1418,8 @@ add_executable(wowee_editor tools/editor/cli_info_magic.cpp tools/editor/cli_animations_catalog.cpp tools/editor/cli_spell_visuals_catalog.cpp + tools/editor/cli_format_table.cpp + tools/editor/cli_summary_dir.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 9562944b..416b7400 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -134,7 +134,7 @@ const char* const kArgRequired[] = { "--gen-liquids", "--gen-liquids-magical", "--gen-liquids-hazardous", "--info-wliq", "--validate-wliq", "--export-wliq-json", "--import-wliq-json", - "--info-magic", + "--info-magic", "--summary-dir", "--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 758af492..8acf429e 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -77,6 +77,7 @@ #include "cli_info_magic.hpp" #include "cli_animations_catalog.hpp" #include "cli_spell_visuals_catalog.hpp" +#include "cli_summary_dir.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -195,6 +196,7 @@ constexpr DispatchFn kDispatchTable[] = { handleInfoMagic, handleAnimationsCatalog, handleSpellVisualsCatalog, + handleSummaryDir, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp new file mode 100644 index 00000000..1e3cf2f7 --- /dev/null +++ b/tools/editor/cli_format_table.cpp @@ -0,0 +1,78 @@ +#include "cli_format_table.hpp" + +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +constexpr FormatMagicEntry kFormats[] = { + {{'W','O','M',' '}, ".wom", "asset", nullptr, "M2 model"}, + {{'W','O','B',' '}, ".wob", "asset", nullptr, "WMO building"}, + {{'W','H','M',' '}, ".whm", "world", nullptr, "ADT heightmap"}, + {{'W','O','T',' '}, ".wot", "world", nullptr, "ADT textures"}, + {{'W','O','W',' '}, ".wow", "world", nullptr, "Per-zone world manifest"}, + {{'W','I','T','M'}, ".wit", "items", "--info-witm", "Item catalog"}, + {{'W','C','R','T'}, ".wcrt", "creatures", "--info-creatures", "Creature catalog"}, + {{'W','S','P','N'}, ".wspn", "spawns", "--info-spawns", "Spawn catalog"}, + {{'W','L','O','T'}, ".wlot", "loot", "--info-loot", "Loot tables"}, + {{'W','G','O','T'}, ".wgot", "objects", "--info-objects", "GameObject catalog"}, + {{'W','S','N','D'}, ".wsnd", "audio", "--info-sound", "Sound entries"}, + {{'W','S','P','L'}, ".wspl", "spells", "--info-wspl", "Spell catalog"}, + {{'W','Q','T','M'}, ".wqt", "quests", "--info-quests", "Quest catalog"}, + {{'W','M','S','X'}, ".wms", "maps", "--info-wms", "Map / area catalog"}, + {{'W','C','H','C'}, ".wchc", "chars", "--info-wchc", "Class + race catalog"}, + {{'W','A','C','H'}, ".wach", "achieve", "--info-wach", "Achievement catalog"}, + {{'W','T','R','N'}, ".wtrr", "trainers", "--info-wtrr", "Trainer catalog"}, + {{'W','G','S','P'}, ".wgoss", "gossip", "--info-wgoss", "Gossip menu catalog"}, + {{'W','T','A','X'}, ".wtax", "taxi", "--info-wtax", "Taxi node catalog"}, + {{'W','T','A','L'}, ".wtal", "talents", "--info-wtal", "Talent catalog"}, + {{'W','T','K','N'}, ".wtkn", "tokens", "--info-wtkn", "Token catalog"}, + {{'W','T','R','G'}, ".wtrg", "triggers", "--info-wtrg", "Trigger catalog"}, + {{'W','T','I','T'}, ".wttl", "titles", "--info-wttl", "Title catalog"}, + {{'W','S','E','A'}, ".wevt", "events", "--info-wevt", "Event catalog"}, + {{'W','M','O','U'}, ".wmnt", "mounts", "--info-wmnt", "Mount catalog"}, + {{'W','B','G','D'}, ".wbgd", "battle", "--info-wbgd", "Battleground catalog"}, + {{'W','M','A','L'}, ".wmal", "mail", "--info-wmal", "Mail catalog"}, + {{'W','G','E','M'}, ".wgem", "gems", "--info-wgem", "Gem catalog"}, + {{'W','G','L','D'}, ".wgld", "guilds", "--info-wgld", "Guild catalog"}, + {{'W','P','C','D'}, ".wcnd", "cond", "--info-wcnd", "Condition catalog"}, + {{'W','P','E','T'}, ".wpet", "pets", "--info-wpet", "Pet catalog"}, + {{'W','A','U','C'}, ".wauc", "auction", "--info-wauc", "Auction catalog"}, + {{'W','C','H','N'}, ".wchn", "channels", "--info-wchn", "Channel catalog"}, + {{'W','C','M','S'}, ".wcms", "cinematic", "--info-wcms", "Cinematic catalog"}, + {{'W','G','L','Y'}, ".wgly", "glyphs", "--info-wgly", "Glyph catalog"}, + {{'W','V','H','C'}, ".wvhc", "vehicles", "--info-wvhc", "Vehicle catalog"}, + {{'W','H','O','L'}, ".whol", "holiday", "--info-whol", "Holiday catalog"}, + {{'W','L','I','Q'}, ".wliq", "liquids", "--info-wliq", "Liquid catalog"}, + {{'W','A','N','I'}, ".wani", "anim", "--info-wani", "Animation catalog"}, + {{'W','S','V','K'}, ".wsvk", "spellfx", "--info-wsvk", "Spell visual kit catalog"}, + {{'W','F','A','C'}, ".wfac", "factions", nullptr, "Faction catalog"}, + {{'W','L','C','K'}, ".wlck", "locks", nullptr, "Lock catalog"}, + {{'W','S','K','L'}, ".wskl", "skills", nullptr, "Skill catalog"}, + {{'W','O','L','A'}, ".wola", "light", nullptr, "Outdoor light catalog"}, + {{'W','O','W','A'}, ".wowa", "weather", nullptr, "Weather schedule catalog"}, + {{'W','M','P','X'}, ".wmpx", "worldmap", nullptr, "World map catalog"}, +}; + +constexpr size_t kFormatsCount = + sizeof(kFormats) / sizeof(kFormats[0]); + +} // namespace + +const FormatMagicEntry* findFormatByMagic(const char magic[4]) { + for (const auto& row : kFormats) { + if (std::memcmp(row.magic, magic, 4) == 0) return &row; + } + return nullptr; +} + +const FormatMagicEntry* formatTableBegin() { return kFormats; } +const FormatMagicEntry* formatTableEnd() { return kFormats + kFormatsCount; } +size_t formatTableSize() { return kFormatsCount; } + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_format_table.hpp b/tools/editor/cli_format_table.hpp new file mode 100644 index 00000000..4a1b16cb --- /dev/null +++ b/tools/editor/cli_format_table.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +// Shared table of every novel open format the editor +// recognizes — extracted so --info-magic and --summary-dir +// can both look up files by their 4-byte magic without +// drifting. Adding a new format requires appending one row +// in cli_format_table.cpp. +struct FormatMagicEntry { + char magic[4]; // 4-char binary magic + const char* extension; // file suffix (with dot) + const char* category; // grouping label + const char* infoFlag; // --info-* flag, nullptr if none + const char* description; +}; + +// Returns a pointer into the static table on match, nullptr +// otherwise. The 4-byte magic argument does NOT need to be +// null-terminated — only the first 4 bytes are inspected. +const FormatMagicEntry* findFormatByMagic(const char magic[4]); + +// Iterate the table — used by --summary-dir to pre-allocate +// per-format counters keyed by index, and by tooling that +// wants to enumerate the full set. +const FormatMagicEntry* formatTableBegin(); +const FormatMagicEntry* formatTableEnd(); +size_t formatTableSize(); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 53b354aa..d94988e2 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1351,6 +1351,8 @@ void printUsage(const char* argv0) { std::printf(" Print the catalog of all novel open formats (magic / extension / category / replaces / description)\n"); std::printf(" --info-magic [--json]\n"); std::printf(" Auto-detect any .w* file by 4-byte magic; report format / version / catalog name / entry count + suggest --info-* flag\n"); + std::printf(" --summary-dir [--json]\n"); + std::printf(" Recursively walk a directory; report per-format file count, total entries, and bytes for every Wowee open format found\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_info_magic.cpp b/tools/editor/cli_info_magic.cpp index 5446e6a4..6c61fad2 100644 --- a/tools/editor/cli_info_magic.cpp +++ b/tools/editor/cli_info_magic.cpp @@ -1,5 +1,6 @@ #include "cli_info_magic.hpp" #include "cli_arg_parse.hpp" +#include "cli_format_table.hpp" #include @@ -15,77 +16,6 @@ namespace cli { namespace { -// Format identification table — duplicated semantics with -// cli_list_formats.cpp because the two flags can drift in -// what they expose. Keep this list aligned when adding new -// formats. Entries ordered as they appear in --list-formats. -struct MagicEntry { - char magic[4]; - const char* extension; - const char* category; - const char* infoFlag; // suggested --info-* flag (or null) - const char* description; -}; - -constexpr MagicEntry kMagicTable[] = { - {{'W','O','M',' '}, ".wom", "asset", nullptr, "M2 model"}, - {{'W','O','B',' '}, ".wob", "asset", nullptr, "WMO building"}, - {{'W','H','M',' '}, ".whm", "world", nullptr, "ADT heightmap"}, - {{'W','O','T',' '}, ".wot", "world", nullptr, "ADT textures"}, - {{'W','O','W',' '}, ".wow", "world", nullptr, "Per-zone world manifest"}, - {{'W','I','T','M'}, ".wit", "items", "--info-witm", "Item catalog"}, - {{'W','C','R','T'}, ".wcrt", "creatures", "--info-creatures", "Creature catalog"}, - {{'W','S','P','N'}, ".wspn", "spawns", "--info-spawns", "Spawn catalog"}, - {{'W','L','O','T'}, ".wlot", "loot", "--info-loot", "Loot tables"}, - {{'W','G','O','T'}, ".wgot", "objects", "--info-objects", "GameObject catalog"}, - {{'W','S','N','D'}, ".wsnd", "audio", "--info-sound", "Sound entries"}, - {{'W','S','P','L'}, ".wspl", "spells", "--info-wspl", "Spell catalog"}, - {{'W','Q','T','M'}, ".wqt", "quests", "--info-quests", "Quest catalog"}, - {{'W','M','S','X'}, ".wms", "maps", "--info-wms", "Map / area catalog"}, - {{'W','C','H','C'}, ".wchc", "chars", "--info-wchc", "Class + race catalog"}, - {{'W','A','C','H'}, ".wach", "achieve", "--info-wach", "Achievement catalog"}, - {{'W','T','R','N'}, ".wtrr", "trainers", "--info-wtrr", "Trainer catalog"}, - {{'W','G','S','P'}, ".wgoss", "gossip", "--info-wgoss", "Gossip menu catalog"}, - {{'W','T','A','X'}, ".wtax", "taxi", "--info-wtax", "Taxi node catalog"}, - {{'W','T','A','L'}, ".wtal", "talents", "--info-wtal", "Talent catalog"}, - {{'W','T','K','N'}, ".wtkn", "tokens", "--info-wtkn", "Token catalog"}, - {{'W','T','R','G'}, ".wtrg", "triggers", "--info-wtrg", "Trigger catalog"}, - {{'W','T','I','T'}, ".wttl", "titles", "--info-wttl", "Title catalog"}, - {{'W','S','E','A'}, ".wevt", "events", "--info-wevt", "Event catalog"}, - {{'W','M','O','U'}, ".wmnt", "mounts", "--info-wmnt", "Mount catalog"}, - {{'W','B','G','D'}, ".wbgd", "battle", "--info-wbgd", "Battleground catalog"}, - {{'W','M','A','L'}, ".wmal", "mail", "--info-wmal", "Mail catalog"}, - {{'W','G','E','M'}, ".wgem", "gems", "--info-wgem", "Gem catalog"}, - {{'W','G','L','D'}, ".wgld", "guilds", "--info-wgld", "Guild catalog"}, - {{'W','P','C','D'}, ".wcnd", "cond", "--info-wcnd", "Condition catalog"}, - {{'W','P','E','T'}, ".wpet", "pets", "--info-wpet", "Pet catalog"}, - {{'W','A','U','C'}, ".wauc", "auction", "--info-wauc", "Auction catalog"}, - {{'W','C','H','N'}, ".wchn", "channels", "--info-wchn", "Channel catalog"}, - {{'W','C','M','S'}, ".wcms", "cinematic", "--info-wcms", "Cinematic catalog"}, - {{'W','G','L','Y'}, ".wgly", "glyphs", "--info-wgly", "Glyph catalog"}, - {{'W','V','H','C'}, ".wvhc", "vehicles", "--info-wvhc", "Vehicle catalog"}, - {{'W','H','O','L'}, ".whol", "holiday", "--info-whol", "Holiday catalog"}, - {{'W','L','I','Q'}, ".wliq", "liquids", "--info-wliq", "Liquid catalog"}, - {{'W','A','N','I'}, ".wani", "anim", "--info-wani", "Animation catalog"}, - {{'W','S','V','K'}, ".wsvk", "spellfx", "--info-wsvk", "Spell visual kit catalog"}, - {{'W','F','A','C'}, ".wfac", "factions", nullptr, "Faction catalog"}, - {{'W','L','C','K'}, ".wlck", "locks", nullptr, "Lock catalog"}, - {{'W','S','K','L'}, ".wskl", "skills", nullptr, "Skill catalog"}, - {{'W','O','L','A'}, ".wola", "light", nullptr, "Outdoor light catalog"}, - {{'W','O','W','A'}, ".wowa", "weather", nullptr, "Weather schedule catalog"}, - {{'W','M','P','X'}, ".wmpx", "worldmap", nullptr, "World map catalog"}, -}; - -constexpr size_t kMagicTableCount = - sizeof(kMagicTable) / sizeof(kMagicTable[0]); - -const MagicEntry* findEntry(const char magic[4]) { - for (const auto& row : kMagicTable) { - if (std::memcmp(row.magic, magic, 4) == 0) return &row; - } - return nullptr; -} - // Read the 4-byte magic + 4-byte version + length-prefixed // catalog name + 4-byte entry count from the standard // header that every Wowee catalog format shares. Asset and @@ -131,7 +61,7 @@ int handleMagic(int& i, int argc, char** argv) { path.c_str()); return 1; } - const MagicEntry* entry = findEntry(magic); + const FormatMagicEntry* entry = findFormatByMagic(magic); StandardHeader hdr; bool standardOk = false; // World/asset formats have non-standard headers — skip diff --git a/tools/editor/cli_summary_dir.cpp b/tools/editor/cli_summary_dir.cpp new file mode 100644 index 00000000..62998dd3 --- /dev/null +++ b/tools/editor/cli_summary_dir.cpp @@ -0,0 +1,181 @@ +#include "cli_summary_dir.hpp" +#include "cli_arg_parse.hpp" +#include "cli_format_table.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +namespace fs = std::filesystem; + +// Per-format counters populated as we walk a directory. +struct FormatBucket { + const FormatMagicEntry* fmt = nullptr; + uint64_t fileCount = 0; + uint64_t totalEntries = 0; + uint64_t totalBytes = 0; +}; + +// Read magic+version+name+entryCount from a candidate file. +// Returns true on success and fills out entryCount; the +// version+name fields are skipped — we only need the count. +bool peekEntryCount(const fs::path& path, char magic[4], + uint32_t& entryCountOut) { + std::ifstream is(path, std::ios::binary); + if (!is) return false; + if (!is.read(magic, 4) || is.gcount() != 4) return false; + uint32_t version = 0; + 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; + is.seekg(nameLen, std::ios::cur); + if (!is.read(reinterpret_cast(&entryCountOut), 4)) return false; + return true; +} + +int handleSummary(int& i, int argc, char** argv) { + std::string dir = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + if (!fs::exists(dir) || !fs::is_directory(dir)) { + std::fprintf(stderr, + "summary-dir: not a directory: %s\n", dir.c_str()); + return 1; + } + std::vector buckets; + buckets.reserve(formatTableSize()); + for (const FormatMagicEntry* p = formatTableBegin(); + p != formatTableEnd(); ++p) { + FormatBucket b; + b.fmt = p; + buckets.push_back(b); + } + uint64_t totalFiles = 0; + uint64_t totalUnknown = 0; + uint64_t totalBytes = 0; + std::vector unknownFiles; + for (const auto& entry : fs::recursive_directory_iterator(dir)) { + if (!entry.is_regular_file()) continue; + ++totalFiles; + char magic[4]; + uint32_t entryCount = 0; + bool readOk = peekEntryCount(entry.path(), magic, entryCount); + const FormatMagicEntry* fmt = nullptr; + if (readOk) fmt = findFormatByMagic(magic); + if (!fmt) { + ++totalUnknown; + // Only collect the first 20 unknown files to + // keep the output manageable on large dirs. + if (unknownFiles.size() < 20) { + unknownFiles.push_back( + fs::relative(entry.path(), dir).string()); + } + continue; + } + uintmax_t fileSize = entry.file_size(); + totalBytes += fileSize; + for (auto& b : buckets) { + if (b.fmt == fmt) { + ++b.fileCount; + // Only catalog formats (those with an + // --info-* flag) follow the standard + // header — for asset/world formats we + // still count the file but leave entries + // at 0. + if (b.fmt->infoFlag != nullptr) { + b.totalEntries += entryCount; + } + b.totalBytes += fileSize; + break; + } + } + } + uint64_t recognized = totalFiles - totalUnknown; + if (jsonOut) { + nlohmann::json j; + j["dir"] = dir; + j["totalFiles"] = totalFiles; + j["recognized"] = recognized; + j["unknown"] = totalUnknown; + j["totalBytes"] = totalBytes; + nlohmann::json arr = nlohmann::json::array(); + for (const auto& b : buckets) { + if (b.fileCount == 0) continue; + char magicStr[5] = {b.fmt->magic[0], b.fmt->magic[1], + b.fmt->magic[2], b.fmt->magic[3], 0}; + arr.push_back({ + {"magic", magicStr}, + {"extension", b.fmt->extension}, + {"category", b.fmt->category}, + {"description", b.fmt->description}, + {"fileCount", b.fileCount}, + {"totalEntries", b.totalEntries}, + {"totalBytes", b.totalBytes}, + }); + } + j["formats"] = arr; + if (!unknownFiles.empty()) { + j["unknownSample"] = unknownFiles; + } + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("summary-dir: %s\n", dir.c_str()); + std::printf(" total files : %llu\n", + static_cast(totalFiles)); + std::printf(" recognized .w* : %llu\n", + static_cast(recognized)); + std::printf(" unrecognized : %llu\n", + static_cast(totalUnknown)); + std::printf(" recognized bytes : %llu\n", + static_cast(totalBytes)); + if (recognized == 0) { + std::printf(" (no Wowee open format files found)\n"); + return 0; + } + std::printf("\n"); + std::printf(" magic ext category files entries bytes\n"); + std::printf(" ------- ------- ----------- ------ -------- --------\n"); + for (const auto& b : buckets) { + if (b.fileCount == 0) continue; + char magicStr[5] = {b.fmt->magic[0], b.fmt->magic[1], + b.fmt->magic[2], b.fmt->magic[3], 0}; + std::printf(" %-7s %-7s %-11s %5llu %7llu %8llu\n", + magicStr, b.fmt->extension, b.fmt->category, + static_cast(b.fileCount), + static_cast(b.totalEntries), + static_cast(b.totalBytes)); + } + if (!unknownFiles.empty()) { + std::printf("\n sample of unrecognized files (up to 20):\n"); + for (const auto& f : unknownFiles) { + std::printf(" - %s\n", f.c_str()); + } + } + return 0; +} + +} // namespace + +bool handleSummaryDir(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--summary-dir") == 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_summary_dir.hpp b/tools/editor/cli_summary_dir.hpp new file mode 100644 index 00000000..3eb95b88 --- /dev/null +++ b/tools/editor/cli_summary_dir.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleSummaryDir(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee