diff --git a/CMakeLists.txt b/CMakeLists.txt
index 08a03173..4902c595 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1420,6 +1420,7 @@ add_executable(wowee_editor
tools/editor/cli_spell_visuals_catalog.cpp
tools/editor/cli_format_table.cpp
tools/editor/cli_summary_dir.cpp
+ tools/editor/cli_rename_magic.cpp
tools/editor/cli_quest_objective.cpp
tools/editor/cli_quest_reward.cpp
tools/editor/cli_clone.cpp
diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp
index 416b7400..6197446f 100644
--- a/tools/editor/cli_arg_required.cpp
+++ b/tools/editor/cli_arg_required.cpp
@@ -134,7 +134,7 @@ const char* const kArgRequired[] = {
"--gen-liquids", "--gen-liquids-magical", "--gen-liquids-hazardous",
"--info-wliq", "--validate-wliq",
"--export-wliq-json", "--import-wliq-json",
- "--info-magic", "--summary-dir",
+ "--info-magic", "--summary-dir", "--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_dispatch.cpp b/tools/editor/cli_dispatch.cpp
index 8acf429e..09e2c7b5 100644
--- a/tools/editor/cli_dispatch.cpp
+++ b/tools/editor/cli_dispatch.cpp
@@ -78,6 +78,7 @@
#include "cli_animations_catalog.hpp"
#include "cli_spell_visuals_catalog.hpp"
#include "cli_summary_dir.hpp"
+#include "cli_rename_magic.hpp"
#include "cli_quest_objective.hpp"
#include "cli_quest_reward.hpp"
#include "cli_clone.hpp"
@@ -197,6 +198,7 @@ constexpr DispatchFn kDispatchTable[] = {
handleAnimationsCatalog,
handleSpellVisualsCatalog,
handleSummaryDir,
+ handleRenameMagic,
handleQuestObjective,
handleQuestReward,
handleClone,
diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp
index d94988e2..f103945e 100644
--- a/tools/editor/cli_help.cpp
+++ b/tools/editor/cli_help.cpp
@@ -1353,6 +1353,8 @@ void printUsage(const char* argv0) {
std::printf(" Auto-detect any .w* file by 4-byte magic; report format / version / catalog name / entry count + suggest --info-* flag\n");
std::printf(" --summary-dir
[--json]\n");
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(" --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
new file mode 100644
index 00000000..0a8e03a4
--- /dev/null
+++ b/tools/editor/cli_rename_magic.cpp
@@ -0,0 +1,101 @@
+#include "cli_rename_magic.hpp"
+#include "cli_arg_parse.hpp"
+#include "cli_format_table.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace wowee {
+namespace editor {
+namespace cli {
+
+namespace {
+
+namespace fs = std::filesystem;
+
+bool readMagic(const fs::path& path, char magic[4]) {
+ std::ifstream is(path, std::ios::binary);
+ if (!is) return false;
+ is.read(magic, 4);
+ return is.gcount() == 4;
+}
+
+int handleRename(int& i, int argc, char** argv) {
+ std::string filePath = 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;
+ }
+ fs::path src = filePath;
+ if (!fs::exists(src) || !fs::is_regular_file(src)) {
+ std::fprintf(stderr,
+ "rename-by-magic: not a file: %s\n", filePath.c_str());
+ return 1;
+ }
+ char magic[4];
+ if (!readMagic(src, magic)) {
+ std::fprintf(stderr,
+ "rename-by-magic: cannot read 4-byte magic: %s\n",
+ filePath.c_str());
+ return 1;
+ }
+ const FormatMagicEntry* fmt = findFormatByMagic(magic);
+ if (!fmt) {
+ char magicStr[5] = {magic[0], magic[1], magic[2], magic[3], 0};
+ std::fprintf(stderr,
+ "rename-by-magic: unrecognized magic '%s' in %s\n",
+ magicStr, filePath.c_str());
+ return 1;
+ }
+ fs::path dst = src;
+ dst.replace_extension(fmt->extension);
+ if (src == dst) {
+ std::printf("rename-by-magic: %s already has correct "
+ "extension (%s) — no change\n",
+ filePath.c_str(), fmt->extension);
+ return 0;
+ }
+ if (fs::exists(dst) && !force) {
+ std::fprintf(stderr,
+ "rename-by-magic: target %s already exists "
+ "(pass --force to overwrite)\n", dst.string().c_str());
+ return 1;
+ }
+ if (dryRun) {
+ std::printf("rename-by-magic (dry-run): %s -> %s\n",
+ filePath.c_str(), dst.string().c_str());
+ return 0;
+ }
+ std::error_code ec;
+ fs::rename(src, dst, ec);
+ if (ec) {
+ std::fprintf(stderr,
+ "rename-by-magic: rename failed: %s\n",
+ ec.message().c_str());
+ return 1;
+ }
+ std::printf("rename-by-magic: %s -> %s\n",
+ filePath.c_str(), dst.string().c_str());
+ return 0;
+}
+
+} // namespace
+
+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;
+ }
+ return false;
+}
+
+} // namespace cli
+} // namespace editor
+} // namespace wowee
diff --git a/tools/editor/cli_rename_magic.hpp b/tools/editor/cli_rename_magic.hpp
new file mode 100644
index 00000000..fb3119c8
--- /dev/null
+++ b/tools/editor/cli_rename_magic.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+namespace wowee {
+namespace editor {
+namespace cli {
+
+bool handleRenameMagic(int& i, int argc, char** argv, int& outRc);
+
+} // namespace cli
+} // namespace editor
+} // namespace wowee