From 9c46d3aeeb0592667719f9911bc78f719eb20afc Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 12:33:32 -0700 Subject: [PATCH] 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. --- tools/editor/main.cpp | 87 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index a3c380f5..3da9eaa8 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -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 [tx ty] Create a blank zone in custom_zones// and exit\n"); + std::printf(" --add-tile [baseHeight]\n"); + std::printf(" Add a new ADT tile to an existing zone (extends the manifest's tiles list)\n"); std::printf(" --add-creature [displayId] [level]\n"); std::printf(" Append one creature spawn to /creatures.json and exit\n"); std::printf(" --add-object [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 [--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 \n"); + return 1; + } if (std::strcmp(argv[i], "--copy-zone") == 0 && i + 2 >= argc) { std::fprintf(stderr, "--copy-zone requires \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