From 8d9690a57ab4bb2359e51f028404c2405f638b8d Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 19:26:10 -0700 Subject: [PATCH] feat(editor): add --remove-zone for safe zone deletion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Counterpart to --scaffold-zone / --copy-zone / --rename-zone — completes the zone-lifecycle CRUD. Defense-in-depth against accidental destruction: wowee_editor --remove-zone custom_zones/Doomed remove-zone: custom_zones/Doomed ('Doomed') would delete: 6 file(s), 174.9 KB re-run with --confirm to actually delete wowee_editor --remove-zone custom_zones/Doomed --confirm Removed custom_zones/Doomed ('Doomed') deleted: 7 filesystem entries, 174.9 KB freed Two-step safety: 1. Without --confirm: dry-run that lists what would be deleted (file count + total bytes + zone display name from manifest). 2. With --confirm: actually wipes the directory. Belt-and-suspenders refusal: even with --confirm, refuses to delete anything that doesn't have a zone.json at the top level. Catches typos like '--remove-zone .' that would otherwise nuke an entire project. Why not just 'rm -rf'? --remove-zone gives: - Per-zone display name in the confirmation - Byte-count audit before deletion - The non-zone-dir guard (rm doesn't know what a zone is) - Symmetric with the rest of the zone-lifecycle CLI Verified: dry-run lists 6 files / 175 KB; '. --confirm' correctly refused (no zone.json at top level); zone-dir --confirm wiped 7 fs entries with byte tally. --- tools/editor/main.cpp | 65 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 2af4cbab..8cec89c0 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -571,6 +571,8 @@ static void printUsage(const char* argv0) { 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(" --remove-zone [--confirm]\n"); + std::printf(" Delete a zone directory entirely (requires --confirm to actually delete)\n"); std::printf(" --clear-zone-content [--creatures] [--objects] [--quests] [--all]\n"); std::printf(" Wipe one or more content files (terrain + manifest preserved)\n"); std::printf(" --strip-zone [--dry-run]\n"); @@ -857,7 +859,8 @@ int main(int argc, char* argv[]) { "--remove-quest-objective", "--clone-quest", "--clone-creature", "--clone-object", "--remove-creature", "--remove-object", "--remove-quest", - "--copy-zone", "--rename-zone", "--clear-zone-content", "--strip-zone", + "--copy-zone", "--rename-zone", "--remove-zone", + "--clear-zone-content", "--strip-zone", "--repair-zone", "--gen-makefile", "--gen-project-makefile", "--build-woc", "--regen-collision", "--fix-zone", "--export-png", "--export-obj", "--import-obj", @@ -11786,6 +11789,66 @@ 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], "--remove-zone") == 0 && i + 1 < argc) { + // Delete a zone directory entirely. Requires --confirm to + // actually delete (defense against accidental destruction + // and against shell glob mishaps). Without --confirm, + // just lists what would be deleted. + std::string zoneDir = argv[++i]; + bool confirm = false; + if (i + 1 < argc && std::strcmp(argv[i + 1], "--confirm") == 0) { + confirm = true; i++; + } + namespace fs = std::filesystem; + if (!fs::exists(zoneDir)) { + std::fprintf(stderr, + "remove-zone: %s does not exist\n", zoneDir.c_str()); + return 1; + } + if (!fs::exists(zoneDir + "/zone.json")) { + // Belt-and-suspenders: refuse to wipe anything that doesn't + // look like a zone dir, even with --confirm. Catches typos + // like '--remove-zone .' that would nuke the whole project. + std::fprintf(stderr, + "remove-zone: %s has no zone.json — refusing to delete (not a zone dir)\n", + zoneDir.c_str()); + return 1; + } + // Read manifest for the user-facing name. + wowee::editor::ZoneManifest zm; + std::string zoneName = zoneDir; + if (zm.load(zoneDir + "/zone.json")) { + zoneName = zm.displayName.empty() ? zm.mapName : zm.displayName; + } + // Walk for what would be removed (counts + total bytes). + int fileCount = 0; + uint64_t totalBytes = 0; + std::error_code ec; + for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) { + if (!e.is_regular_file()) continue; + fileCount++; + totalBytes += e.file_size(ec); + } + if (!confirm) { + std::printf("remove-zone: %s ('%s')\n", + zoneDir.c_str(), zoneName.c_str()); + std::printf(" would delete: %d file(s), %.1f KB\n", + fileCount, totalBytes / 1024.0); + std::printf(" re-run with --confirm to actually delete\n"); + return 0; + } + // Confirmed — wipe it. + uintmax_t removed = fs::remove_all(zoneDir, ec); + if (ec) { + std::fprintf(stderr, + "remove-zone: failed to remove %s (%s)\n", + zoneDir.c_str(), ec.message().c_str()); + return 1; + } + std::printf("Removed %s ('%s')\n", zoneDir.c_str(), zoneName.c_str()); + std::printf(" deleted: %ju filesystem entries, %.1f KB freed\n", + static_cast(removed), totalBytes / 1024.0); + return 0; } else if (std::strcmp(argv[i], "--clear-zone-content") == 0 && i + 1 < argc) { // Wipe content files (creatures.json / objects.json / // quests.json) from a zone while keeping terrain + manifest