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