diff --git a/CMakeLists.txt b/CMakeLists.txt index 47c0abcf..111cac32 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1483,6 +1483,7 @@ add_executable(wowee_editor tools/editor/cli_bulk_json.cpp tools/editor/cli_diff_tree.cpp tools/editor/cli_orphan_jsons.cpp + tools/editor/cli_list_by_magic.cpp tools/editor/cli_macros_catalog.cpp tools/editor/cli_char_features_catalog.cpp tools/editor/cli_pvp_catalog.cpp diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index c103fc9f..fd8bf851 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -139,7 +139,7 @@ const char* const kArgRequired[] = { "--catalog-grep", "--diff-headers", "--audit-tree", "--magic-fix", "--bulk-validate", "--bulk-export-json", "--bulk-import-json", - "--diff-tree", "--orphan-jsons", + "--diff-tree", "--orphan-jsons", "--list-by-magic", "--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 c6d5ccf2..a4d7d9a3 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -100,6 +100,7 @@ #include "cli_bulk_json.hpp" #include "cli_diff_tree.hpp" #include "cli_orphan_jsons.hpp" +#include "cli_list_by_magic.hpp" #include "cli_macros_catalog.hpp" #include "cli_char_features_catalog.hpp" #include "cli_pvp_catalog.hpp" @@ -271,6 +272,7 @@ constexpr DispatchFn kDispatchTable[] = { handleBulkJson, handleDiffTree, handleOrphanJsons, + handleListByMagic, handleMacrosCatalog, handleCharFeaturesCatalog, handlePVPCatalog, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index f7b0a030..cf76e401 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1379,6 +1379,8 @@ void printUsage(const char* argv0) { std::printf(" Compare two directory trees of .w* catalogs at the magic+size level. Reports only-in-A / only-in-B / magic-changed / size-changed / identical counts and lists changed paths. Exit 1 if any difference\n"); std::printf(" --orphan-jsons [--json]\n"); std::printf(" Find .wXXX.json sidecars whose binary .wXXX is missing. Useful after deleting/moving binaries — orphan JSONs accumulate noise and may shadow re-imports. Exit 1 if any orphans found\n"); + std::printf(" --list-by-magic [--json]\n"); + std::printf(" List every file in a directory tree matching a 4-char magic (e.g. WSPL). Reports per-file size + entry count + catalog name + relative path. Exit 1 if no matches\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_list_by_magic.cpp b/tools/editor/cli_list_by_magic.cpp new file mode 100644 index 00000000..e8df1dc7 --- /dev/null +++ b/tools/editor/cli_list_by_magic.cpp @@ -0,0 +1,180 @@ +#include "cli_list_by_magic.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; + +struct Match { + fs::path path; + uintmax_t size = 0; + uint32_t entryCount = 0; + std::string catalogName; +}; + +// Read magic + version + name + entryCount. Same shape as +// the helper in cli_summary_dir — kept local to avoid +// header-only utility ping-pong. +bool peekHeader(const fs::path& path, char magic[4], + std::string& nameOut, 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; + nameOut.resize(nameLen); + if (nameLen > 0) { + is.read(nameOut.data(), nameLen); + if (is.gcount() != static_cast(nameLen)) { + nameOut.clear(); + return false; + } + } + if (!is.read(reinterpret_cast(&entryCountOut), 4)) + return false; + return true; +} + +// Normalize a 4-character magic input. Accepts both +// "WSPL" and "wspl" (case-insensitive uppercase). +bool parseMagicArg(const char* arg, char magic[4]) { + if (!arg) return false; + size_t n = std::strlen(arg); + if (n != 4) return false; + for (int i = 0; i < 4; ++i) { + char c = arg[i]; + if (c >= 'a' && c <= 'z') c = c - 'a' + 'A'; + magic[i] = c; + } + return true; +} + +int handleList(int& i, int argc, char** argv) { + std::string dir = argv[++i]; + std::string magicArg = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + if (!fs::exists(dir) || !fs::is_directory(dir)) { + std::fprintf(stderr, + "list-by-magic: not a directory: %s\n", dir.c_str()); + return 1; + } + char wantedMagic[4]; + if (!parseMagicArg(magicArg.c_str(), wantedMagic)) { + std::fprintf(stderr, + "list-by-magic: magic must be exactly 4 characters: %s\n", + magicArg.c_str()); + return 1; + } + const FormatMagicEntry* fmt = findFormatByMagic(wantedMagic); + // fmt is allowed to be null — we still match files + // with that magic, just won't have format metadata for + // the report header. + std::vector matches; + uintmax_t totalBytes = 0; + uint64_t totalEntries = 0; + for (const auto& entry : fs::recursive_directory_iterator(dir)) { + if (!entry.is_regular_file()) continue; + char magic[4]; + std::string nameField; + uint32_t entryCount = 0; + if (!peekHeader(entry.path(), magic, nameField, entryCount)) + continue; + if (std::memcmp(magic, wantedMagic, 4) != 0) continue; + Match m; + m.path = entry.path(); + m.size = entry.file_size(); + m.entryCount = entryCount; + m.catalogName = nameField; + totalBytes += m.size; + totalEntries += entryCount; + matches.push_back(std::move(m)); + } + if (jsonOut) { + nlohmann::json j; + j["dir"] = dir; + char ms[5] = {wantedMagic[0], wantedMagic[1], + wantedMagic[2], wantedMagic[3], 0}; + j["magic"] = ms; + if (fmt) { + j["format"] = fmt->extension; + j["category"] = fmt->category; + j["description"] = fmt->description; + } else { + j["format"] = nullptr; + } + j["count"] = matches.size(); + j["totalBytes"] = totalBytes; + j["totalEntries"] = totalEntries; + nlohmann::json arr = nlohmann::json::array(); + for (const auto& m : matches) { + arr.push_back({ + {"path", fs::relative(m.path, dir).string()}, + {"size", m.size}, + {"entryCount", m.entryCount}, + {"catalogName", m.catalogName}, + }); + } + j["matches"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return matches.empty() ? 1 : 0; + } + char ms[5] = {wantedMagic[0], wantedMagic[1], + wantedMagic[2], wantedMagic[3], 0}; + std::printf("list-by-magic: '%s' in %s\n", ms, dir.c_str()); + if (fmt) { + std::printf(" format : %s (%s, %s)\n", + fmt->description, fmt->extension, fmt->category); + } else { + std::printf(" format : (unknown magic — no metadata)\n"); + } + std::printf(" matches : %zu\n", matches.size()); + std::printf(" total : %llu bytes, %llu entries\n", + static_cast(totalBytes), + static_cast(totalEntries)); + if (matches.empty()) { + std::printf(" no files with magic '%s' under %s\n", + ms, dir.c_str()); + return 1; + } + std::printf("\n"); + std::printf(" bytes entries catalogName path\n"); + for (const auto& m : matches) { + std::printf(" %8llu %6u %-25s %s\n", + static_cast(m.size), + m.entryCount, + m.catalogName.c_str(), + fs::relative(m.path, dir).string().c_str()); + } + return 0; +} + +} // namespace + +bool handleListByMagic(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--list-by-magic") == 0 && i + 2 < argc) { + outRc = handleList(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_list_by_magic.hpp b/tools/editor/cli_list_by_magic.hpp new file mode 100644 index 00000000..e1f1e450 --- /dev/null +++ b/tools/editor/cli_list_by_magic.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleListByMagic(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee