diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 5ce30f78..d6cff958 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -475,6 +475,8 @@ static void printUsage(const char* argv0) { std::printf(" Wipe one or more content files (terrain + manifest preserved)\n"); std::printf(" --strip-zone [--dry-run]\n"); std::printf(" Remove derived outputs (.glb/.obj/.stl/.html/.dot/.csv/ZONE.md/DEPS.md)\n"); + std::printf(" --repair-zone [--dry-run]\n"); + std::printf(" Auto-fix manifest/disk drift (missing tiles in manifest, hasCreatures flag)\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"); @@ -678,6 +680,7 @@ int main(int argc, char* argv[]) { "--clone-object", "--remove-creature", "--remove-object", "--remove-quest", "--copy-zone", "--rename-zone", "--clear-zone-content", "--strip-zone", + "--repair-zone", "--build-woc", "--regen-collision", "--fix-zone", "--export-png", "--export-obj", "--import-obj", "--export-wob-obj", "--import-wob-obj", @@ -9149,6 +9152,117 @@ int main(int argc, char* argv[]) { std::printf(" freed : %.1f KB\n", bytesFreed / 1024.0); } 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], "--pack-wcp") == 0 && i + 1 < argc) { // Pack a zone directory into a .wcp archive. // Usage: --pack-wcp [destPath]