From 34c7021e5ca9a4fe92fe398f27a4442a912b5459 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 20:27:16 -0700 Subject: [PATCH] feat(editor): add --catalog-grep search-by-name across content tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recursively walks a directory, parses the standard catalog header (magic + version + name + entryCount) of every recognized catalog format, and reports files whose internal catalog NAME field matches a pattern. Useful when you've got a content bundle and need to find "where is the catalog named WintergraspUI?" or "list every Starter* preset in this directory" without per-format parsing. Case-insensitive substring match by default (--case-sensitive opts in to literal match). Returns exit 1 when no match — designed for shell composition (`if catalog-grep ... ; then ...`). World/asset formats (.wom/.wob/.whm/.wot/.wow) are skipped since they don't follow the catalog-header layout. Supports --json variant for tooling integration. Reuses cli_format_table.cpp so any new catalog format is searchable automatically. --- CMakeLists.txt | 1 + tools/editor/cli_arg_required.cpp | 1 + tools/editor/cli_catalog_grep.cpp | 168 ++++++++++++++++++++++++++++++ tools/editor/cli_catalog_grep.hpp | 11 ++ tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_help.cpp | 2 + 6 files changed, 185 insertions(+) create mode 100644 tools/editor/cli_catalog_grep.cpp create mode 100644 tools/editor/cli_catalog_grep.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0829d853..e4580323 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1445,6 +1445,7 @@ add_executable(wowee_editor tools/editor/cli_tree_summary_md.cpp tools/editor/cli_spell_schools_catalog.cpp tools/editor/cli_lfg_catalog.cpp + tools/editor/cli_catalog_grep.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 f91709ad..5b01e7a7 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -136,6 +136,7 @@ const char* const kArgRequired[] = { "--export-wliq-json", "--import-wliq-json", "--info-magic", "--summary-dir", "--rename-by-magic", "--bulk-rename-by-magic", "--touch-tree", "--tree-summary-md", + "--catalog-grep", "--gen-animations", "--gen-animations-combat", "--gen-animations-movement", "--info-wani", "--validate-wani", "--export-wani-json", "--import-wani-json", diff --git a/tools/editor/cli_catalog_grep.cpp b/tools/editor/cli_catalog_grep.cpp new file mode 100644 index 00000000..8d78f8b5 --- /dev/null +++ b/tools/editor/cli_catalog_grep.cpp @@ -0,0 +1,168 @@ +#include "cli_catalog_grep.hpp" +#include "cli_arg_parse.hpp" +#include "cli_format_table.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +namespace fs = std::filesystem; + +struct GrepHit { + fs::path relPath; + const FormatMagicEntry* fmt; + uint32_t version; + uint32_t entryCount; + std::string catalogName; +}; + +std::string toLower(std::string s) { + for (char& c : s) c = static_cast(std::tolower(static_cast(c))); + return s; +} + +bool readStandardHeader(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 handleGrep(int& i, int argc, char** argv) { + std::string pattern = argv[++i]; + if (i + 1 >= argc) { + std::fprintf(stderr, + "catalog-grep: missing argument after pattern\n"); + return 1; + } + std::string dir = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + bool caseSensitive = false; + while (i + 1 < argc) { + std::string a = argv[i + 1]; + if (a == "--case-sensitive") { caseSensitive = true; ++i; } + else break; + } + if (!fs::exists(dir) || !fs::is_directory(dir)) { + std::fprintf(stderr, + "catalog-grep: not a directory: %s\n", dir.c_str()); + return 1; + } + std::string needle = caseSensitive ? pattern : toLower(pattern); + std::vector hits; + uint64_t scanned = 0; + uint64_t recognized = 0; + for (const auto& entry : fs::recursive_directory_iterator(dir)) { + if (!entry.is_regular_file()) continue; + ++scanned; + char magic[4]; + uint32_t version = 0, entryCount = 0; + std::string catalogName; + if (!readStandardHeader(entry.path(), magic, version, + catalogName, entryCount)) { + continue; + } + const FormatMagicEntry* fmt = findFormatByMagic(magic); + // Only catalog formats have the standard header; world/ + // asset formats (infoFlag == nullptr) won't match. + if (!fmt || fmt->infoFlag == nullptr) continue; + ++recognized; + std::string haystack = + caseSensitive ? catalogName : toLower(catalogName); + if (haystack.find(needle) == std::string::npos) continue; + GrepHit h; + h.relPath = fs::relative(entry.path(), dir); + h.fmt = fmt; + h.version = version; + h.entryCount = entryCount; + h.catalogName = catalogName; + hits.push_back(std::move(h)); + } + if (jsonOut) { + nlohmann::json j; + j["pattern"] = pattern; + j["caseSensitive"] = caseSensitive; + j["dir"] = dir; + j["scanned"] = scanned; + j["recognized"] = recognized; + j["matches"] = hits.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& h : hits) { + char ms[5] = {h.fmt->magic[0], h.fmt->magic[1], + h.fmt->magic[2], h.fmt->magic[3], 0}; + arr.push_back({ + {"path", h.relPath.string()}, + {"magic", ms}, + {"extension", h.fmt->extension}, + {"version", h.version}, + {"entryCount", h.entryCount}, + {"catalogName", h.catalogName}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return hits.empty() ? 1 : 0; + } + std::printf("catalog-grep: pattern='%s'%s in %s\n", + pattern.c_str(), + caseSensitive ? " (case-sensitive)" : "", + dir.c_str()); + std::printf(" scanned : %llu\n", + static_cast(scanned)); + std::printf(" recognized : %llu (catalog headers parsed)\n", + static_cast(recognized)); + std::printf(" matches : %zu\n", hits.size()); + if (hits.empty()) { + std::printf(" (no catalog names matched)\n"); + return 1; + } + std::printf("\n"); + std::printf(" magic ext ver entries catalogName path\n"); + std::printf(" ------ ------- --- ------- ----------------------- ------\n"); + for (const auto& h : hits) { + char ms[5] = {h.fmt->magic[0], h.fmt->magic[1], + h.fmt->magic[2], h.fmt->magic[3], 0}; + std::printf(" %-6s %-7s %3u %7u %-23s %s\n", + ms, h.fmt->extension, h.version, h.entryCount, + h.catalogName.c_str(), h.relPath.string().c_str()); + } + return 0; +} + +} // namespace + +bool handleCatalogGrep(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--catalog-grep") == 0 && i + 2 < argc) { + outRc = handleGrep(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_catalog_grep.hpp b/tools/editor/cli_catalog_grep.hpp new file mode 100644 index 00000000..43498ff8 --- /dev/null +++ b/tools/editor/cli_catalog_grep.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleCatalogGrep(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index ec38ca3e..599989e4 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -92,6 +92,7 @@ #include "cli_tree_summary_md.hpp" #include "cli_spell_schools_catalog.hpp" #include "cli_lfg_catalog.hpp" +#include "cli_catalog_grep.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -225,6 +226,7 @@ constexpr DispatchFn kDispatchTable[] = { handleTreeSummaryMd, handleSpellSchoolsCatalog, handleLFGCatalog, + handleCatalogGrep, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index d1d27151..bce6dcab 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1361,6 +1361,8 @@ void printUsage(const char* argv0) { 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(" --catalog-grep [--case-sensitive] [--json]\n"); + std::printf(" Recursively search catalog NAMES (the internal name field) across .w* files in . Case-insensitive by default. Exit 1 if no match\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");