From ef38998a4e2cd63440be59850b9df9c53132b60c Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 7 May 2026 19:31:09 -0700 Subject: [PATCH] feat(editor): add --copy-project recursive project tree copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recursively copies an entire project tree (every zone subdir + manifests + content files). Refuses to overwrite an existing destination so a typo doesn't silently merge into the wrong project — user must delete the destination first if intentional. Reports zone count, total file count, and total byte count of what was copied. Useful for forking a project for experimentation, or for creating snapshot backups before risky bulk operations like --strip-data-tree or --remove-project-orphans. Verified: 2-zone gen-random-project source → copy-project → "zones: 2, files: 12, total bytes: 395989" reported correctly, existing destination correctly rejected. --- tools/editor/main.cpp | 51 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 44141de5..1654cce3 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -915,6 +915,8 @@ static void printUsage(const char* argv0) { std::printf(" One-line compact zone summary (tiles, biome, counts, audio status)\n"); std::printf(" --info-project-overview [--json]\n"); std::printf(" One-line summary per zone in a project (single-page health check)\n"); + std::printf(" --copy-project \n"); + std::printf(" Recursively copy a project tree (every zone subdir + manifests)\n"); std::printf(" --info-creatures

[--json]\n"); std::printf(" Print creatures.json summary (counts, behaviors) and exit\n"); std::printf(" --info-creatures-by-faction

[--json]\n"); @@ -1019,7 +1021,7 @@ int main(int argc, char* argv[]) { "--info-pack-tree", "--info-m2", "--info-wmo", "--info-adt", "--info-zone", "--info-zone-overview", "--info-project-overview", - "--info-wcp", "--list-wcp", + "--copy-project", "--info-wcp", "--list-wcp", "--list-creatures", "--list-objects", "--list-quests", "--list-quest-objectives", "--list-quest-rewards", "--info-creature", "--info-quest", "--info-object", @@ -3821,6 +3823,53 @@ int main(int argc, char* argv[]) { r.hasAudio ? "yes" : "no"); } return 0; + } else if (std::strcmp(argv[i], "--copy-project") == 0 && i + 2 < argc) { + // Recursively copy an entire project tree. Refuses to + // overwrite an existing destination so a typo doesn't + // silently merge into the wrong project. + std::string fromDir = argv[++i]; + std::string toDir = argv[++i]; + namespace fs = std::filesystem; + if (!fs::exists(fromDir) || !fs::is_directory(fromDir)) { + std::fprintf(stderr, + "copy-project: %s is not a directory\n", fromDir.c_str()); + return 1; + } + if (fs::exists(toDir)) { + std::fprintf(stderr, + "copy-project: destination %s already exists " + "(delete it first if intentional)\n", toDir.c_str()); + return 1; + } + std::error_code ec; + fs::copy(fromDir, toDir, + fs::copy_options::recursive | fs::copy_options::copy_symlinks, + ec); + if (ec) { + std::fprintf(stderr, + "copy-project: copy failed (%s)\n", ec.message().c_str()); + return 1; + } + // Count what was copied for the report. + int zoneCount = 0, fileCount = 0; + uint64_t totalBytes = 0; + for (const auto& entry : fs::directory_iterator(toDir, ec)) { + if (entry.is_directory() && + fs::exists(entry.path() / "zone.json")) zoneCount++; + } + for (const auto& e : fs::recursive_directory_iterator(toDir, ec)) { + if (e.is_regular_file()) { + fileCount++; + totalBytes += e.file_size(ec); + } + } + std::printf("Copied %s -> %s\n", fromDir.c_str(), toDir.c_str()); + std::printf(" zones : %d\n", zoneCount); + std::printf(" files : %d\n", fileCount); + std::printf(" total bytes : %llu (%.1f MB)\n", + static_cast(totalBytes), + totalBytes / (1024.0 * 1024.0)); + return 0; } else if (std::strcmp(argv[i], "--info-creatures") == 0 && i + 1 < argc) { std::string path = argv[++i]; bool jsonOut = (i + 1 < argc &&