feat(editor): add --remove-tile and --list-tiles for zone tile management

Round out the tile lifecycle CLI. --add-tile shipped earlier; these
are the symmetric counterparts:

  wowee_editor --list-tiles custom_zones/MyZone
  wowee_editor --list-tiles custom_zones/MyZone --json
  wowee_editor --remove-tile custom_zones/MyZone 31 30

--list-tiles: shows tile coords AND on-disk presence of .whm/.wot/.woc
per tile, so you can spot manifest-vs-disk drift before pack-wcp would
complain. JSON mode emits per-tile records for programmatic checks.

--remove-tile: drops the manifest entry AND deletes the .whm/.wot/.woc
files for that tile so the zone stays consistent (no orphan sidecars).
Refuses to remove the last tile — server module gen and pack-wcp both
expect at least one, so a zero-tile zone would just be a footgun. Use
'rm -rf custom_zones/X/' for total removal.

Verified end-to-end: scaffolded zone, added 2 more tiles, built WOC
on one. --list-tiles shows 3 tiles with correct file presence (y/y/y
on the built one, y/y/- on the others). Removed (31, 30) — 2 files
deleted, 2 tiles remaining. Tried removing last tile after dropping
to 1: correctly refused with exit 1.
This commit is contained in:
Kelsi 2026-05-06 12:40:19 -07:00
parent 86978b55eb
commit d4b789b811

View file

