feat(editor): add --info-tilemap for project-wide ADT grid visualization

Renders the WoW 64x64 ADT coordinate grid as ASCII art showing which
tiles are claimed by which zones. Useful for spotting tile-coord
collisions before two zones ship overlapping content, and for
'where am I working?' overview of multi-zone projects:

  wowee_editor --info-tilemap custom_zones

  Tilemap: custom_zones
    zones      : 2
    tiles used : 5
    collisions : 0 (multiple zones claiming same tile)
    legend     : D=Desert F=Forest

         0         1         2         3         4         5         6
         0123456789012345678901234567890123456789012345678901234567890123
    y=30 ..............................FFDD..............................
    y=31 ...............................F................................

Glyphs are first-letter-of-zone uppercased; collision tiles render
as '*'. Empty rows are skipped to keep output bounded for projects
in one corner of the map (skipping all 60+ blank rows of the full
64x64 grid).

Exit 1 on collisions so CI can gate before merging two PRs that
both add tiles in the same area. JSON mode emits per-tile claims
for programmatic consumption.

Verified on a 2-zone project: Forest (3 tiles) + Desert (2 tiles)
correctly rendered, no collisions detected, glyphs F/D placed at
the right grid coords.
This commit is contained in:
Kelsi 2026-05-06 15:01:32 -07:00
parent 1718c2333f
commit f73b377b4d

View file

