From d76fa937605ef07956f23c1234c1f2b691b931e6 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 19:44:52 -0700 Subject: [PATCH] feat(editor): add --bulk-rename-by-magic recursive recovery flag Walks a directory tree, identifies every file by 4-byte magic, and renames each one to use the correct .w* extension. Counterpart to single-file --rename-by-magic for the case where a whole asset bundle came in with mangled extensions (extracted from a tarball that lost suffixes, downloaded with CDN-rewritten names, dumped from a database BLOB column). Same safety semantics as the single-file flag: conflicts are skipped without --force, --dry-run prints the planned moves without touching the filesystem. Reports per-file PASS/FAIL/ CONFLICT and a final summary (renamed / skipped-conflict / already-correct / unrecognized counts). Exits 1 if any rename failed (e.g. permission errors) so it can be wired into CI as a gate. --- tools/editor/cli_arg_required.cpp | 1 + tools/editor/cli_help.cpp | 2 + tools/editor/cli_rename_magic.cpp | 112 ++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+) diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 6446f487..8c3a54e2 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -135,6 +135,7 @@ const char* const kArgRequired[] = { "--info-wliq", "--validate-wliq", "--export-wliq-json", "--import-wliq-json", "--info-magic", "--summary-dir", "--rename-by-magic", + "--bulk-rename-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_help.cpp b/tools/editor/cli_help.cpp index 62021870..4bfd5485 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1355,6 +1355,8 @@ void printUsage(const char* argv0) { std::printf(" Recursively walk a directory; report per-format file count, total entries, and bytes for every Wowee open format found\n"); std::printf(" --rename-by-magic [--dry-run] [--force]\n"); std::printf(" Recover the correct .w* extension on a file by reading its 4-byte magic. --dry-run prints the planned move; --force overwrites\n"); + std::printf(" --bulk-rename-by-magic [--dry-run] [--force]\n"); + std::printf(" Apply --rename-by-magic recursively to every file in . Conflicts are skipped without --force; exits 1 if any rename failed\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_rename_magic.cpp b/tools/editor/cli_rename_magic.cpp index 0a8e03a4..5587d3da 100644 --- a/tools/editor/cli_rename_magic.cpp +++ b/tools/editor/cli_rename_magic.cpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace wowee { namespace editor { @@ -24,6 +25,113 @@ bool readMagic(const fs::path& path, char magic[4]) { return is.gcount() == 4; } +struct RenamePlan { + fs::path src; + fs::path dst; + const FormatMagicEntry* fmt; + bool conflict; // dst already exists +}; + +int handleBulkRename(int& i, int argc, char** argv) { + std::string dir = argv[++i]; + bool dryRun = false; + bool force = false; + while (i + 1 < argc) { + std::string a = argv[i + 1]; + if (a == "--dry-run") { dryRun = true; ++i; } + else if (a == "--force") { force = true; ++i; } + else break; + } + if (!fs::exists(dir) || !fs::is_directory(dir)) { + std::fprintf(stderr, + "bulk-rename-by-magic: not a directory: %s\n", dir.c_str()); + return 1; + } + std::vector plans; + uint64_t skippedAlreadyCorrect = 0; + uint64_t skippedUnknown = 0; + for (const auto& entry : fs::recursive_directory_iterator(dir)) { + if (!entry.is_regular_file()) continue; + char magic[4]; + if (!readMagic(entry.path(), magic)) { + ++skippedUnknown; + continue; + } + const FormatMagicEntry* fmt = findFormatByMagic(magic); + if (!fmt) { + ++skippedUnknown; + continue; + } + fs::path src = entry.path(); + fs::path dst = src; + dst.replace_extension(fmt->extension); + if (src == dst) { + ++skippedAlreadyCorrect; + continue; + } + RenamePlan p; + p.src = src; p.dst = dst; p.fmt = fmt; + p.conflict = fs::exists(dst); + plans.push_back(p); + } + uint64_t conflictCount = 0; + for (const auto& p : plans) if (p.conflict) ++conflictCount; + std::printf("bulk-rename-by-magic: %s%s\n", + dir.c_str(), dryRun ? " (dry-run)" : ""); + std::printf(" candidates : %zu\n", plans.size()); + std::printf(" conflicts : %llu%s\n", + static_cast(conflictCount), + force ? "" : " (skipped without --force)"); + std::printf(" already-correct : %llu (skipped)\n", + static_cast(skippedAlreadyCorrect)); + std::printf(" unrecognized : %llu (skipped)\n", + static_cast(skippedUnknown)); + if (plans.empty()) return 0; + uint64_t renamed = 0; + uint64_t skippedConflict = 0; + uint64_t failed = 0; + for (const auto& p : plans) { + if (p.conflict && !force) { + ++skippedConflict; + std::printf(" CONFLICT: %s -> %s\n", + p.src.string().c_str(), p.dst.string().c_str()); + continue; + } + if (dryRun) { + std::printf(" WOULD: %s -> %s\n", + p.src.string().c_str(), p.dst.string().c_str()); + ++renamed; + continue; + } + std::error_code ec; + fs::rename(p.src, p.dst, ec); + if (ec) { + std::fprintf(stderr, + " FAIL : %s -> %s (%s)\n", + p.src.string().c_str(), p.dst.string().c_str(), + ec.message().c_str()); + ++failed; + continue; + } + std::printf(" OK : %s -> %s\n", + p.src.string().c_str(), p.dst.string().c_str()); + ++renamed; + } + std::printf(" %s%llu file%s\n", + dryRun ? "would rename: " : "renamed: ", + static_cast(renamed), + renamed == 1 ? "" : "s"); + if (skippedConflict > 0) { + std::printf(" skipped (conflict): %llu\n", + static_cast(skippedConflict)); + } + if (failed > 0) { + std::fprintf(stderr, " failed: %llu\n", + static_cast(failed)); + } + return failed > 0 ? 1 : 0; +} + int handleRename(int& i, int argc, char** argv) { std::string filePath = argv[++i]; bool dryRun = false; @@ -93,6 +201,10 @@ bool handleRenameMagic(int& i, int argc, char** argv, int& outRc) { if (std::strcmp(argv[i], "--rename-by-magic") == 0 && i + 1 < argc) { outRc = handleRename(i, argc, argv); return true; } + if (std::strcmp(argv[i], "--bulk-rename-by-magic") == 0 && + i + 1 < argc) { + outRc = handleBulkRename(i, argc, argv); return true; + } return false; }