From 0f05759027468fcb5e018515886f8363804f4fef Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 12:24:36 -0700 Subject: [PATCH] feat(editor): add --rename-zone for in-place zone renames --copy-zone duplicates and renames; --rename-zone does it in place without doubling disk usage. Useful when fixing typos or rebranding a zone without touching its data: wowee_editor --rename-zone custom_zones/Old 'Brand New Name' What changes: - zone.json mapName: Old -> Brand_New_Name - zone.json displayName: 'Old' -> 'Brand New Name' - zone.json files block (adt_NN_NN, wdt): regenerated from new slug - slug-prefixed files: Old_28_30.whm -> Brand_New_Name_28_30.whm - the directory itself: custom_zones/Old/ -> custom_zones/Brand_New_Name/ Order of operations matters for crash safety: slug-prefixed files get renamed first (atomic per-file via fs::rename), then the manifest is rewritten in the still-named source dir, then the directory is moved last. If the dir-move fails we surface the manifest-already-updated state so the user can recover. Refuses to run if target dir already exists (avoids silent merge), or if both slug AND displayName already match the target (no-op). Verified end-to-end: scaffolded 'Old' with 1 creature, renamed to 'Brand New Name'. Result: dir renamed, .whm/.wot files renamed, zone.json fully updated, creatures.json preserved with 1 entry. --- tools/editor/main.cpp | 108 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 1 deletion(-) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 0fc7a9c7..c67b47f1 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -425,6 +425,8 @@ static void printUsage(const char* argv0) { std::printf(" Remove quest at given 0-based index from /quests.json\n"); std::printf(" --copy-zone \n"); std::printf(" Duplicate a zone to custom_zones// with renamed slug-prefixed files\n"); + std::printf(" --rename-zone \n"); + std::printf(" In-place rename (zone.json + slug-prefixed files + dir); no copy\n"); std::printf(" --build-woc Generate a WOC collision mesh from WHM/WOT and exit\n"); std::printf(" --regen-collision Rebuild every WOC under a zone dir and exit\n"); std::printf(" --fix-zone Re-parse + re-save zone JSONs to apply latest scrubs/caps and exit\n"); @@ -517,7 +519,7 @@ int main(int argc, char* argv[]) { "--scaffold-zone", "--add-creature", "--add-object", "--add-quest", "--add-quest-objective", "--add-quest-reward-item", "--set-quest-reward", "--remove-creature", "--remove-object", "--remove-quest", - "--copy-zone", + "--copy-zone", "--rename-zone", "--build-woc", "--regen-collision", "--fix-zone", "--export-png", "--export-obj", "--import-obj", "--export-wob-obj", "--import-wob-obj", @@ -579,6 +581,11 @@ int main(int argc, char* argv[]) { "--copy-zone requires \n"); return 1; } + if (std::strcmp(argv[i], "--rename-zone") == 0 && i + 2 >= argc) { + std::fprintf(stderr, + "--rename-zone requires \n"); + return 1; + } for (const char* opt : {"--remove-creature", "--remove-object", "--remove-quest"}) { if (std::strcmp(argv[i], opt) == 0 && i + 2 >= argc) { @@ -3596,6 +3603,105 @@ int main(int argc, char* argv[]) { std::printf(" mapName : %s -> %s\n", oldSlug.c_str(), newSlug.c_str()); std::printf(" renamed : %d slug-prefixed file(s)\n", renamed); return 0; + } else if (std::strcmp(argv[i], "--rename-zone") == 0 && i + 2 < argc) { + // In-place rename — like --copy-zone but no copy. Useful when + // the user wants to fix a typo or change a name without + // doubling disk usage. Renames the directory itself too + // (Old/ -> New/ under the same parent), so paths shift. + std::string srcDir = argv[++i]; + std::string rawName = argv[++i]; + namespace fs = std::filesystem; + if (!fs::exists(srcDir) || !fs::is_directory(srcDir)) { + std::fprintf(stderr, "rename-zone: source dir not found: %s\n", + srcDir.c_str()); + return 1; + } + if (!fs::exists(srcDir + "/zone.json")) { + std::fprintf(stderr, "rename-zone: %s has no zone.json — not a zone dir\n", + srcDir.c_str()); + return 1; + } + std::string newSlug; + for (char c : rawName) { + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '_' || c == '-') { + newSlug += c; + } else if (c == ' ') { + newSlug += '_'; + } + } + if (newSlug.empty()) { + std::fprintf(stderr, "rename-zone: name '%s' has no valid characters\n", + rawName.c_str()); + return 1; + } + wowee::editor::ZoneManifest zm; + if (!zm.load(srcDir + "/zone.json")) { + std::fprintf(stderr, "rename-zone: failed to parse %s/zone.json\n", + srcDir.c_str()); + return 1; + } + std::string oldSlug = zm.mapName; + if (oldSlug == newSlug && rawName == zm.displayName) { + std::fprintf(stderr, + "rename-zone: nothing to do (slug=%s, displayName=%s already match)\n", + oldSlug.c_str(), rawName.c_str()); + return 1; + } + // Compute target directory: same parent, new slug name. If the + // current directory name already matches the new slug, skip + // the dir rename (only manifest + slug-prefixed files change). + fs::path srcPath = fs::absolute(srcDir); + fs::path parent = srcPath.parent_path(); + fs::path dstPath = parent / newSlug; + bool needDirRename = (srcPath.filename() != newSlug); + if (needDirRename && fs::exists(dstPath)) { + std::fprintf(stderr, "rename-zone: target dir already exists: %s\n", + dstPath.string().c_str()); + return 1; + } + // Rename slug-prefixed files inside the source dir BEFORE + // moving the directory — fewer paths to fix up if anything + // fails midway. fs::rename is atomic per-call. + std::error_code ec; + int renamed = 0; + for (const auto& entry : fs::recursive_directory_iterator(srcDir)) { + if (!entry.is_regular_file()) continue; + std::string fname = entry.path().filename().string(); + bool match = (oldSlug != newSlug && + fname.size() > oldSlug.size() + 1 && + fname.compare(0, oldSlug.size(), oldSlug) == 0 && + (fname[oldSlug.size()] == '_' || + fname[oldSlug.size()] == '.')); + if (!match) continue; + std::string newName = newSlug + fname.substr(oldSlug.size()); + fs::rename(entry.path(), entry.path().parent_path() / newName, ec); + if (!ec) renamed++; + } + // Update manifest and save BEFORE the dir rename so the file + // exists at the path we're saving to. + zm.mapName = newSlug; + zm.displayName = rawName; + if (!zm.save(srcDir + "/zone.json")) { + std::fprintf(stderr, "rename-zone: failed to write zone.json\n"); + return 1; + } + // Now move the directory itself. + std::string finalDir = srcDir; + if (needDirRename) { + fs::rename(srcPath, dstPath, ec); + if (ec) { + std::fprintf(stderr, + "rename-zone: dir rename failed (%s); manifest already updated\n", + ec.message().c_str()); + return 1; + } + finalDir = dstPath.string(); + } + std::printf("Renamed %s -> %s\n", srcDir.c_str(), finalDir.c_str()); + std::printf(" mapName : %s -> %s\n", oldSlug.c_str(), newSlug.c_str()); + std::printf(" renamed : %d slug-prefixed file(s)\n", renamed); + return 0; } else if (std::strcmp(argv[i], "--pack-wcp") == 0 && i + 1 < argc) { // Pack a zone directory into a .wcp archive. // Usage: --pack-wcp [destPath]