@ -426,6 +426,8 @@ static void printUsage(const char* argv0) {
std::printf(" --list-zones [--json] List discovered custom zones and exit\n");
std::printf(" --zone-stats <projectDir> [--json]\n");
std::printf(" Aggregate counts across every zone in <projectDir>\n");
std::printf(" --info-tilemap <projectDir> [--json]\n");
std::printf(" ASCII-render the 64x64 WoW ADT grid showing tile claims by zone\n");
std::printf(" --list-zone-deps <zoneDir> [--json]\n");
std::printf(" List external M2/WMO model paths a zone references (objects + WOB doodads)\n");
std::printf(" --export-zone-deps-md <zoneDir> [out.md]\n");
@ -672,8 +674,8 @@ int main(int argc, char* argv[]) {
"--export-zone-summary-md", "--export-quest-graph",
"--export-zone-csv", "--export-zone-html",
"--scaffold-zone", "--add-tile", "--remove-tile", "--list-tiles",
"--for-each-zone", "--zone-stats", "--list-zone-deps",
"--check-zone-refs", "--export-zone-deps-md",
"--for-each-zone", "--zone-stats", "--info-tilemap",
"--list-zone-deps", "--check-zone-refs", "--export-zone-deps-md",
"--add-creature", "--add-object", "--add-quest",
"--add-quest-objective", "--add-quest-reward-item", "--set-quest-reward",
"--remove-quest-objective", "--clone-quest", "--clone-creature",
@ -9511,6 +9513,125 @@ int main(int argc, char* argv[]) {
r.bytes / kKB);
}
return 0;
} else if (std::strcmp(argv[i], "--info-tilemap") == 0 && i + 1 < argc) {
// Visualize the WoW 64x64 ADT grid showing which tiles are
// claimed by which zones across a project. Useful for
// spotting tile-coord collisions before two zones try to
// ship overlapping content, and for getting a 'where am I
// working?' overview of a multi-zone project.
std::string projectDir = argv[++i];
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
namespace fs = std::filesystem;
if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) {
std::fprintf(stderr,
"info-tilemap: %s is not a directory\n", projectDir.c_str());
return 1;
}
// Map (tx, ty) -> vector<zone names> so collision overlaps
// are visible. Walk every zone in the project.
std::map<std::pair<int,int>, std::vector<std::string>> claims;
std::vector<std::string> zones;
for (const auto& entry : fs::directory_iterator(projectDir)) {
if (!entry.is_directory()) continue;
if (!fs::exists(entry.path() / "zone.json")) continue;
wowee::editor::ZoneManifest zm;
if (!zm.load((entry.path() / "zone.json").string())) continue;
std::string zname = zm.mapName.empty()
? entry.path().filename().string() : zm.mapName;
zones.push_back(zname);
for (const auto& [tx, ty] : zm.tiles) {
if (tx >= 0 && tx < 64 && ty >= 0 && ty < 64) {
claims[{tx, ty}].push_back(zname);
}
}
}
// Per-zone label glyph: first letter of the zone name,
// uppercased so different zones get distinct chars in the
// grid. Multi-letter overlap collapses to '*'.
std::map<std::string, char> zoneGlyph;
char nextGlyph = 'A';
for (const auto& z : zones) {
if (zoneGlyph.count(z)) continue;
if (!z.empty() && std::isalpha(static_cast<unsigned char>(z[0]))) {
zoneGlyph[z] = static_cast<char>(std::toupper(static_cast<unsigned char>(z[0])));
} else {
zoneGlyph[z] = nextGlyph++;
if (nextGlyph > 'Z') nextGlyph = 'a';
}
}
int collisions = 0;
for (const auto& [coord, owners] : claims) {
if (owners.size() > 1) collisions++;
}
if (jsonOut) {
nlohmann::json j;
j["projectDir"] = projectDir;
j["zoneCount"] = zones.size();
j["claimedTiles"] = claims.size();
j["collisions"] = collisions;
nlohmann::json claimsJson = nlohmann::json::array();
for (const auto& [coord, owners] : claims) {
claimsJson.push_back({{"x", coord.first},
{"y", coord.second},
{"zones", owners}});
}
j["claims"] = claimsJson;
std::printf("%s\n", j.dump(2).c_str());
return collisions == 0 ? 0 : 1;
}
std::printf("Tilemap: %s\n", projectDir.c_str());
std::printf(" zones : %zu\n", zones.size());
std::printf(" tiles used : %zu\n", claims.size());
std::printf(" collisions : %d (multiple zones claiming same tile)\n",
collisions);
std::printf(" legend :");
for (const auto& [name, glyph] : zoneGlyph) {
std::printf(" %c=%s", glyph, name.c_str());
}
std::printf("\n\n");
// Render 64x64 grid. Print column header in groups of 10
// for readability.
std::printf(" ");
for (int x = 0; x < 64; ++x) {
std::printf("%c", (x % 10 == 0) ? '0' + (x / 10) : ' ');
}
std::printf("\n");
std::printf(" ");
for (int x = 0; x < 64; ++x) std::printf("%d", x % 10);
std::printf("\n");
for (int y = 0; y < 64; ++y) {
// Skip rows that have no tiles claimed — keeps the
// output bounded for projects in one corner of the map.
bool rowHasContent = false;
for (int x = 0; x < 64 && !rowHasContent; ++x) {
if (claims.count({x, y})) rowHasContent = true;
}
if (!rowHasContent) continue;
std::printf(" y=%2d ", y);
for (int x = 0; x < 64; ++x) {
auto it = claims.find({x, y});
if (it == claims.end()) {
std::printf(".");
} else if (it->second.size() > 1) {
std::printf("*"); // collision
} else {
std::printf("%c", zoneGlyph[it->second[0]]);
}
}
std::printf("\n");
}
if (collisions > 0) {
std::printf("\n COLLISIONS:\n");
for (const auto& [coord, owners] : claims) {
if (owners.size() < 2) continue;
std::printf(" (%d, %d) claimed by:", coord.first, coord.second);
for (const auto& o : owners) std::printf(" %s", o.c_str());
std::printf("\n");
}
}
return collisions == 0 ? 0 : 1;
} else if (std::strcmp(argv[i], "--list-zone-deps") == 0 && i + 1 < argc) {
// Enumerate every external model path a zone references —
// both directly placed (objects.json) and indirectly via