diff --git a/CMakeLists.txt b/CMakeLists.txt index e649da92..316f649a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1338,6 +1338,7 @@ add_executable(wowee_editor tools/editor/cli_world_info.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp + tools/editor/cli_clone.cpp tools/editor/editor_app.cpp tools/editor/editor_camera.cpp tools/editor/editor_viewport.cpp diff --git a/tools/editor/cli_clone.cpp b/tools/editor/cli_clone.cpp new file mode 100644 index 00000000..5eca06f1 --- /dev/null +++ b/tools/editor/cli_clone.cpp @@ -0,0 +1,230 @@ +#include "cli_clone.hpp" + +#include "quest_editor.hpp" +#include "npc_spawner.hpp" +#include "object_placer.hpp" + +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +int handleCloneQuest(int& i, int argc, char** argv) { + // Duplicate a quest. Useful for templating: create a base + // quest with objectives + rewards once, then clone N times + // for variants ('Slay Wolves', 'Slay Bears' with the same + // shape). Optional newTitle replaces the cloned copy's title; + // omit to get ' (copy)'. + std::string zoneDir = argv[++i]; + std::string idxStr = argv[++i]; + std::string newTitle; + if (i + 1 < argc && argv[i + 1][0] != '-') { + newTitle = argv[++i]; + } + std::string path = zoneDir + "/quests.json"; + if (!std::filesystem::exists(path)) { + std::fprintf(stderr, "clone-quest: %s not found\n", path.c_str()); + return 1; + } + int qIdx; + try { qIdx = std::stoi(idxStr); } + catch (...) { + std::fprintf(stderr, "clone-quest: bad questIdx '%s'\n", idxStr.c_str()); + return 1; + } + wowee::editor::QuestEditor qe; + if (!qe.loadFromFile(path)) { + std::fprintf(stderr, "clone-quest: failed to load %s\n", path.c_str()); + return 1; + } + if (qIdx < 0 || qIdx >= static_cast(qe.questCount())) { + std::fprintf(stderr, + "clone-quest: questIdx %d out of range [0, %zu)\n", + qIdx, qe.questCount()); + return 1; + } + // Deep-copy by value via vector iteration; .objectives and + // .reward are STL containers so the copy is automatic. + wowee::editor::Quest clone = qe.getQuests()[qIdx]; + // Reset id so the editor's auto-id sequence assigns a fresh + // one — addQuest does this internally if id==0. + clone.id = 0; + // Reset chain link too — copying a chained quest with the + // same nextQuestId would corrupt the chain semantics. + clone.nextQuestId = 0; + clone.title = newTitle.empty() + ? (clone.title + " (copy)") + : newTitle; + qe.addQuest(clone); + if (!qe.saveToFile(path)) { + std::fprintf(stderr, "clone-quest: failed to write %s\n", path.c_str()); + return 1; + } + std::printf("Cloned quest %d -> '%s' (now %zu total)\n", + qIdx, clone.title.c_str(), qe.questCount()); + std::printf(" carried %zu objective(s), %zu item reward(s), xp=%u\n", + clone.objectives.size(), + clone.reward.itemRewards.size(), + clone.reward.xp); + return 0; +} + +int handleCloneCreature(int& i, int argc, char** argv) { + // Duplicate a creature spawn. Common workflow: design one + // 'patrol guard' archetype, then clone it across spawn points + // around a town. Preserves stats, faction, behavior, equipment; + // resets id and offsets position by 5 yards by default so the + // copy doesn't z-fight with the original. + std::string zoneDir = argv[++i]; + std::string idxStr = argv[++i]; + std::string newName; + float dx = 5.0f, dy = 0.0f, dz = 0.0f; + if (i + 1 < argc && argv[i + 1][0] != '-') { + newName = argv[++i]; + } + // Optional 3-axis offset after newName. + if (i + 3 < argc && argv[i + 1][0] != '-') { + try { + dx = std::stof(argv[++i]); + dy = std::stof(argv[++i]); + dz = std::stof(argv[++i]); + } catch (...) { + std::fprintf(stderr, "clone-creature: bad offset coordinate\n"); + return 1; + } + } + std::string path = zoneDir + "/creatures.json"; + if (!std::filesystem::exists(path)) { + std::fprintf(stderr, "clone-creature: %s not found\n", path.c_str()); + return 1; + } + int idx; + try { idx = std::stoi(idxStr); } + catch (...) { + std::fprintf(stderr, "clone-creature: bad idx '%s'\n", idxStr.c_str()); + return 1; + } + wowee::editor::NpcSpawner sp; + if (!sp.loadFromFile(path)) { + std::fprintf(stderr, "clone-creature: failed to load %s\n", path.c_str()); + return 1; + } + if (idx < 0 || idx >= static_cast(sp.spawnCount())) { + std::fprintf(stderr, + "clone-creature: idx %d out of range [0, %zu)\n", + idx, sp.spawnCount()); + return 1; + } + // Deep-copy by value; CreatureSpawn is POD-ish (vectors for + // patrol points copy automatically). + wowee::editor::CreatureSpawn clone = sp.getSpawns()[idx]; + clone.id = 0; // addCreature auto-assigns a fresh id + clone.name = newName.empty() + ? (clone.name + " (copy)") + : newName; + clone.position.x += dx; + clone.position.y += dy; + clone.position.z += dz; + // Patrol path is intentionally NOT offset — patrol points are + // typically authored as world-space waypoints, not relative to + // the spawn. Designers re-author the path if needed. + sp.getSpawns().push_back(clone); + if (!sp.saveToFile(path)) { + std::fprintf(stderr, "clone-creature: failed to write %s\n", path.c_str()); + return 1; + } + std::printf("Cloned creature %d -> '%s' at (%.1f, %.1f, %.1f) (now %zu total)\n", + idx, clone.name.c_str(), + clone.position.x, clone.position.y, clone.position.z, + sp.spawnCount()); + return 0; +} + +int handleCloneObject(int& i, int argc, char** argv) { + // Symmetric to --clone-creature/--clone-quest. Common + // workflow: place one tree/lamp/barrel just right, then + // clone N copies along a path or around a square. Default + // 5-yard X offset prevents z-fighting; rotation/scale are + // preserved so a tilted object stays tilted. + std::string zoneDir = argv[++i]; + std::string idxStr = argv[++i]; + float dx = 5.0f, dy = 0.0f, dz = 0.0f; + if (i + 3 < argc && argv[i + 1][0] != '-') { + try { + dx = std::stof(argv[++i]); + dy = std::stof(argv[++i]); + dz = std::stof(argv[++i]); + } catch (...) { + std::fprintf(stderr, "clone-object: bad offset\n"); + return 1; + } + } + std::string path = zoneDir + "/objects.json"; + if (!std::filesystem::exists(path)) { + std::fprintf(stderr, "clone-object: %s not found\n", path.c_str()); + return 1; + } + int idx; + try { idx = std::stoi(idxStr); } + catch (...) { + std::fprintf(stderr, "clone-object: bad idx '%s'\n", idxStr.c_str()); + return 1; + } + wowee::editor::ObjectPlacer placer; + if (!placer.loadFromFile(path)) { + std::fprintf(stderr, "clone-object: failed to load %s\n", path.c_str()); + return 1; + } + auto& objs = placer.getObjects(); + if (idx < 0 || idx >= static_cast(objs.size())) { + std::fprintf(stderr, + "clone-object: idx %d out of range [0, %zu)\n", + idx, objs.size()); + return 1; + } + // Deep-copy by value. uniqueId is reset so the new object + // doesn't collide with the source's identifier in any + // downstream system that dedups by it. + wowee::editor::PlacedObject clone = objs[idx]; + clone.uniqueId = 0; + clone.selected = false; + clone.position.x += dx; + clone.position.y += dy; + clone.position.z += dz; + objs.push_back(clone); + if (!placer.saveToFile(path)) { + std::fprintf(stderr, "clone-object: failed to write %s\n", path.c_str()); + return 1; + } + std::printf("Cloned object %d -> '%s' at (%.1f, %.1f, %.1f) (now %zu total)\n", + idx, clone.path.c_str(), + clone.position.x, clone.position.y, clone.position.z, + objs.size()); + return 0; +} + +} // namespace + +bool handleClone(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--clone-quest") == 0 && i + 2 < argc) { + outRc = handleCloneQuest(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--clone-creature") == 0 && i + 2 < argc) { + outRc = handleCloneCreature(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--clone-object") == 0 && i + 2 < argc) { + outRc = handleCloneObject(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_clone.hpp b/tools/editor/cli_clone.hpp new file mode 100644 index 00000000..bc0f6204 --- /dev/null +++ b/tools/editor/cli_clone.hpp @@ -0,0 +1,20 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +// Dispatch the clone-* duplicate handlers — deep-copy a quest / +// creature spawn / object placement by index, optionally +// renaming and offsetting the new copy. Useful for templating +// patterns: design once, clone N times. +// --clone-quest duplicate a quest with all objectives + rewards +// --clone-creature duplicate a spawn (default 5-yard X offset) +// --clone-object duplicate an object placement (5-yard X offset) +// +// Returns true if matched; outRc holds the exit code. +bool handleClone(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 0a80aaad..54a5dc48 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -39,6 +39,7 @@ #include "cli_world_info.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" +#include "cli_clone.hpp" #include "content_pack.hpp" #include "npc_spawner.hpp" #include "object_placer.hpp" @@ -490,6 +491,9 @@ int main(int argc, char* argv[]) { if (wowee::editor::cli::handleQuestReward(i, argc, argv, outRc)) { return outRc; } + if (wowee::editor::cli::handleClone(i, argc, argv, outRc)) { + return outRc; + } } if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) { dataPath = argv[++i]; @@ -1418,194 +1422,6 @@ int main(int argc, char* argv[]) { outPath.c_str(), col.triangles.size(), col.walkableCount(), col.steepCount()); return 0; - } else if (std::strcmp(argv[i], "--clone-quest") == 0 && i + 2 < argc) { - // Duplicate a quest. Useful for templating: create a base - // quest with objectives + rewards once, then clone N times - // for variants ('Slay Wolves', 'Slay Bears' with the same - // shape). Optional newTitle replaces the cloned copy's title; - // omit to get ' (copy)'. - std::string zoneDir = argv[++i]; - std::string idxStr = argv[++i]; - std::string newTitle; - if (i + 1 < argc && argv[i + 1][0] != '-') { - newTitle = argv[++i]; - } - std::string path = zoneDir + "/quests.json"; - if (!std::filesystem::exists(path)) { - std::fprintf(stderr, "clone-quest: %s not found\n", path.c_str()); - return 1; - } - int qIdx; - try { qIdx = std::stoi(idxStr); } - catch (...) { - std::fprintf(stderr, "clone-quest: bad questIdx '%s'\n", idxStr.c_str()); - return 1; - } - wowee::editor::QuestEditor qe; - if (!qe.loadFromFile(path)) { - std::fprintf(stderr, "clone-quest: failed to load %s\n", path.c_str()); - return 1; - } - if (qIdx < 0 || qIdx >= static_cast(qe.questCount())) { - std::fprintf(stderr, - "clone-quest: questIdx %d out of range [0, %zu)\n", - qIdx, qe.questCount()); - return 1; - } - // Deep-copy by value via vector iteration; .objectives and - // .reward are STL containers so the copy is automatic. - wowee::editor::Quest clone = qe.getQuests()[qIdx]; - // Reset id so the editor's auto-id sequence assigns a fresh - // one — addQuest does this internally if id==0. - clone.id = 0; - // Reset chain link too — copying a chained quest with the - // same nextQuestId would corrupt the chain semantics. - clone.nextQuestId = 0; - clone.title = newTitle.empty() - ? (clone.title + " (copy)") - : newTitle; - qe.addQuest(clone); - if (!qe.saveToFile(path)) { - std::fprintf(stderr, "clone-quest: failed to write %s\n", path.c_str()); - return 1; - } - std::printf("Cloned quest %d -> '%s' (now %zu total)\n", - qIdx, clone.title.c_str(), qe.questCount()); - std::printf(" carried %zu objective(s), %zu item reward(s), xp=%u\n", - clone.objectives.size(), - clone.reward.itemRewards.size(), - clone.reward.xp); - return 0; - } else if (std::strcmp(argv[i], "--clone-creature") == 0 && i + 2 < argc) { - // Duplicate a creature spawn. Common workflow: design one - // 'patrol guard' archetype, then clone it across spawn points - // around a town. Preserves stats, faction, behavior, equipment; - // resets id and offsets position by 5 yards by default so the - // copy doesn't z-fight with the original. - std::string zoneDir = argv[++i]; - std::string idxStr = argv[++i]; - std::string newName; - float dx = 5.0f, dy = 0.0f, dz = 0.0f; - if (i + 1 < argc && argv[i + 1][0] != '-') { - newName = argv[++i]; - } - // Optional 3-axis offset after newName. - if (i + 3 < argc && argv[i + 1][0] != '-') { - try { - dx = std::stof(argv[++i]); - dy = std::stof(argv[++i]); - dz = std::stof(argv[++i]); - } catch (...) { - std::fprintf(stderr, "clone-creature: bad offset coordinate\n"); - return 1; - } - } - std::string path = zoneDir + "/creatures.json"; - if (!std::filesystem::exists(path)) { - std::fprintf(stderr, "clone-creature: %s not found\n", path.c_str()); - return 1; - } - int idx; - try { idx = std::stoi(idxStr); } - catch (...) { - std::fprintf(stderr, "clone-creature: bad idx '%s'\n", idxStr.c_str()); - return 1; - } - wowee::editor::NpcSpawner sp; - if (!sp.loadFromFile(path)) { - std::fprintf(stderr, "clone-creature: failed to load %s\n", path.c_str()); - return 1; - } - if (idx < 0 || idx >= static_cast(sp.spawnCount())) { - std::fprintf(stderr, - "clone-creature: idx %d out of range [0, %zu)\n", - idx, sp.spawnCount()); - return 1; - } - // Deep-copy by value; CreatureSpawn is POD-ish (vectors for - // patrol points copy automatically). - wowee::editor::CreatureSpawn clone = sp.getSpawns()[idx]; - clone.id = 0; // addCreature auto-assigns a fresh id - clone.name = newName.empty() - ? (clone.name + " (copy)") - : newName; - clone.position.x += dx; - clone.position.y += dy; - clone.position.z += dz; - // Patrol path is intentionally NOT offset — patrol points are - // typically authored as world-space waypoints, not relative to - // the spawn. Designers re-author the path if needed. - sp.getSpawns().push_back(clone); - if (!sp.saveToFile(path)) { - std::fprintf(stderr, "clone-creature: failed to write %s\n", path.c_str()); - return 1; - } - std::printf("Cloned creature %d -> '%s' at (%.1f, %.1f, %.1f) (now %zu total)\n", - idx, clone.name.c_str(), - clone.position.x, clone.position.y, clone.position.z, - sp.spawnCount()); - return 0; - } else if (std::strcmp(argv[i], "--clone-object") == 0 && i + 2 < argc) { - // Symmetric to --clone-creature/--clone-quest. Common - // workflow: place one tree/lamp/barrel just right, then - // clone N copies along a path or around a square. Default - // 5-yard X offset prevents z-fighting; rotation/scale are - // preserved so a tilted object stays tilted. - std::string zoneDir = argv[++i]; - std::string idxStr = argv[++i]; - float dx = 5.0f, dy = 0.0f, dz = 0.0f; - if (i + 3 < argc && argv[i + 1][0] != '-') { - try { - dx = std::stof(argv[++i]); - dy = std::stof(argv[++i]); - dz = std::stof(argv[++i]); - } catch (...) { - std::fprintf(stderr, "clone-object: bad offset\n"); - return 1; - } - } - std::string path = zoneDir + "/objects.json"; - if (!std::filesystem::exists(path)) { - std::fprintf(stderr, "clone-object: %s not found\n", path.c_str()); - return 1; - } - int idx; - try { idx = std::stoi(idxStr); } - catch (...) { - std::fprintf(stderr, "clone-object: bad idx '%s'\n", idxStr.c_str()); - return 1; - } - wowee::editor::ObjectPlacer placer; - if (!placer.loadFromFile(path)) { - std::fprintf(stderr, "clone-object: failed to load %s\n", path.c_str()); - return 1; - } - auto& objs = placer.getObjects(); - if (idx < 0 || idx >= static_cast(objs.size())) { - std::fprintf(stderr, - "clone-object: idx %d out of range [0, %zu)\n", - idx, objs.size()); - return 1; - } - // Deep-copy by value. uniqueId is reset so the new object - // doesn't collide with the source's identifier in any - // downstream system that dedups by it. - wowee::editor::PlacedObject clone = objs[idx]; - clone.uniqueId = 0; - clone.selected = false; - clone.position.x += dx; - clone.position.y += dy; - clone.position.z += dz; - objs.push_back(clone); - if (!placer.saveToFile(path)) { - std::fprintf(stderr, "clone-object: failed to write %s\n", path.c_str()); - return 1; - } - std::printf("Cloned object %d -> '%s' at (%.1f, %.1f, %.1f) (now %zu total)\n", - idx, clone.path.c_str(), - clone.position.x, clone.position.y, clone.position.z, - objs.size()); - return 0; } else if (std::strcmp(argv[i], "--remove-creature") == 0 && i + 2 < argc) { // Remove a creature spawn by 0-based index. Pair with // --info-creatures (or your editor) to find the right index