From 73b4a6362f51fbcb29b3ae8a846ab3c7d76e0953 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 08:59:51 -0700 Subject: [PATCH] refactor(editor): extract --repair-zone / --repair-project into cli_repair.cpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the two manifest-drift fix handlers (--repair-zone, --repair-project) out of main.cpp into a new cli_repair.{hpp,cpp} module. Both auto-fix the common manifest-vs-disk inconsistencies that accumulate when zones are hand-edited or partially copied — adding orphan WHM tiles to the manifest, syncing the hasCreatures flag with the actual creatures.json content, and warning (not removing) for tiles in the manifest without backing files. Both honor --dry-run for safe previews. main.cpp shrinks by 155 lines (3,946 to 3,791). --- CMakeLists.txt | 1 + tools/editor/cli_repair.cpp | 202 ++++++++++++++++++++++++++++++++++++ tools/editor/cli_repair.hpp | 19 ++++ tools/editor/main.cpp | 163 +---------------------------- 4 files changed, 226 insertions(+), 159 deletions(-) create mode 100644 tools/editor/cli_repair.cpp create mode 100644 tools/editor/cli_repair.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 55a9487c..4e481045 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1348,6 +1348,7 @@ add_executable(wowee_editor tools/editor/cli_tiles.cpp tools/editor/cli_zone_mgmt.cpp tools/editor/cli_strip.cpp + tools/editor/cli_repair.cpp tools/editor/editor_app.cpp tools/editor/editor_camera.cpp tools/editor/editor_viewport.cpp diff --git a/tools/editor/cli_repair.cpp b/tools/editor/cli_repair.cpp new file mode 100644 index 00000000..015b5e5e --- /dev/null +++ b/tools/editor/cli_repair.cpp @@ -0,0 +1,202 @@ +#include "cli_repair.hpp" + +#include "zone_manifest.hpp" +#include "npc_spawner.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +int handleRepairZone(int& i, int argc, char** argv) { + // Auto-fix the common manifest-vs-disk drift issues that + // accumulate when a zone is hand-edited or partially copied: + // - WHM/WOT files exist on disk but tile not in manifest + // -> add to tiles + // - manifest hasCreatures=false but creatures.json exists + // and is non-empty -> set true + // - manifest hasCreatures=true but no creatures.json or + // empty -> clear false + // + // Tiles in manifest with NO disk files are NOT auto-removed + // (they may indicate work-in-progress); they're warned about + // so the user can decide. + // + // --dry-run flag previews changes without writing. + std::string zoneDir = argv[++i]; + bool dryRun = false; + if (i + 1 < argc && std::strcmp(argv[i + 1], "--dry-run") == 0) { + dryRun = true; i++; + } + namespace fs = std::filesystem; + std::string manifestPath = zoneDir + "/zone.json"; + if (!fs::exists(manifestPath)) { + std::fprintf(stderr, + "repair-zone: %s has no zone.json\n", zoneDir.c_str()); + return 1; + } + wowee::editor::ZoneManifest zm; + if (!zm.load(manifestPath)) { + std::fprintf(stderr, "repair-zone: parse failed\n"); + return 1; + } + int fixes = 0, warnings = 0; + // Pass 1: scan disk for WHM files matching mapName_X_Y.whm + // pattern. Match against manifest tiles. Anything on disk + // but missing from manifest gets queued for addition. + std::set> manifestTiles( + zm.tiles.begin(), zm.tiles.end()); + std::set> diskTiles; + std::error_code ec; + for (const auto& e : fs::directory_iterator(zoneDir, ec)) { + if (!e.is_regular_file()) continue; + std::string name = e.path().filename().string(); + if (e.path().extension() != ".whm") continue; + // Expect "_TX_TY.whm". Parse out the two + // integers between the last two underscores. + std::string stem = name.substr(0, name.size() - 4); + std::string prefix = zm.mapName + "_"; + if (stem.size() <= prefix.size() || + stem.substr(0, prefix.size()) != prefix) { + continue; // doesn't match map slug + } + std::string coords = stem.substr(prefix.size()); + auto under = coords.find('_'); + if (under == std::string::npos) continue; + try { + int tx = std::stoi(coords.substr(0, under)); + int ty = std::stoi(coords.substr(under + 1)); + diskTiles.insert({tx, ty}); + } catch (...) {} + } + // Tiles on disk but not in manifest -> add. + std::vector> toAdd; + for (const auto& d : diskTiles) { + if (manifestTiles.count(d) == 0) toAdd.push_back(d); + } + for (const auto& [tx, ty] : toAdd) { + std::printf(" %s tile (%d, %d) to manifest\n", + dryRun ? "would add" : "added", tx, ty); + if (!dryRun) zm.tiles.push_back({tx, ty}); + fixes++; + } + // Tiles in manifest but no .whm on disk -> warn (not auto-removed). + for (const auto& m : manifestTiles) { + if (diskTiles.count(m) == 0) { + std::printf(" WARN: tile (%d, %d) in manifest but no %s_%d_%d.whm on disk\n", + m.first, m.second, zm.mapName.c_str(), + m.first, m.second); + warnings++; + } + } + // hasCreatures flag sync. + bool creaturesPresent = false; + wowee::editor::NpcSpawner sp; + if (sp.loadFromFile(zoneDir + "/creatures.json") && + sp.spawnCount() > 0) { + creaturesPresent = true; + } + if (zm.hasCreatures != creaturesPresent) { + std::printf(" %s hasCreatures: %s -> %s\n", + dryRun ? "would set" : "set", + zm.hasCreatures ? "true" : "false", + creaturesPresent ? "true" : "false"); + if (!dryRun) zm.hasCreatures = creaturesPresent; + fixes++; + } + if (!dryRun && fixes > 0) { + if (!zm.save(manifestPath)) { + std::fprintf(stderr, + "repair-zone: failed to write %s\n", manifestPath.c_str()); + return 1; + } + } + std::printf("\nrepair-zone: %s%s\n", + zoneDir.c_str(), dryRun ? " (dry-run)" : ""); + std::printf(" fixes : %d\n", fixes); + std::printf(" warnings : %d (manual decision needed)\n", warnings); + if (dryRun && fixes > 0) { + std::printf(" re-run without --dry-run to apply\n"); + } + return 0; +} + +int handleRepairProject(int& i, int argc, char** argv) { + // Project-wide wrapper around --repair-zone. Spawns the + // binary per-zone so each zone's full repair report + // streams through, then aggregates a final tally. Honors + // --dry-run for safe previews. + std::string projectDir = argv[++i]; + bool dryRun = false; + if (i + 1 < argc && std::strcmp(argv[i + 1], "--dry-run") == 0) { + dryRun = true; i++; + } + namespace fs = std::filesystem; + if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) { + std::fprintf(stderr, + "repair-project: %s is not a directory\n", + projectDir.c_str()); + return 1; + } + std::vector zones; + for (const auto& entry : fs::directory_iterator(projectDir)) { + if (!entry.is_directory()) continue; + if (!fs::exists(entry.path() / "zone.json")) continue; + zones.push_back(entry.path().string()); + } + std::sort(zones.begin(), zones.end()); + std::string self = argv[0]; + int totalFailed = 0; + std::printf("repair-project: %s%s\n", + projectDir.c_str(), dryRun ? " (dry-run)" : ""); + std::printf(" zones : %zu\n", zones.size()); + for (const auto& zoneDir : zones) { + std::printf("\n--- %s ---\n", + fs::path(zoneDir).filename().string().c_str()); + // Flush so the section marker lands before the spawned + // child's stdout — std::system inherits FDs but each + // process has its own buffer. + std::fflush(stdout); + std::string cmd = "\"" + self + "\" --repair-zone \"" + + zoneDir + "\"" + (dryRun ? " --dry-run" : ""); + int rc = std::system(cmd.c_str()); + if (rc != 0) totalFailed++; + } + std::printf("\n--- summary ---\n"); + std::printf(" zones processed : %zu\n", zones.size()); + std::printf(" failures : %d\n", totalFailed); + if (dryRun) { + std::printf(" re-run without --dry-run to apply changes\n"); + } + return totalFailed == 0 ? 0 : 1; +} + + +} // namespace + +bool handleRepair(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--repair-zone") == 0 && i + 1 < argc) { + outRc = handleRepairZone(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--repair-project") == 0 && i + 1 < argc) { + outRc = handleRepairProject(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_repair.hpp b/tools/editor/cli_repair.hpp new file mode 100644 index 00000000..687db19f --- /dev/null +++ b/tools/editor/cli_repair.hpp @@ -0,0 +1,19 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +// Dispatch the repair-* manifest-drift fix handlers — auto-fix +// the common manifest-vs-disk inconsistencies that accumulate +// when zones are hand-edited or partially copied. Both honor +// --dry-run for safe previews. +// --repair-zone fix one zone (sync tiles, hasCreatures) +// --repair-project per-zone wrapper with aggregate tally +// +// Returns true if matched; outRc holds the exit code. +bool handleRepair(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 048ec765..41661cd6 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -49,6 +49,7 @@ #include "cli_tiles.hpp" #include "cli_zone_mgmt.hpp" #include "cli_strip.hpp" +#include "cli_repair.hpp" #include "content_pack.hpp" #include "npc_spawner.hpp" #include "object_placer.hpp" @@ -534,6 +535,9 @@ int main(int argc, char* argv[]) { if (wowee::editor::cli::handleStrip(i, argc, argv, outRc)) { return outRc; } + if (wowee::editor::cli::handleRepair(i, argc, argv, outRc)) { + return outRc; + } } if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) { dataPath = argv[++i]; @@ -1662,165 +1666,6 @@ int main(int argc, char* argv[]) { std::printf(" next : --add-texture-to-mesh %s\n", destPath.c_str()); return 0; - } else if (std::strcmp(argv[i], "--repair-zone") == 0 && i + 1 < argc) { - // Auto-fix the common manifest-vs-disk drift issues that - // accumulate when a zone is hand-edited or partially copied: - // - WHM/WOT files exist on disk but tile not in manifest - // -> add to tiles - // - manifest hasCreatures=false but creatures.json exists - // and is non-empty -> set true - // - manifest hasCreatures=true but no creatures.json or - // empty -> clear false - // - // Tiles in manifest with NO disk files are NOT auto-removed - // (they may indicate work-in-progress); they're warned about - // so the user can decide. - // - // --dry-run flag previews changes without writing. - std::string zoneDir = argv[++i]; - bool dryRun = false; - if (i + 1 < argc && std::strcmp(argv[i + 1], "--dry-run") == 0) { - dryRun = true; i++; - } - namespace fs = std::filesystem; - std::string manifestPath = zoneDir + "/zone.json"; - if (!fs::exists(manifestPath)) { - std::fprintf(stderr, - "repair-zone: %s has no zone.json\n", zoneDir.c_str()); - return 1; - } - wowee::editor::ZoneManifest zm; - if (!zm.load(manifestPath)) { - std::fprintf(stderr, "repair-zone: parse failed\n"); - return 1; - } - int fixes = 0, warnings = 0; - // Pass 1: scan disk for WHM files matching mapName_X_Y.whm - // pattern. Match against manifest tiles. Anything on disk - // but missing from manifest gets queued for addition. - std::set> manifestTiles( - zm.tiles.begin(), zm.tiles.end()); - std::set> diskTiles; - std::error_code ec; - for (const auto& e : fs::directory_iterator(zoneDir, ec)) { - if (!e.is_regular_file()) continue; - std::string name = e.path().filename().string(); - if (e.path().extension() != ".whm") continue; - // Expect "_TX_TY.whm". Parse out the two - // integers between the last two underscores. - std::string stem = name.substr(0, name.size() - 4); - std::string prefix = zm.mapName + "_"; - if (stem.size() <= prefix.size() || - stem.substr(0, prefix.size()) != prefix) { - continue; // doesn't match map slug - } - std::string coords = stem.substr(prefix.size()); - auto under = coords.find('_'); - if (under == std::string::npos) continue; - try { - int tx = std::stoi(coords.substr(0, under)); - int ty = std::stoi(coords.substr(under + 1)); - diskTiles.insert({tx, ty}); - } catch (...) {} - } - // Tiles on disk but not in manifest -> add. - std::vector> toAdd; - for (const auto& d : diskTiles) { - if (manifestTiles.count(d) == 0) toAdd.push_back(d); - } - for (const auto& [tx, ty] : toAdd) { - std::printf(" %s tile (%d, %d) to manifest\n", - dryRun ? "would add" : "added", tx, ty); - if (!dryRun) zm.tiles.push_back({tx, ty}); - fixes++; - } - // Tiles in manifest but no .whm on disk -> warn (not auto-removed). - for (const auto& m : manifestTiles) { - if (diskTiles.count(m) == 0) { - std::printf(" WARN: tile (%d, %d) in manifest but no %s_%d_%d.whm on disk\n", - m.first, m.second, zm.mapName.c_str(), - m.first, m.second); - warnings++; - } - } - // hasCreatures flag sync. - bool creaturesPresent = false; - wowee::editor::NpcSpawner sp; - if (sp.loadFromFile(zoneDir + "/creatures.json") && - sp.spawnCount() > 0) { - creaturesPresent = true; - } - if (zm.hasCreatures != creaturesPresent) { - std::printf(" %s hasCreatures: %s -> %s\n", - dryRun ? "would set" : "set", - zm.hasCreatures ? "true" : "false", - creaturesPresent ? "true" : "false"); - if (!dryRun) zm.hasCreatures = creaturesPresent; - fixes++; - } - if (!dryRun && fixes > 0) { - if (!zm.save(manifestPath)) { - std::fprintf(stderr, - "repair-zone: failed to write %s\n", manifestPath.c_str()); - return 1; - } - } - std::printf("\nrepair-zone: %s%s\n", - zoneDir.c_str(), dryRun ? " (dry-run)" : ""); - std::printf(" fixes : %d\n", fixes); - std::printf(" warnings : %d (manual decision needed)\n", warnings); - if (dryRun && fixes > 0) { - std::printf(" re-run without --dry-run to apply\n"); - } - return 0; - } else if (std::strcmp(argv[i], "--repair-project") == 0 && i + 1 < argc) { - // Project-wide wrapper around --repair-zone. Spawns the - // binary per-zone so each zone's full repair report - // streams through, then aggregates a final tally. Honors - // --dry-run for safe previews. - std::string projectDir = argv[++i]; - bool dryRun = false; - if (i + 1 < argc && std::strcmp(argv[i + 1], "--dry-run") == 0) { - dryRun = true; i++; - } - namespace fs = std::filesystem; - if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) { - std::fprintf(stderr, - "repair-project: %s is not a directory\n", - projectDir.c_str()); - return 1; - } - std::vector zones; - for (const auto& entry : fs::directory_iterator(projectDir)) { - if (!entry.is_directory()) continue; - if (!fs::exists(entry.path() / "zone.json")) continue; - zones.push_back(entry.path().string()); - } - std::sort(zones.begin(), zones.end()); - std::string self = argv[0]; - int totalFailed = 0; - std::printf("repair-project: %s%s\n", - projectDir.c_str(), dryRun ? " (dry-run)" : ""); - std::printf(" zones : %zu\n", zones.size()); - for (const auto& zoneDir : zones) { - std::printf("\n--- %s ---\n", - fs::path(zoneDir).filename().string().c_str()); - // Flush so the section marker lands before the spawned - // child's stdout — std::system inherits FDs but each - // process has its own buffer. - std::fflush(stdout); - std::string cmd = "\"" + self + "\" --repair-zone \"" + - zoneDir + "\"" + (dryRun ? " --dry-run" : ""); - int rc = std::system(cmd.c_str()); - if (rc != 0) totalFailed++; - } - std::printf("\n--- summary ---\n"); - std::printf(" zones processed : %zu\n", zones.size()); - std::printf(" failures : %d\n", totalFailed); - if (dryRun) { - std::printf(" re-run without --dry-run to apply changes\n"); - } - return totalFailed == 0 ? 0 : 1; } else if (std::strcmp(argv[i], "--gen-makefile") == 0 && i + 1 < argc) { // Generate a Makefile that rebuilds every derived output for // a zone. With this in place, designers can `make` to refresh