@ -414,6 +414,10 @@ static void printUsage(const char* argv0) {
std::printf(" --scaffold-zone <name> [tx ty] Create a blank zone in custom_zones/<name>/ and exit\n");
std::printf(" --add-tile <zoneDir> <tx> <ty> [baseHeight]\n");
std::printf(" Add a new ADT tile to an existing zone (extends the manifest's tiles list)\n");
std::printf(" --remove-tile <zoneDir> <tx> <ty>\n");
std::printf(" Remove a tile from a zone (drops manifest entry + deletes WHM/WOT/WOC files)\n");
std::printf(" --list-tiles <zoneDir> [--json]\n");
std::printf(" List every tile in a zone manifest with on-disk file presence\n");
std::printf(" --add-creature <zoneDir> <name> <x> <y> <z> [displayId] [level]\n");
std::printf(" Append one creature spawn to <zoneDir>/creatures.json and exit\n");
std::printf(" --add-object <zoneDir> <m2|wmo> <gamePath> <x> <y> <z> [scale]\n");
@ -529,7 +533,7 @@ int main(int argc, char* argv[]) {
"--unpack-wcp", "--pack-wcp",
"--validate", "--validate-wom", "--validate-wob", "--validate-woc",
"--validate-whm", "--validate-all", "--zone-summary",
"--scaffold-zone", "--add-tile",
"--scaffold-zone", "--add-tile", "--remove-tile", "--list-tiles",
"--add-creature", "--add-object", "--add-quest",
"--add-quest-objective", "--add-quest-reward-item", "--set-quest-reward",
"--remove-quest-objective",
@ -602,6 +606,11 @@ int main(int argc, char* argv[]) {
"--add-tile requires <zoneDir> <tx> <ty>\n");
return 1;
}
if (std::strcmp(argv[i], "--remove-tile") == 0 && i + 3 >= argc) {
std::fprintf(stderr,
"--remove-tile requires <zoneDir> <tx> <ty>\n");
return 1;
}
if (std::strcmp(argv[i], "--copy-zone") == 0 && i + 2 >= argc) {
std::fprintf(stderr,
"--copy-zone requires <srcDir> <newName>\n");
@ -3769,6 +3778,119 @@ int main(int argc, char* argv[]) {
(zm.mapName + "_" + std::to_string(tx) + "_" + std::to_string(ty)).c_str());
std::printf(" tiles now : %zu total\n", zm.tiles.size());
return 0;
} else if (std::strcmp(argv[i], "--remove-tile") == 0 && i + 3 < argc) {
// Symmetric counterpart to --add-tile. Drops the entry from
// ZoneManifest::tiles AND deletes the WHM/WOT/WOC files on
// disk so the zone is left consistent (no orphan sidecars).
std::string zoneDir = argv[++i];
int tx, ty;
try {
tx = std::stoi(argv[++i]);
ty = std::stoi(argv[++i]);
} catch (...) {
std::fprintf(stderr, "remove-tile: bad coordinates\n");
return 1;
}
namespace fs = std::filesystem;
std::string manifestPath = zoneDir + "/zone.json";
if (!fs::exists(manifestPath)) {
std::fprintf(stderr, "remove-tile: %s has no zone.json — not a zone dir\n",
zoneDir.c_str());
return 1;
}
wowee::editor::ZoneManifest zm;
if (!zm.load(manifestPath)) {
std::fprintf(stderr, "remove-tile: failed to parse %s\n", manifestPath.c_str());
return 1;
}
auto it = std::find_if(zm.tiles.begin(), zm.tiles.end(),
[&](const std::pair<int,int>& p) { return p.first == tx && p.second == ty; });
if (it == zm.tiles.end()) {
std::fprintf(stderr,
"remove-tile: tile (%d, %d) not in manifest\n", tx, ty);
return 1;
}
// Don't strand a zone with zero tiles — server module gen and
// pack-wcp both expect at least one. The user can --rename-zone
// or rm -rf if they want the zone gone entirely.
if (zm.tiles.size() == 1) {
std::fprintf(stderr,
"remove-tile: refusing to remove last tile (zone would be empty)\n");
return 1;
}
zm.tiles.erase(it);
// Delete the slug-prefixed files for this tile. Use error_code
// so we don't throw on missing files — partial removal from
// earlier failures shouldn't block cleanup of what's left.
std::string base = zoneDir + "/" + zm.mapName + "_" +
std::to_string(tx) + "_" + std::to_string(ty);
int deleted = 0;
std::error_code ec;
for (const char* ext : {".whm", ".wot", ".woc"}) {
if (fs::remove(base + ext, ec)) deleted++;
}
if (!zm.save(manifestPath)) {
std::fprintf(stderr, "remove-tile: failed to save %s\n", manifestPath.c_str());
return 1;
}
std::printf("Removed tile (%d, %d) from %s\n", tx, ty, zoneDir.c_str());
std::printf(" deleted : %d file(s) (.whm/.wot/.woc)\n", deleted);
std::printf(" tiles now : %zu remaining\n", zm.tiles.size());
return 0;
} else if (std::strcmp(argv[i], "--list-tiles") == 0 && i + 1 < argc) {
// Enumerate every tile in the zone manifest with on-disk
// file presence — useful for spotting missing/orphan files
// before pack-wcp would fail.
std::string zoneDir = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
namespace fs = std::filesystem;
std::string manifestPath = zoneDir + "/zone.json";
if (!fs::exists(manifestPath)) {
std::fprintf(stderr, "list-tiles: %s has no zone.json\n", zoneDir.c_str());
return 1;
}
wowee::editor::ZoneManifest zm;
if (!zm.load(manifestPath)) {
std::fprintf(stderr, "list-tiles: failed to parse %s\n", manifestPath.c_str());
return 1;
}
auto baseFor = [&](int tx, int ty) {
return zoneDir + "/" + zm.mapName + "_" +
std::to_string(tx) + "_" + std::to_string(ty);
};
if (jsonOut) {
nlohmann::json j;
j["zone"] = zoneDir;
j["mapName"] = zm.mapName;
j["count"] = zm.tiles.size();
nlohmann::json arr = nlohmann::json::array();
for (const auto& [tx, ty] : zm.tiles) {
std::string b = baseFor(tx, ty);
arr.push_back({
{"x", tx}, {"y", ty},
{"whm", fs::exists(b + ".whm")},
{"wot", fs::exists(b + ".wot")},
{"woc", fs::exists(b + ".woc")},
});
}
j["tiles"] = arr;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("Zone: %s (%s, %zu tile(s))\n",
zoneDir.c_str(), zm.mapName.c_str(), zm.tiles.size());
std::printf(" tx ty whm wot woc\n");
for (const auto& [tx, ty] : zm.tiles) {
std::string b = baseFor(tx, ty);
std::printf(" %3d %3d %s %s %s\n",
tx, ty,
fs::exists(b + ".whm") ? "y" : "-",
fs::exists(b + ".wot") ? "y" : "-",
fs::exists(b + ".woc") ? "y" : "-");
}
return 0;
} else if (std::strcmp(argv[i], "--copy-zone") == 0 && i + 2 < argc) {
// Duplicate a zone — copy every file then rename slug-prefixed
// ones (heightmap/terrain/collision sidecars carry the slug in