diff --git a/CMakeLists.txt b/CMakeLists.txt index bd52f8b9..30c175ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1351,6 +1351,7 @@ add_executable(wowee_editor tools/editor/cli_repair.cpp tools/editor/cli_makefile.cpp tools/editor/cli_zone_list.cpp + tools/editor/cli_tilemap.cpp tools/editor/editor_app.cpp tools/editor/editor_camera.cpp tools/editor/editor_viewport.cpp diff --git a/tools/editor/cli_tilemap.cpp b/tools/editor/cli_tilemap.cpp new file mode 100644 index 00000000..9ee32438 --- /dev/null +++ b/tools/editor/cli_tilemap.cpp @@ -0,0 +1,155 @@ +#include "cli_tilemap.hpp" + +#include "zone_manifest.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +int handleInfoTilemap(int& i, int argc, char** argv) { + // 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 so collision overlaps + // are visible. Walk every zone in the project. + std::map, std::vector> claims; + std::vector 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 zoneGlyph; + char nextGlyph = 'A'; + for (const auto& z : zones) { + if (zoneGlyph.count(z)) continue; + if (!z.empty() && std::isalpha(static_cast(z[0]))) { + zoneGlyph[z] = static_cast(std::toupper(static_cast(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; +} + + +} // namespace + +bool handleTilemap(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--info-tilemap") == 0 && i + 1 < argc) { + outRc = handleInfoTilemap(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_tilemap.hpp b/tools/editor/cli_tilemap.hpp new file mode 100644 index 00000000..c45b3ba3 --- /dev/null +++ b/tools/editor/cli_tilemap.hpp @@ -0,0 +1,19 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +// Dispatch the --info-tilemap project-wide tile-grid handler. +// Renders the WoW 64x64 ADT grid showing which tiles are +// claimed by which zones, with collision detection (multiple +// zones claiming the same tile). Useful for spotting tile- +// coord overlaps before two zones try to ship conflicting +// content. Supports --json for machine-readable reports. +// +// Returns true if matched; outRc holds the exit code. +bool handleTilemap(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index d07d4e86..41eb41ce 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -52,6 +52,7 @@ #include "cli_repair.hpp" #include "cli_makefile.hpp" #include "cli_zone_list.hpp" +#include "cli_tilemap.hpp" #include "content_pack.hpp" #include "npc_spawner.hpp" #include "object_placer.hpp" @@ -548,6 +549,9 @@ int main(int argc, char* argv[]) { if (wowee::editor::cli::handleZoneList(i, argc, argv, outRc)) { return outRc; } + if (wowee::editor::cli::handleTilemap(i, argc, argv, outRc)) { + return outRc; + } } if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) { dataPath = argv[++i]; @@ -1676,125 +1680,6 @@ int main(int argc, char* argv[]) { std::printf(" next : --add-texture-to-mesh %s\n", destPath.c_str()); 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 so collision overlaps - // are visible. Walk every zone in the project. - std::map, std::vector> claims; - std::vector 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 zoneGlyph; - char nextGlyph = 'A'; - for (const auto& z : zones) { - if (zoneGlyph.count(z)) continue; - if (!z.empty() && std::isalpha(static_cast(z[0]))) { - zoneGlyph[z] = static_cast(std::toupper(static_cast(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