From f43b44405641a503d16f18b2793a959e51feac10 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 23:07:09 -0700 Subject: [PATCH] feat(editor): add --orphan-jsons to find sidecars without binaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Walks a directory recursively and flags every .wXXX.json sidecar whose matching binary .wXXX is missing. Useful after deleting or moving binary catalogs — the orphan JSON sidecars accumulate as noise and may shadow re-imports if --bulk-import-json runs over the tree (since import would re-create the binary from possibly stale JSON). Reports total sidecars / paired / orphan counts, lists orphan paths with their expected binary, and suggests recovery (re-import to recreate from JSON, or delete the orphan sidecars). Returns exit 1 if any orphans found, so it composes into shell pipelines and CI checks. Companion to --bulk-export-json + --bulk-import-json: those two go between binary <-> JSON; this one keeps the binary side in sync. Suggested workflow: --bulk-export-json mydir # generate sidecars git rm something.wsrg # delete a binary --orphan-jsons mydir # detects something.wsrg.json # is now orphaned Reuses the same case-insensitive extension matcher as --audit-tree and --magic-fix. CLI flag count 1017 -> 1018. This is the 16th cross-format utility: --list-formats / --info-magic / --summary-dir / --rename-by-magic --bulk-rename-by-magic / --touch-tree / --tree-summary-md --catalog-grep / --diff-headers / --audit-tree / --magic-fix --bulk-validate / --bulk-export-json / --bulk-import-json --diff-tree / --orphan-jsons --- CMakeLists.txt | 1 + tools/editor/cli_arg_required.cpp | 2 +- tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_help.cpp | 2 + tools/editor/cli_orphan_jsons.cpp | 142 ++++++++++++++++++++++++++++++ tools/editor/cli_orphan_jsons.hpp | 11 +++ 6 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 tools/editor/cli_orphan_jsons.cpp create mode 100644 tools/editor/cli_orphan_jsons.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f074e6ef..9640aba0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1480,6 +1480,7 @@ add_executable(wowee_editor tools/editor/cli_bulk_validate.cpp tools/editor/cli_bulk_json.cpp tools/editor/cli_diff_tree.cpp + tools/editor/cli_orphan_jsons.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 c0af76b5..367859ce 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", + "--diff-tree", "--orphan-jsons", "--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 3796985e..3937d05e 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -99,6 +99,7 @@ #include "cli_bulk_validate.hpp" #include "cli_bulk_json.hpp" #include "cli_diff_tree.hpp" +#include "cli_orphan_jsons.hpp" #include "cli_macros_catalog.hpp" #include "cli_char_features_catalog.hpp" #include "cli_pvp_catalog.hpp" @@ -267,6 +268,7 @@ constexpr DispatchFn kDispatchTable[] = { handleBulkValidate, handleBulkJson, handleDiffTree, + handleOrphanJsons, handleMacrosCatalog, handleCharFeaturesCatalog, handlePVPCatalog, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index e60cd0c0..cd9e767a 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1377,6 +1377,8 @@ void printUsage(const char* argv0) { std::printf(" Recursively import every .wXXX.json sidecar back to its binary .w* form via the per-format --import-X-json flag. Inverse of --bulk-export-json. Exit 1 if any failure\n"); std::printf(" --diff-tree [--json]\n"); 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(" --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_orphan_jsons.cpp b/tools/editor/cli_orphan_jsons.cpp new file mode 100644 index 00000000..c4b3c3ee --- /dev/null +++ b/tools/editor/cli_orphan_jsons.cpp @@ -0,0 +1,142 @@ +#include "cli_orphan_jsons.hpp" +#include "cli_arg_parse.hpp" +#include "cli_format_table.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +namespace fs = std::filesystem; + +struct Orphan { + fs::path jsonPath; // the sidecar with no binary + fs::path expectedBinary; // the binary it expected + const FormatMagicEntry* fmt = nullptr; +}; + +// Match an extension against the format table case- +// insensitively. Mirrors helpers in cli_audit_tree / +// cli_magic_fix. +const FormatMagicEntry* findFormatByExtension(const std::string& ext) { + if (ext.empty()) return nullptr; + for (const FormatMagicEntry* p = formatTableBegin(); + p != formatTableEnd(); ++p) { + const char* a = p->extension; + const char* b = ext.c_str(); + bool match = true; + while (*a && *b) { + char ca = *a; char cb = *b; + if (ca >= 'A' && ca <= 'Z') ca += 32; + if (cb >= 'A' && cb <= 'Z') cb += 32; + if (ca != cb) { match = false; break; } + ++a; ++b; + } + if (match && *a == 0 && *b == 0) return p; + } + return nullptr; +} + +int handleScan(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, + "orphan-jsons: not a directory: %s\n", dir.c_str()); + return 1; + } + std::vector orphans; + size_t totalSidecars = 0; + size_t pairedSidecars = 0; + for (const auto& entry : fs::recursive_directory_iterator(dir)) { + if (!entry.is_regular_file()) continue; + const fs::path& p = entry.path(); + // Only inspect .wXXX.json sidecars. Filename must + // end in ".json" and the ".wXXX" before it must + // match a known format extension. + std::string fname = p.filename().string(); + if (fname.size() < 6) continue; + if (fname.compare(fname.size() - 5, 5, ".json") != 0) continue; + std::string stem = fname.substr(0, fname.size() - 5); + size_t dot = stem.rfind('.'); + if (dot == std::string::npos) continue; + std::string ext = stem.substr(dot); + const FormatMagicEntry* fmt = findFormatByExtension(ext); + if (!fmt) continue; // .json that isn't ours + ++totalSidecars; + // Expected binary = same path with the ".json" + // suffix stripped (so foo.wsrg.json -> foo.wsrg). + fs::path expectedBin = p.parent_path() / stem; + std::error_code ec; + if (fs::exists(expectedBin, ec)) { + ++pairedSidecars; + continue; + } + Orphan o; + o.jsonPath = p; + o.expectedBinary = expectedBin; + o.fmt = fmt; + orphans.push_back(std::move(o)); + } + bool anyOrphans = !orphans.empty(); + if (jsonOut) { + nlohmann::json j; + j["dir"] = dir; + j["totalSidecars"] = totalSidecars; + j["paired"] = pairedSidecars; + j["orphans"] = orphans.size(); + j["allPaired"] = !anyOrphans; + nlohmann::json arr = nlohmann::json::array(); + for (const auto& o : orphans) { + arr.push_back({ + {"jsonPath", fs::relative(o.jsonPath, dir).string()}, + {"expectedBinary", fs::relative(o.expectedBinary, dir).string()}, + {"format", std::string(o.fmt->extension)}, + }); + } + j["orphanList"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return anyOrphans ? 1 : 0; + } + std::printf("orphan-jsons: %s\n", dir.c_str()); + std::printf(" total sidecars : %zu\n", totalSidecars); + std::printf(" paired : %zu\n", pairedSidecars); + std::printf(" orphans : %zu\n", orphans.size()); + if (!anyOrphans) { + std::printf(" every .wXXX.json sidecar has a matching binary\n"); + return 0; + } + std::printf("\n orphan sidecars (binary missing):\n"); + for (const auto& o : orphans) { + std::printf(" %s\n expected: %s [%s]\n", + fs::relative(o.jsonPath, dir).string().c_str(), + fs::relative(o.expectedBinary, dir).string().c_str(), + o.fmt->extension); + } + std::printf("\n to repair: re-run --bulk-import-json on the " + "directory, or remove the orphan sidecars manually\n"); + return 1; +} + +} // namespace + +bool handleOrphanJsons(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--orphan-jsons") == 0 && i + 1 < argc) { + outRc = handleScan(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_orphan_jsons.hpp b/tools/editor/cli_orphan_jsons.hpp new file mode 100644 index 00000000..ecb31b37 --- /dev/null +++ b/tools/editor/cli_orphan_jsons.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleOrphanJsons(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee