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.
This commit is contained in:
Kelsi 2026-05-09 19:44:52 -07:00
parent 77384f600e
commit d76fa93760
3 changed files with 115 additions and 0 deletions

View file

@ -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",

View file

@ -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 <file> [--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 <dir> [--dry-run] [--force]\n");
std::printf(" Apply --rename-by-magic recursively to every file in <dir>. Conflicts are skipped without --force; exits 1 if any rename failed\n");
std::printf(" --gen-animations <wani-base> [name]\n");
std::printf(" Emit .wani starter: 5 essential animations (Stand / Walk / Run / Death / AttackUnarmed) with fallback chains\n");
std::printf(" --gen-animations-combat <wani-base> [name]\n");

View file

@ -8,6 +8,7 @@
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
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<RenamePlan> 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<unsigned long long>(conflictCount),
force ? "" : " (skipped without --force)");
std::printf(" already-correct : %llu (skipped)\n",
static_cast<unsigned long long>(skippedAlreadyCorrect));
std::printf(" unrecognized : %llu (skipped)\n",
static_cast<unsigned long long>(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<unsigned long long>(renamed),
renamed == 1 ? "" : "s");
if (skippedConflict > 0) {
std::printf(" skipped (conflict): %llu\n",
static_cast<unsigned long long>(skippedConflict));
}
if (failed > 0) {
std::fprintf(stderr, " failed: %llu\n",
static_cast<unsigned long long>(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;
}