feat(editor): add --add-tile to extend a zone with another ADT tile

--scaffold-zone creates a zone with one tile; some zones (continent
fragments, large dungeons) span multiple ADT tiles. This extends an
existing zone:

  wowee_editor --add-tile custom_zones/MyZone 29 30           # default baseHeight=100
  wowee_editor --add-tile custom_zones/MyZone 28 31 250.5     # custom baseHeight

What it does:
- Generates a fresh blank-flat WHM/WOT pair via the same factory
  --scaffold-zone uses, so output is consistent.
- Appends (tx, ty) to ZoneManifest::tiles. Save() rebuilds the
  files-block from tiles, so the new adt_TX_TY entry appears
  automatically in zone.json.

Safety:
- Tile coord must be in WoW grid [0, 64) per axis; rejects 99,99.
- Refuses if the tile is already in the manifest (catches typos).
- Refuses if the .whm/.wot files exist on disk but aren't in the
  manifest (catches manifest-out-of-sync drift from hand edits).
- Optional baseHeight allows seeding flat terrain at a non-default
  elevation.

Verified end-to-end: scaffolded 1-tile zone, added 2 more tiles
(one with custom height). Result: 3 tiles in manifest, 6 files on
disk, files-block has all 3 adt_TX_TY entries. Duplicate and
out-of-range cases both rejected with exit 1.
This commit is contained in:
Kelsi 2026-05-06 12:33:32 -07:00
parent b04a3ede99
commit 9c46d3aeeb

View file

@ -409,6 +409,8 @@ static void printUsage(const char* argv0) {
std::printf(" Convert a wowee JSON DBC back to binary DBC for private-server compat\n");
std::printf(" --list-zones [--json] List discovered custom zones and exit\n");
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(" --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");
@ -522,7 +524,8 @@ 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-creature", "--add-object", "--add-quest",
"--scaffold-zone", "--add-tile",
"--add-creature", "--add-object", "--add-quest",
"--add-quest-objective", "--add-quest-reward-item", "--set-quest-reward",
"--remove-creature", "--remove-object", "--remove-quest",
"--copy-zone", "--rename-zone",
@ -583,6 +586,11 @@ int main(int argc, char* argv[]) {
"--set-quest-reward requires <zoneDir> <questIdx> [--xp N] [--gold N] [--silver N] [--copper N]\n");
return 1;
}
if (std::strcmp(argv[i], "--add-tile") == 0 && i + 3 >= argc) {
std::fprintf(stderr,
"--add-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");
@ -3622,6 +3630,83 @@ int main(int argc, char* argv[]) {
slug.c_str(), slug.c_str());
std::printf(" next step: run editor without args, then File → Open Zone\n");
return 0;
} else if (std::strcmp(argv[i], "--add-tile") == 0 && i + 3 < argc) {
// Extend an existing zone with another ADT tile. Zones can
// span multiple tiles (e.g. a continent fragment), but
// --scaffold-zone only creates one. This adds another:
// wowee_editor --add-tile custom_zones/MyZone 29 30
// Generates a fresh blank-flat WHM/WOT pair at the new tile
// and appends to the zone manifest's tiles list.
std::string zoneDir = argv[++i];
int tx, ty;
try {
tx = std::stoi(argv[++i]);
ty = std::stoi(argv[++i]);
} catch (...) {
std::fprintf(stderr, "add-tile: bad coordinates\n");
return 1;
}
float baseHeight = 100.0f;
if (i + 1 < argc && argv[i + 1][0] != '-') {
try { baseHeight = std::stof(argv[++i]); }
catch (...) {}
}
if (tx < 0 || tx >= 64 || ty < 0 || ty >= 64) {
std::fprintf(stderr, "add-tile: tile coord (%d, %d) out of WoW grid [0, 64)\n",
tx, ty);
return 1;
}
namespace fs = std::filesystem;
std::string manifestPath = zoneDir + "/zone.json";
if (!fs::exists(manifestPath)) {
std::fprintf(stderr, "add-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, "add-tile: failed to parse %s\n", manifestPath.c_str());
return 1;
}
// Reject duplicates so we don't silently overwrite an existing
// tile's heightmap when the user makes a typo.
for (const auto& [ex, ey] : zm.tiles) {
if (ex == tx && ey == ty) {
std::fprintf(stderr,
"add-tile: tile (%d, %d) already in manifest\n", tx, ty);
return 1;
}
}
// Also bail if the file would clobber an existing one outside
// the manifest (e.g. user hand-created tiles without updating
// zone.json). Catches drift between disk and manifest.
std::string base = zoneDir + "/" + zm.mapName + "_" +
std::to_string(tx) + "_" + std::to_string(ty);
if (fs::exists(base + ".whm") || fs::exists(base + ".wot")) {
std::fprintf(stderr,
"add-tile: %s.{whm,wot} already exists on disk (manifest out of sync?)\n",
base.c_str());
return 1;
}
// Generate the new heightmap. Reuses the same factory that
// --scaffold-zone uses, so the output is consistent.
auto terrain = wowee::editor::TerrainEditor::createBlankTerrain(
tx, ty, baseHeight, wowee::editor::Biome::Grassland);
wowee::editor::WoweeTerrain::exportOpen(terrain, base, tx, ty);
// Append + save manifest. ZoneManifest::save rebuilds the
// files block from the tiles list, so the new adt_tx_ty entry
// appears automatically in zone.json.
zm.tiles.push_back({tx, ty});
if (!zm.save(manifestPath)) {
std::fprintf(stderr, "add-tile: failed to save %s\n", manifestPath.c_str());
return 1;
}
std::printf("Added tile (%d, %d) to %s\n", tx, ty, zoneDir.c_str());
std::printf(" files : %s.whm, %s.wot\n",
(zm.mapName + "_" + std::to_string(tx) + "_" + std::to_string(ty)).c_str(),
(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], "--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