From 32e7eef107550eddeeba6d50b00469ea2fdb50b9 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 10:01:33 -0700 Subject: [PATCH] refactor(editor): extract zone-level export handlers into cli_zone_export.cpp Moves three per-zone visual / report exporters (--export-png, --export-zone-deps-md, --export-zone-spawn-png) out of main.cpp into a new cli_zone_export.{hpp,cpp} module. - export-png renders heightmap + normal-map + zone-map PNG triplet for terrain previews - export-zone-deps-md emits a GitHub-renderable model dependency table (PR-friendly counterpart to --list-zone-deps) - export-zone-spawn-png plots top-down spawn distribution PNG bound to the zone's tile range main.cpp shrinks by 267 lines (944 to 677) and crosses below 700 lines on the way down. The previous --info-tilemap dump used a 64x64 grid; this set complements it with content- relative views. --- CMakeLists.txt | 1 + tools/editor/cli_zone_export.cpp | 326 +++++++++++++++++++++++++++++++ tools/editor/cli_zone_export.hpp | 19 ++ tools/editor/main.cpp | 275 +------------------------- 4 files changed, 350 insertions(+), 271 deletions(-) create mode 100644 tools/editor/cli_zone_export.cpp create mode 100644 tools/editor/cli_zone_export.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 574e732f..ebd84345 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1360,6 +1360,7 @@ add_executable(wowee_editor tools/editor/cli_mesh_info.cpp tools/editor/cli_zone_data.cpp tools/editor/cli_project_actions.cpp + tools/editor/cli_zone_export.cpp tools/editor/editor_app.cpp tools/editor/editor_camera.cpp tools/editor/editor_viewport.cpp diff --git a/tools/editor/cli_zone_export.cpp b/tools/editor/cli_zone_export.cpp new file mode 100644 index 00000000..285740b6 --- /dev/null +++ b/tools/editor/cli_zone_export.cpp @@ -0,0 +1,326 @@ +#include "cli_zone_export.hpp" + +#include "zone_manifest.hpp" +#include "npc_spawner.hpp" +#include "object_placer.hpp" +#include "wowee_terrain.hpp" +#include "pipeline/wowee_terrain_loader.hpp" +#include "pipeline/wowee_building.hpp" +#include "pipeline/adt_loader.hpp" +#include "stb_image_write.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +int handleExportPng(int& i, int argc, char** argv) { + // Render heightmap, normal-map, and zone-map PNG previews for a + // terrain. Useful for portfolio screenshots, ground-truth map + // comparison, and quick visual validation without launching GUI. + std::string base = argv[++i]; + for (const char* ext : {".wot", ".whm"}) { + if (base.size() >= 4 && base.substr(base.size() - 4) == ext) { + base = base.substr(0, base.size() - 4); + break; + } + } + if (!wowee::pipeline::WoweeTerrainLoader::exists(base)) { + std::fprintf(stderr, "WOT/WHM not found at base: %s\n", base.c_str()); + return 1; + } + wowee::pipeline::ADTTerrain terrain; + if (!wowee::pipeline::WoweeTerrainLoader::load(base, terrain)) { + std::fprintf(stderr, "Failed to load terrain: %s\n", base.c_str()); + return 1; + } + wowee::editor::WoweeTerrain::exportHeightmapPreview(terrain, base + "_heightmap.png"); + wowee::editor::WoweeTerrain::exportNormalMap(terrain, base + "_normals.png"); + wowee::editor::WoweeTerrain::exportZoneMap(terrain, base + "_zone.png", 512); + std::printf("Exported PNGs: %s_{heightmap,normals,zone}.png\n", base.c_str()); + return 0; +} + +int handleExportZoneDepsMd(int& i, int argc, char** argv) { + // Markdown counterpart to --list-zone-deps. Writes a sortable + // GitHub-rendered table of every external model the zone + // references plus on-disk presence (so PR reviewers see at a + // glance whether dependencies are accounted for in the + // accompanying asset bundle). + std::string zoneDir = argv[++i]; + std::string outPath; + if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; + namespace fs = std::filesystem; + if (!fs::exists(zoneDir + "/zone.json")) { + std::fprintf(stderr, + "export-zone-deps-md: %s has no zone.json\n", zoneDir.c_str()); + return 1; + } + wowee::editor::ZoneManifest zm; + zm.load(zoneDir + "/zone.json"); + if (outPath.empty()) outPath = zoneDir + "/DEPS.md"; + // Same dep-collection pass as --list-zone-deps. + std::map directM2; + std::map directWMO; + std::map doodadM2; + wowee::editor::ObjectPlacer op; + if (op.loadFromFile(zoneDir + "/objects.json")) { + for (const auto& o : op.getObjects()) { + if (o.type == wowee::editor::PlaceableType::M2) directM2[o.path]++; + else if (o.type == wowee::editor::PlaceableType::WMO) directWMO[o.path]++; + } + } + int wobCount = 0; + std::error_code ec; + for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) { + if (!e.is_regular_file() || + e.path().extension() != ".wob") continue; + wobCount++; + std::string base = e.path().string(); + if (base.size() >= 4) base = base.substr(0, base.size() - 4); + auto bld = wowee::pipeline::WoweeBuildingLoader::load(base); + for (const auto& d : bld.doodads) { + if (!d.modelPath.empty()) doodadM2[d.modelPath]++; + } + } + // Resolve dep on disk. Same heuristic as --check-zone-refs: + // try both open + proprietary in conventional roots. + auto stripExt = [](const std::string& p, const char* ext) { + size_t n = std::strlen(ext); + if (p.size() >= n) { + std::string tail = p.substr(p.size() - n); + std::string lower = tail; + for (auto& c : lower) c = std::tolower(static_cast(c)); + if (lower == ext) return p.substr(0, p.size() - n); + } + return p; + }; + auto resolveStatus = [&](const std::string& path, bool isWMO) { + std::string base, openExt, propExt; + if (isWMO) { + base = stripExt(path, ".wmo"); + openExt = ".wob"; propExt = ".wmo"; + } else { + base = stripExt(path, ".m2"); + openExt = ".wom"; propExt = ".m2"; + } + std::vector roots = { + "", zoneDir + "/", "output/", "custom_zones/", "Data/" + }; + bool hasOpen = false, hasProp = false; + for (const auto& root : roots) { + if (fs::exists(root + base + openExt)) hasOpen = true; + if (fs::exists(root + base + propExt)) hasProp = true; + } + if (hasOpen && hasProp) return "open + proprietary"; + if (hasOpen) return "open only"; + if (hasProp) return "proprietary only"; + return "MISSING"; + }; + std::ofstream out(outPath); + if (!out) { + std::fprintf(stderr, + "export-zone-deps-md: cannot write %s\n", outPath.c_str()); + return 1; + } + out << "# Dependencies — " << + (zm.displayName.empty() ? zm.mapName : zm.displayName) << "\n\n"; + out << "*Auto-generated by `wowee_editor --export-zone-deps-md`. " + "Status is best-effort — checks zone-local, output/, " + "custom_zones/, Data/ roots in that order.*\n\n"; + auto emitTable = [&](const char* heading, + const std::map& m, + bool isWMO) { + out << "## " << heading << " (" << m.size() << ")\n\n"; + if (m.empty()) { + out << "*None.*\n\n"; + return; + } + out << "| Refs | Path | Status |\n"; + out << "|---:|---|---|\n"; + for (const auto& [path, count] : m) { + out << "| " << count << " | `" << path << "` | " + << resolveStatus(path, isWMO) << " |\n"; + } + out << "\n"; + }; + emitTable("Direct M2 placements", directM2, false); + emitTable("Direct WMO placements", directWMO, true); + emitTable("WOB doodad M2 refs", doodadM2, false); + out << "## Summary\n\n"; + out << "- Zone: `" << zm.mapName << "`\n"; + out << "- WOBs scanned: " << wobCount << "\n"; + out << "- Unique dependencies: " << + directM2.size() + directWMO.size() + doodadM2.size() << "\n"; + out.close(); + std::printf("Wrote %s\n", outPath.c_str()); + std::printf(" %zu M2 placements, %zu WMO placements, %zu WOB doodad refs\n", + directM2.size(), directWMO.size(), doodadM2.size()); + return 0; +} + +int handleExportZoneSpawnPng(int& i, int argc, char** argv) { + // Top-down PNG of spawn positions colored by type. Bound by + // the zone's tile range so the image is properly framed. + // Useful for design review (does the spawn distribution + // match the intended encounter design?) and for showing + // collaborators 'where are the mobs'. + std::string zoneDir = argv[++i]; + std::string outPath; + if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; + namespace fs = std::filesystem; + std::string manifestPath = zoneDir + "/zone.json"; + if (!fs::exists(manifestPath)) { + std::fprintf(stderr, + "export-zone-spawn-png: %s has no zone.json\n", zoneDir.c_str()); + return 1; + } + wowee::editor::ZoneManifest zm; + if (!zm.load(manifestPath)) { + std::fprintf(stderr, + "export-zone-spawn-png: parse failed\n"); + return 1; + } + if (zm.tiles.empty()) { + std::fprintf(stderr, "export-zone-spawn-png: zone has no tiles\n"); + return 1; + } + if (outPath.empty()) outPath = zoneDir + "/" + zm.mapName + "_spawns.png"; + // Compute world-space bounds from manifest tiles. Same math + // as --info-zone-extents. + constexpr float kTileSize = 533.33333f; + int tileMinX = 64, tileMaxX = -1; + int tileMinY = 64, tileMaxY = -1; + for (const auto& [tx, ty] : zm.tiles) { + tileMinX = std::min(tileMinX, tx); + tileMaxX = std::max(tileMaxX, tx); + tileMinY = std::min(tileMinY, ty); + tileMaxY = std::max(tileMaxY, ty); + } + float worldMinX = (32.0f - tileMaxY - 1) * kTileSize; + float worldMaxX = (32.0f - tileMinY) * kTileSize; + float worldMinY = (32.0f - tileMaxX - 1) * kTileSize; + float worldMaxY = (32.0f - tileMinX) * kTileSize; + // Image dimensions: 256px per tile so detail is visible + // without inflating per-pixel cost. + int tilesX = tileMaxY - tileMinY + 1; // tile.y maps to world.x + int tilesY = tileMaxX - tileMinX + 1; + const int kPxPerTile = 256; + int imgW = tilesX * kPxPerTile; + int imgH = tilesY * kPxPerTile; + // Cap output size — 16-tile-wide projects shouldn't exceed + // 4096 wide. Scale down if needed. + int maxDim = std::max(imgW, imgH); + if (maxDim > 4096) { + int divisor = (maxDim + 4095) / 4096; + imgW = std::max(64, imgW / divisor); + imgH = std::max(64, imgH / divisor); + } + std::vector img(imgW * imgH * 3, 32); // dark grey background + // Tile-grid lines so the boundary is visible. + for (int t = 1; t < tilesX; ++t) { + int x = (t * imgW) / tilesX; + if (x >= 0 && x < imgW) { + for (int y = 0; y < imgH; ++y) { + size_t off = (y * imgW + x) * 3; + img[off] = img[off+1] = img[off+2] = 64; + } + } + } + for (int t = 1; t < tilesY; ++t) { + int y = (t * imgH) / tilesY; + if (y >= 0 && y < imgH) { + for (int x = 0; x < imgW; ++x) { + size_t off = (y * imgW + x) * 3; + img[off] = img[off+1] = img[off+2] = 64; + } + } + } + // Plot spawn points. Map world (X, Y) to image (px, py): + // px = (worldMaxX - X) / (worldMaxX - worldMinX) * imgW + // py = (worldMaxY - Y) / (worldMaxY - worldMinY) * imgH + // since +X world is north (up) and +Y world is west (left) + // in WoW coords. + float wRangeX = worldMaxX - worldMinX; + float wRangeY = worldMaxY - worldMinY; + auto plotPoint = [&](float wx, float wy, uint8_t r, uint8_t g, uint8_t b) { + if (wRangeX <= 0 || wRangeY <= 0) return; + int px = static_cast((worldMaxX - wx) / wRangeX * imgW); + int py = static_cast((worldMaxY - wy) / wRangeY * imgH); + // 3×3 dot. + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + int x = px + dx, y = py + dy; + if (x < 0 || x >= imgW || y < 0 || y >= imgH) continue; + size_t off = (y * imgW + x) * 3; + img[off] = r; img[off+1] = g; img[off+2] = b; + } + } + }; + // Creatures = red. + wowee::editor::NpcSpawner sp; + int creaturesPlotted = 0; + if (sp.loadFromFile(zoneDir + "/creatures.json")) { + for (const auto& s : sp.getSpawns()) { + plotPoint(s.position.x, s.position.y, 220, 60, 60); + creaturesPlotted++; + } + } + // Objects = green (M2) / blue (WMO). + wowee::editor::ObjectPlacer op; + int objectsPlotted = 0; + if (op.loadFromFile(zoneDir + "/objects.json")) { + for (const auto& o : op.getObjects()) { + if (o.type == wowee::editor::PlaceableType::M2) { + plotPoint(o.position.x, o.position.y, 60, 200, 60); + } else { + plotPoint(o.position.x, o.position.y, 60, 120, 220); + } + objectsPlotted++; + } + } + if (!stbi_write_png(outPath.c_str(), imgW, imgH, 3, + img.data(), imgW * 3)) { + std::fprintf(stderr, + "export-zone-spawn-png: stbi_write_png failed\n"); + return 1; + } + std::printf("Wrote %s\n", outPath.c_str()); + std::printf(" %dx%d px, tile grid %dx%d, %d creatures (red), %d objects (green/blue)\n", + imgW, imgH, tilesX, tilesY, creaturesPlotted, objectsPlotted); + return 0; +} + + +} // namespace + +bool handleZoneExport(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--export-png") == 0 && i + 1 < argc) { + outRc = handleExportPng(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--export-zone-deps-md") == 0 && i + 1 < argc) { + outRc = handleExportZoneDepsMd(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--export-zone-spawn-png") == 0 && i + 1 < argc) { + outRc = handleExportZoneSpawnPng(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_zone_export.hpp b/tools/editor/cli_zone_export.hpp new file mode 100644 index 00000000..bd137cf4 --- /dev/null +++ b/tools/editor/cli_zone_export.hpp @@ -0,0 +1,19 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +// Dispatch the zone-level visual / report export handlers — +// render PNG previews of terrain, spawn distributions, and +// Markdown dependency reports. All three operate per-zone. +// --export-png heightmap + normals + zone-map PNG triplet +// --export-zone-deps-md GitHub-renderable model-dependency table +// --export-zone-spawn-png top-down spawn distribution PNG +// +// Returns true if matched; outRc holds the exit code. +bool handleZoneExport(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 357d7a29..f21522df 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -61,6 +61,7 @@ #include "cli_mesh_info.hpp" #include "cli_zone_data.hpp" #include "cli_project_actions.hpp" +#include "cli_zone_export.hpp" #include "content_pack.hpp" #include "npc_spawner.hpp" #include "object_placer.hpp" @@ -587,6 +588,9 @@ int main(int argc, char* argv[]) { if (wowee::editor::cli::handleProjectActions(i, argc, argv, outRc)) { return outRc; } + if (wowee::editor::cli::handleZoneExport(i, argc, argv, outRc)) { + return outRc; + } } if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) { dataPath = argv[++i]; @@ -594,277 +598,6 @@ int main(int argc, char* argv[]) { adtMap = argv[++i]; adtX = std::atoi(argv[++i]); adtY = std::atoi(argv[++i]); - } else if (std::strcmp(argv[i], "--export-png") == 0 && i + 1 < argc) { - // Render heightmap, normal-map, and zone-map PNG previews for a - // terrain. Useful for portfolio screenshots, ground-truth map - // comparison, and quick visual validation without launching GUI. - std::string base = argv[++i]; - for (const char* ext : {".wot", ".whm"}) { - if (base.size() >= 4 && base.substr(base.size() - 4) == ext) { - base = base.substr(0, base.size() - 4); - break; - } - } - if (!wowee::pipeline::WoweeTerrainLoader::exists(base)) { - std::fprintf(stderr, "WOT/WHM not found at base: %s\n", base.c_str()); - return 1; - } - wowee::pipeline::ADTTerrain terrain; - if (!wowee::pipeline::WoweeTerrainLoader::load(base, terrain)) { - std::fprintf(stderr, "Failed to load terrain: %s\n", base.c_str()); - return 1; - } - wowee::editor::WoweeTerrain::exportHeightmapPreview(terrain, base + "_heightmap.png"); - wowee::editor::WoweeTerrain::exportNormalMap(terrain, base + "_normals.png"); - wowee::editor::WoweeTerrain::exportZoneMap(terrain, base + "_zone.png", 512); - std::printf("Exported PNGs: %s_{heightmap,normals,zone}.png\n", base.c_str()); - return 0; - } else if (std::strcmp(argv[i], "--export-zone-deps-md") == 0 && i + 1 < argc) { - // Markdown counterpart to --list-zone-deps. Writes a sortable - // GitHub-rendered table of every external model the zone - // references plus on-disk presence (so PR reviewers see at a - // glance whether dependencies are accounted for in the - // accompanying asset bundle). - std::string zoneDir = argv[++i]; - std::string outPath; - if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; - namespace fs = std::filesystem; - if (!fs::exists(zoneDir + "/zone.json")) { - std::fprintf(stderr, - "export-zone-deps-md: %s has no zone.json\n", zoneDir.c_str()); - return 1; - } - wowee::editor::ZoneManifest zm; - zm.load(zoneDir + "/zone.json"); - if (outPath.empty()) outPath = zoneDir + "/DEPS.md"; - // Same dep-collection pass as --list-zone-deps. - std::map directM2; - std::map directWMO; - std::map doodadM2; - wowee::editor::ObjectPlacer op; - if (op.loadFromFile(zoneDir + "/objects.json")) { - for (const auto& o : op.getObjects()) { - if (o.type == wowee::editor::PlaceableType::M2) directM2[o.path]++; - else if (o.type == wowee::editor::PlaceableType::WMO) directWMO[o.path]++; - } - } - int wobCount = 0; - std::error_code ec; - for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) { - if (!e.is_regular_file() || - e.path().extension() != ".wob") continue; - wobCount++; - std::string base = e.path().string(); - if (base.size() >= 4) base = base.substr(0, base.size() - 4); - auto bld = wowee::pipeline::WoweeBuildingLoader::load(base); - for (const auto& d : bld.doodads) { - if (!d.modelPath.empty()) doodadM2[d.modelPath]++; - } - } - // Resolve dep on disk. Same heuristic as --check-zone-refs: - // try both open + proprietary in conventional roots. - auto stripExt = [](const std::string& p, const char* ext) { - size_t n = std::strlen(ext); - if (p.size() >= n) { - std::string tail = p.substr(p.size() - n); - std::string lower = tail; - for (auto& c : lower) c = std::tolower(static_cast(c)); - if (lower == ext) return p.substr(0, p.size() - n); - } - return p; - }; - auto resolveStatus = [&](const std::string& path, bool isWMO) { - std::string base, openExt, propExt; - if (isWMO) { - base = stripExt(path, ".wmo"); - openExt = ".wob"; propExt = ".wmo"; - } else { - base = stripExt(path, ".m2"); - openExt = ".wom"; propExt = ".m2"; - } - std::vector roots = { - "", zoneDir + "/", "output/", "custom_zones/", "Data/" - }; - bool hasOpen = false, hasProp = false; - for (const auto& root : roots) { - if (fs::exists(root + base + openExt)) hasOpen = true; - if (fs::exists(root + base + propExt)) hasProp = true; - } - if (hasOpen && hasProp) return "open + proprietary"; - if (hasOpen) return "open only"; - if (hasProp) return "proprietary only"; - return "MISSING"; - }; - std::ofstream out(outPath); - if (!out) { - std::fprintf(stderr, - "export-zone-deps-md: cannot write %s\n", outPath.c_str()); - return 1; - } - out << "# Dependencies — " << - (zm.displayName.empty() ? zm.mapName : zm.displayName) << "\n\n"; - out << "*Auto-generated by `wowee_editor --export-zone-deps-md`. " - "Status is best-effort — checks zone-local, output/, " - "custom_zones/, Data/ roots in that order.*\n\n"; - auto emitTable = [&](const char* heading, - const std::map& m, - bool isWMO) { - out << "## " << heading << " (" << m.size() << ")\n\n"; - if (m.empty()) { - out << "*None.*\n\n"; - return; - } - out << "| Refs | Path | Status |\n"; - out << "|---:|---|---|\n"; - for (const auto& [path, count] : m) { - out << "| " << count << " | `" << path << "` | " - << resolveStatus(path, isWMO) << " |\n"; - } - out << "\n"; - }; - emitTable("Direct M2 placements", directM2, false); - emitTable("Direct WMO placements", directWMO, true); - emitTable("WOB doodad M2 refs", doodadM2, false); - out << "## Summary\n\n"; - out << "- Zone: `" << zm.mapName << "`\n"; - out << "- WOBs scanned: " << wobCount << "\n"; - out << "- Unique dependencies: " << - directM2.size() + directWMO.size() + doodadM2.size() << "\n"; - out.close(); - std::printf("Wrote %s\n", outPath.c_str()); - std::printf(" %zu M2 placements, %zu WMO placements, %zu WOB doodad refs\n", - directM2.size(), directWMO.size(), doodadM2.size()); - return 0; - } else if (std::strcmp(argv[i], "--export-zone-spawn-png") == 0 && i + 1 < argc) { - // Top-down PNG of spawn positions colored by type. Bound by - // the zone's tile range so the image is properly framed. - // Useful for design review (does the spawn distribution - // match the intended encounter design?) and for showing - // collaborators 'where are the mobs'. - std::string zoneDir = argv[++i]; - std::string outPath; - if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; - namespace fs = std::filesystem; - std::string manifestPath = zoneDir + "/zone.json"; - if (!fs::exists(manifestPath)) { - std::fprintf(stderr, - "export-zone-spawn-png: %s has no zone.json\n", zoneDir.c_str()); - return 1; - } - wowee::editor::ZoneManifest zm; - if (!zm.load(manifestPath)) { - std::fprintf(stderr, - "export-zone-spawn-png: parse failed\n"); - return 1; - } - if (zm.tiles.empty()) { - std::fprintf(stderr, "export-zone-spawn-png: zone has no tiles\n"); - return 1; - } - if (outPath.empty()) outPath = zoneDir + "/" + zm.mapName + "_spawns.png"; - // Compute world-space bounds from manifest tiles. Same math - // as --info-zone-extents. - constexpr float kTileSize = 533.33333f; - int tileMinX = 64, tileMaxX = -1; - int tileMinY = 64, tileMaxY = -1; - for (const auto& [tx, ty] : zm.tiles) { - tileMinX = std::min(tileMinX, tx); - tileMaxX = std::max(tileMaxX, tx); - tileMinY = std::min(tileMinY, ty); - tileMaxY = std::max(tileMaxY, ty); - } - float worldMinX = (32.0f - tileMaxY - 1) * kTileSize; - float worldMaxX = (32.0f - tileMinY) * kTileSize; - float worldMinY = (32.0f - tileMaxX - 1) * kTileSize; - float worldMaxY = (32.0f - tileMinX) * kTileSize; - // Image dimensions: 256px per tile so detail is visible - // without inflating per-pixel cost. - int tilesX = tileMaxY - tileMinY + 1; // tile.y maps to world.x - int tilesY = tileMaxX - tileMinX + 1; - const int kPxPerTile = 256; - int imgW = tilesX * kPxPerTile; - int imgH = tilesY * kPxPerTile; - // Cap output size — 16-tile-wide projects shouldn't exceed - // 4096 wide. Scale down if needed. - int maxDim = std::max(imgW, imgH); - if (maxDim > 4096) { - int divisor = (maxDim + 4095) / 4096; - imgW = std::max(64, imgW / divisor); - imgH = std::max(64, imgH / divisor); - } - std::vector img(imgW * imgH * 3, 32); // dark grey background - // Tile-grid lines so the boundary is visible. - for (int t = 1; t < tilesX; ++t) { - int x = (t * imgW) / tilesX; - if (x >= 0 && x < imgW) { - for (int y = 0; y < imgH; ++y) { - size_t off = (y * imgW + x) * 3; - img[off] = img[off+1] = img[off+2] = 64; - } - } - } - for (int t = 1; t < tilesY; ++t) { - int y = (t * imgH) / tilesY; - if (y >= 0 && y < imgH) { - for (int x = 0; x < imgW; ++x) { - size_t off = (y * imgW + x) * 3; - img[off] = img[off+1] = img[off+2] = 64; - } - } - } - // Plot spawn points. Map world (X, Y) to image (px, py): - // px = (worldMaxX - X) / (worldMaxX - worldMinX) * imgW - // py = (worldMaxY - Y) / (worldMaxY - worldMinY) * imgH - // since +X world is north (up) and +Y world is west (left) - // in WoW coords. - float wRangeX = worldMaxX - worldMinX; - float wRangeY = worldMaxY - worldMinY; - auto plotPoint = [&](float wx, float wy, uint8_t r, uint8_t g, uint8_t b) { - if (wRangeX <= 0 || wRangeY <= 0) return; - int px = static_cast((worldMaxX - wx) / wRangeX * imgW); - int py = static_cast((worldMaxY - wy) / wRangeY * imgH); - // 3×3 dot. - for (int dy = -1; dy <= 1; ++dy) { - for (int dx = -1; dx <= 1; ++dx) { - int x = px + dx, y = py + dy; - if (x < 0 || x >= imgW || y < 0 || y >= imgH) continue; - size_t off = (y * imgW + x) * 3; - img[off] = r; img[off+1] = g; img[off+2] = b; - } - } - }; - // Creatures = red. - wowee::editor::NpcSpawner sp; - int creaturesPlotted = 0; - if (sp.loadFromFile(zoneDir + "/creatures.json")) { - for (const auto& s : sp.getSpawns()) { - plotPoint(s.position.x, s.position.y, 220, 60, 60); - creaturesPlotted++; - } - } - // Objects = green (M2) / blue (WMO). - wowee::editor::ObjectPlacer op; - int objectsPlotted = 0; - if (op.loadFromFile(zoneDir + "/objects.json")) { - for (const auto& o : op.getObjects()) { - if (o.type == wowee::editor::PlaceableType::M2) { - plotPoint(o.position.x, o.position.y, 60, 200, 60); - } else { - plotPoint(o.position.x, o.position.y, 60, 120, 220); - } - objectsPlotted++; - } - } - if (!stbi_write_png(outPath.c_str(), imgW, imgH, 3, - img.data(), imgW * 3)) { - std::fprintf(stderr, - "export-zone-spawn-png: stbi_write_png failed\n"); - return 1; - } - std::printf("Wrote %s\n", outPath.c_str()); - std::printf(" %dx%d px, tile grid %dx%d, %d creatures (red), %d objects (green/blue)\n", - imgW, imgH, tilesX, tilesY, creaturesPlotted, objectsPlotted); - return 0; } else if (std::strcmp(argv[i], "--version") == 0 || std::strcmp(argv[i], "-v") == 0) { std::printf("Wowee World Editor v1.0.0\n"); std::printf("Open formats: WOT/WHM/WOM/WOB/WOC/WCP + PNG/JSON (all novel)\n");