refactor(editor): extract remove-* by-index handlers into cli_remove.cpp

Moves the four bounds-checked remove-by-index handlers
(--remove-creature, --remove-object, --remove-quest,
--remove-item) out of main.cpp into a new
cli_remove.{hpp,cpp} module. All four share the same
load-erase-save pattern with index validation and a "what
was removed" report for audit trails. The first three use
their respective editor classes (NpcSpawner, ObjectPlacer,
QuestEditor); --remove-item walks raw nlohmann::json since
items.json doesn't have a dedicated editor class yet.

main.cpp shrinks by 145 lines (6,131 to 5,986).
This commit is contained in:
Kelsi 2026-05-09 08:11:50 -07:00
parent f1a2e6850b
commit 614d48a49b
4 changed files with 225 additions and 150 deletions

199
tools/editor/cli_remove.cpp Normal file
View file

@ -0,0 +1,199 @@
#include "cli_remove.hpp"
#include "npc_spawner.hpp"
#include "object_placer.hpp"
#include "quest_editor.hpp"
#include <nlohmann/json.hpp>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
namespace wowee {
namespace editor {
namespace cli {
namespace {
int handleRemoveCreature(int& i, int /*argc*/, char** argv) {
// Remove a creature spawn by 0-based index. Pair with
// --info-creatures (or your editor) to find the right index
// first; nothing identifies entries reliably across reloads.
std::string zoneDir = argv[++i];
std::string idxStr = argv[++i];
std::string path = zoneDir + "/creatures.json";
if (!std::filesystem::exists(path)) {
std::fprintf(stderr, "remove-creature: %s not found\n", path.c_str());
return 1;
}
int idx;
try { idx = std::stoi(idxStr); }
catch (...) {
std::fprintf(stderr, "remove-creature: bad index '%s'\n", idxStr.c_str());
return 1;
}
wowee::editor::NpcSpawner sp;
sp.loadFromFile(path);
if (idx < 0 || idx >= static_cast<int>(sp.spawnCount())) {
std::fprintf(stderr, "remove-creature: index %d out of range [0, %zu)\n",
idx, sp.spawnCount());
return 1;
}
std::string removedName = sp.getSpawns()[idx].name;
sp.removeCreature(idx);
if (!sp.saveToFile(path)) {
std::fprintf(stderr, "remove-creature: failed to write %s\n", path.c_str());
return 1;
}
std::printf("Removed creature '%s' (was index %d) from %s (now %zu total)\n",
removedName.c_str(), idx, path.c_str(), sp.spawnCount());
return 0;
}
int handleRemoveObject(int& i, int /*argc*/, char** argv) {
std::string zoneDir = argv[++i];
std::string idxStr = argv[++i];
std::string path = zoneDir + "/objects.json";
if (!std::filesystem::exists(path)) {
std::fprintf(stderr, "remove-object: %s not found\n", path.c_str());
return 1;
}
int idx;
try { idx = std::stoi(idxStr); }
catch (...) {
std::fprintf(stderr, "remove-object: bad index '%s'\n", idxStr.c_str());
return 1;
}
wowee::editor::ObjectPlacer placer;
placer.loadFromFile(path);
auto& objs = placer.getObjects();
if (idx < 0 || idx >= static_cast<int>(objs.size())) {
std::fprintf(stderr, "remove-object: index %d out of range [0, %zu)\n",
idx, objs.size());
return 1;
}
std::string removedPath = objs[idx].path;
objs.erase(objs.begin() + idx);
if (!placer.saveToFile(path)) {
std::fprintf(stderr, "remove-object: failed to write %s\n", path.c_str());
return 1;
}
std::printf("Removed object '%s' (was index %d) from %s (now %zu total)\n",
removedPath.c_str(), idx, path.c_str(), objs.size());
return 0;
}
int handleRemoveQuest(int& i, int /*argc*/, char** argv) {
std::string zoneDir = argv[++i];
std::string idxStr = argv[++i];
std::string path = zoneDir + "/quests.json";
if (!std::filesystem::exists(path)) {
std::fprintf(stderr, "remove-quest: %s not found\n", path.c_str());
return 1;
}
int idx;
try { idx = std::stoi(idxStr); }
catch (...) {
std::fprintf(stderr, "remove-quest: bad index '%s'\n", idxStr.c_str());
return 1;
}
wowee::editor::QuestEditor qe;
qe.loadFromFile(path);
if (idx < 0 || idx >= static_cast<int>(qe.questCount())) {
std::fprintf(stderr, "remove-quest: index %d out of range [0, %zu)\n",
idx, qe.questCount());
return 1;
}
std::string removedTitle = qe.getQuests()[idx].title;
qe.removeQuest(idx);
if (!qe.saveToFile(path)) {
std::fprintf(stderr, "remove-quest: failed to write %s\n", path.c_str());
return 1;
}
std::printf("Removed quest '%s' (was index %d) from %s (now %zu total)\n",
removedTitle.c_str(), idx, path.c_str(), qe.questCount());
return 0;
}
int handleRemoveItem(int& i, int /*argc*/, char** argv) {
// Remove the item at given 0-based index from <zoneDir>/
// items.json. Mirrors --remove-creature/--remove-object/
// --remove-quest semantics — bounds-checked, file rewrites
// on success, exit 1 on out-of-range.
std::string zoneDir = argv[++i];
int idx = -1;
try { idx = std::stoi(argv[++i]); }
catch (...) {
std::fprintf(stderr,
"remove-item: index must be an integer\n");
return 1;
}
namespace fs = std::filesystem;
std::string path = zoneDir + "/items.json";
if (!fs::exists(path)) {
std::fprintf(stderr,
"remove-item: %s has no items.json\n", zoneDir.c_str());
return 1;
}
nlohmann::json doc;
try {
std::ifstream in(path);
in >> doc;
} catch (...) {
std::fprintf(stderr,
"remove-item: %s is not valid JSON\n", path.c_str());
return 1;
}
if (!doc.contains("items") || !doc["items"].is_array()) {
std::fprintf(stderr,
"remove-item: %s has no 'items' array\n", path.c_str());
return 1;
}
auto& items = doc["items"];
if (idx < 0 || static_cast<size_t>(idx) >= items.size()) {
std::fprintf(stderr,
"remove-item: index %d out of range (have %zu)\n",
idx, items.size());
return 1;
}
std::string removedName = items[idx].value("name", std::string("(unnamed)"));
uint32_t removedId = items[idx].value("id", 0u);
items.erase(items.begin() + idx);
std::ofstream out(path);
if (!out) {
std::fprintf(stderr,
"remove-item: failed to write %s\n", path.c_str());
return 1;
}
out << doc.dump(2);
out.close();
std::printf("Removed item '%s' (id=%u) from %s (now %zu total)\n",
removedName.c_str(), removedId,
path.c_str(), items.size());
return 0;
}
} // namespace
bool handleRemove(int& i, int argc, char** argv, int& outRc) {
if (std::strcmp(argv[i], "--remove-creature") == 0 && i + 2 < argc) {
outRc = handleRemoveCreature(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--remove-object") == 0 && i + 2 < argc) {
outRc = handleRemoveObject(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--remove-quest") == 0 && i + 2 < argc) {
outRc = handleRemoveQuest(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--remove-item") == 0 && i + 2 < argc) {
outRc = handleRemoveItem(i, argc, argv); return true;
}
return false;
}
} // namespace cli
} // namespace editor
} // namespace wowee

View file

@ -0,0 +1,21 @@
#pragma once
namespace wowee {
namespace editor {
namespace cli {
// Dispatch the remove-* by-index handlers — strip a single
// entry out of a zone's creatures/objects/quests/items list
// by 0-based index. All four use bounds-checked load-erase-save
// and report what was removed for audit trails.
// --remove-creature
// --remove-object
// --remove-quest
// --remove-item
//
// Returns true if matched; outRc holds the exit code.
bool handleRemove(int& i, int argc, char** argv, int& outRc);
} // namespace cli
} // namespace editor
} // namespace wowee

View file

@ -40,6 +40,7 @@
#include "cli_quest_objective.hpp"
#include "cli_quest_reward.hpp"
#include "cli_clone.hpp"
#include "cli_remove.hpp"
#include "content_pack.hpp"
#include "npc_spawner.hpp"
#include "object_placer.hpp"
@ -495,6 +496,9 @@ int main(int argc, char* argv[]) {
if (wowee::editor::cli::handleClone(i, argc, argv, outRc)) {
return outRc;
}
if (wowee::editor::cli::handleRemove(i, argc, argv, outRc)) {
return outRc;
}
}
if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) {
dataPath = argv[++i];
@ -1423,100 +1427,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], "--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
// first; nothing identifies entries reliably across reloads.
std::string zoneDir = argv[++i];
std::string idxStr = argv[++i];
std::string path = zoneDir + "/creatures.json";
if (!std::filesystem::exists(path)) {
std::fprintf(stderr, "remove-creature: %s not found\n", path.c_str());
return 1;
}
int idx;
try { idx = std::stoi(idxStr); }
catch (...) {
std::fprintf(stderr, "remove-creature: bad index '%s'\n", idxStr.c_str());
return 1;
}
wowee::editor::NpcSpawner sp;
sp.loadFromFile(path);
if (idx < 0 || idx >= static_cast<int>(sp.spawnCount())) {
std::fprintf(stderr, "remove-creature: index %d out of range [0, %zu)\n",
idx, sp.spawnCount());
return 1;
}
std::string removedName = sp.getSpawns()[idx].name;
sp.removeCreature(idx);
if (!sp.saveToFile(path)) {
std::fprintf(stderr, "remove-creature: failed to write %s\n", path.c_str());
return 1;
}
std::printf("Removed creature '%s' (was index %d) from %s (now %zu total)\n",
removedName.c_str(), idx, path.c_str(), sp.spawnCount());
return 0;
} else if (std::strcmp(argv[i], "--remove-object") == 0 && i + 2 < argc) {
std::string zoneDir = argv[++i];
std::string idxStr = argv[++i];
std::string path = zoneDir + "/objects.json";
if (!std::filesystem::exists(path)) {
std::fprintf(stderr, "remove-object: %s not found\n", path.c_str());
return 1;
}
int idx;
try { idx = std::stoi(idxStr); }
catch (...) {
std::fprintf(stderr, "remove-object: bad index '%s'\n", idxStr.c_str());
return 1;
}
wowee::editor::ObjectPlacer placer;
placer.loadFromFile(path);
auto& objs = placer.getObjects();
if (idx < 0 || idx >= static_cast<int>(objs.size())) {
std::fprintf(stderr, "remove-object: index %d out of range [0, %zu)\n",
idx, objs.size());
return 1;
}
std::string removedPath = objs[idx].path;
objs.erase(objs.begin() + idx);
if (!placer.saveToFile(path)) {
std::fprintf(stderr, "remove-object: failed to write %s\n", path.c_str());
return 1;
}
std::printf("Removed object '%s' (was index %d) from %s (now %zu total)\n",
removedPath.c_str(), idx, path.c_str(), objs.size());
return 0;
} else if (std::strcmp(argv[i], "--remove-quest") == 0 && i + 2 < argc) {
std::string zoneDir = argv[++i];
std::string idxStr = argv[++i];
std::string path = zoneDir + "/quests.json";
if (!std::filesystem::exists(path)) {
std::fprintf(stderr, "remove-quest: %s not found\n", path.c_str());
return 1;
}
int idx;
try { idx = std::stoi(idxStr); }
catch (...) {
std::fprintf(stderr, "remove-quest: bad index '%s'\n", idxStr.c_str());
return 1;
}
wowee::editor::QuestEditor qe;
qe.loadFromFile(path);
if (idx < 0 || idx >= static_cast<int>(qe.questCount())) {
std::fprintf(stderr, "remove-quest: index %d out of range [0, %zu)\n",
idx, qe.questCount());
return 1;
}
std::string removedTitle = qe.getQuests()[idx].title;
qe.removeQuest(idx);
if (!qe.saveToFile(path)) {
std::fprintf(stderr, "remove-quest: failed to write %s\n", path.c_str());
return 1;
}
std::printf("Removed quest '%s' (was index %d) from %s (now %zu total)\n",
removedTitle.c_str(), idx, path.c_str(), qe.questCount());
return 0;
} else if (std::strcmp(argv[i], "--add-object") == 0 && i + 5 < argc) {
// Append a single object placement to a zone's objects.json.
// Args: <zoneDir> <m2|wmo> <gamePath> <x> <y> <z> [scale]
@ -2578,62 +2488,6 @@ int main(int argc, char* argv[]) {
std::printf(" zones with items : %zu\n", zones.size());
std::printf(" rows : %d\n", totalRows);
return 0;
} else if (std::strcmp(argv[i], "--remove-item") == 0 && i + 2 < argc) {
// Remove the item at given 0-based index from <zoneDir>/
// items.json. Mirrors --remove-creature/--remove-object/
// --remove-quest semantics — bounds-checked, file rewrites
// on success, exit 1 on out-of-range.
std::string zoneDir = argv[++i];
int idx = -1;
try { idx = std::stoi(argv[++i]); }
catch (...) {
std::fprintf(stderr,
"remove-item: index must be an integer\n");
return 1;
}
namespace fs = std::filesystem;
std::string path = zoneDir + "/items.json";
if (!fs::exists(path)) {
std::fprintf(stderr,
"remove-item: %s has no items.json\n", zoneDir.c_str());
return 1;
}
nlohmann::json doc;
try {
std::ifstream in(path);
in >> doc;
} catch (...) {
std::fprintf(stderr,
"remove-item: %s is not valid JSON\n", path.c_str());
return 1;
}
if (!doc.contains("items") || !doc["items"].is_array()) {
std::fprintf(stderr,
"remove-item: %s has no 'items' array\n", path.c_str());
return 1;
}
auto& items = doc["items"];
if (idx < 0 || static_cast<size_t>(idx) >= items.size()) {
std::fprintf(stderr,
"remove-item: index %d out of range (have %zu)\n",
idx, items.size());
return 1;
}
std::string removedName = items[idx].value("name", std::string("(unnamed)"));
uint32_t removedId = items[idx].value("id", 0u);
items.erase(items.begin() + idx);
std::ofstream out(path);
if (!out) {
std::fprintf(stderr,
"remove-item: failed to write %s\n", path.c_str());
return 1;
}
out << doc.dump(2);
out.close();
std::printf("Removed item '%s' (id=%u) from %s (now %zu total)\n",
removedName.c_str(), removedId,
path.c_str(), items.size());
return 0;
} else if (std::strcmp(argv[i], "--copy-zone-items") == 0 && i + 2 < argc) {
// Copy items from one zone to another. Default mode
// replaces the destination items.json wholesale; --merge