diff --git a/CMakeLists.txt b/CMakeLists.txt index 621cdc36..b4a34447 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -698,6 +698,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_pet_talents.cpp src/pipeline/wowee_heroic_scaling.cpp src/pipeline/wowee_reputation_rewards.cpp + src/pipeline/wowee_minimap_levels.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1559,6 +1560,7 @@ add_executable(wowee_editor tools/editor/cli_pet_talents_catalog.cpp tools/editor/cli_heroic_scaling_catalog.cpp tools/editor/cli_reputation_rewards_catalog.cpp + tools/editor/cli_minimap_levels_catalog.cpp tools/editor/cli_catalog_pluck.cpp tools/editor/cli_catalog_find.cpp tools/editor/cli_catalog_by_name.cpp @@ -1738,6 +1740,7 @@ add_executable(wowee_editor src/pipeline/wowee_pet_talents.cpp src/pipeline/wowee_heroic_scaling.cpp src/pipeline/wowee_reputation_rewards.cpp + src/pipeline/wowee_minimap_levels.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_minimap_levels.hpp b/include/pipeline/wowee_minimap_levels.hpp new file mode 100644 index 00000000..057ab9bc --- /dev/null +++ b/include/pipeline/wowee_minimap_levels.hpp @@ -0,0 +1,118 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Minimap Multi-Level catalog (.wmnl) — +// novel replacement for the WorldMapTransforms.dbc + +// WorldMapOverlay.dbc pair that vanilla WoW used to +// describe zones with multiple vertical layers visible +// on the minimap (Stormwind has Old Town / Cathedral / +// Keep visible at separate altitudes; Dalaran has +// Sewers / Street / Above Street / Floating; Undercity +// has Throne / Inner Ring / Outer Ring / Canal / Sewer). +// Each entry binds one (mapId, areaId, levelIndex) +// triplet to its Z-range, minimap texture, and display +// label. +// +// Cross-references with previously-added formats: +// WMS: mapId references the WMS map catalog; +// areaId references the WMS sub-area entry. +// WMPX: WMNL acts as a per-level overlay on top of +// the per-zone WMPX world-map mapping; if a +// player's Z falls within a WMNL entry's +// range, the WMPX overlay layer texture +// switches to the WMNL.texturePath. +// +// Binary layout (little-endian): +// magic[4] = "WMNL" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// levelId (uint32) +// nameLen + name +// descLen + description +// mapId (uint32) / areaId (uint32) +// levelIndex (uint8) — 0=ground, 1=upper, +// 2=second-upper, etc. +// pad0 (uint8) / pad1 (uint8) / pad2 (uint8) +// minZ (float) / maxZ (float) — world units +// pathLen + texturePath — minimap layer +// texture (BLP/WOT) +// labelLen + displayName — UI label, e.g. +// "Ground Floor" +// iconColorRGBA (uint32) +struct WoweeMinimapLevels { + struct Entry { + uint32_t levelId = 0; + std::string name; + std::string description; + uint32_t mapId = 0; + uint32_t areaId = 0; + uint8_t levelIndex = 0; + uint8_t pad0 = 0; + uint8_t pad1 = 0; + uint8_t pad2 = 0; + float minZ = 0.0f; + float maxZ = 0.0f; + std::string texturePath; + std::string displayName; + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t levelId) const; + + // Returns the level entry whose Z-range contains the + // player's current Z position, for the given (map, + // area). Used by the minimap renderer at every camera + // tick to swap the overlay layer when the player + // crosses a floor boundary. + const Entry* findContainingZ(uint32_t mapId, + uint32_t areaId, + float z) const; + + // Returns all levels for one (map, area) sorted by + // levelIndex. Used by the world-map UI to populate + // the per-zone level-picker dropdown. + std::vector findByArea(uint32_t mapId, + uint32_t areaId) const; +}; + +class WoweeMinimapLevelsLoader { +public: + static bool save(const WoweeMinimapLevels& cat, + const std::string& basePath); + static WoweeMinimapLevels load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-mnl* variants. + // + // makeStormwind — 3 levels (Old Town / Cathedral + // District / Stormwind Keep + // Throne Room) covering the + // city's vertical extent at + // mapId 0 / areaId 1519. + // makeDalaran — 4 levels (Sewers / Street / + // Above Street / Floating + // Cathedral) — most vertical + // city in Northrend. + // makeUndercity — 5 levels (Throne / Inner Ring / + // Outer Ring / Canal / Sewer) — + // deepest vertical-layer city. + static WoweeMinimapLevels makeStormwind(const std::string& catalogName); + static WoweeMinimapLevels makeDalaran(const std::string& catalogName); + static WoweeMinimapLevels makeUndercity(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_minimap_levels.cpp b/src/pipeline/wowee_minimap_levels.cpp new file mode 100644 index 00000000..3cf4b5ab --- /dev/null +++ b/src/pipeline/wowee_minimap_levels.cpp @@ -0,0 +1,317 @@ +#include "pipeline/wowee_minimap_levels.hpp" + +#include +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'M', 'N', 'L'}; +constexpr uint32_t kVersion = 1; + +template +void writePOD(std::ofstream& os, const T& v) { + os.write(reinterpret_cast(&v), sizeof(T)); +} + +template +bool readPOD(std::ifstream& is, T& v) { + is.read(reinterpret_cast(&v), sizeof(T)); + return is.gcount() == static_cast(sizeof(T)); +} + +void writeStr(std::ofstream& os, const std::string& s) { + uint32_t n = static_cast(s.size()); + writePOD(os, n); + if (n > 0) os.write(s.data(), n); +} + +bool readStr(std::ifstream& is, std::string& s) { + uint32_t n = 0; + if (!readPOD(is, n)) return false; + if (n > (1u << 20)) return false; + s.resize(n); + if (n > 0) { + is.read(s.data(), n); + if (is.gcount() != static_cast(n)) { + s.clear(); + return false; + } + } + return true; +} + +std::string normalizePath(std::string base) { + if (base.size() < 5 || base.substr(base.size() - 5) != ".wmnl") { + base += ".wmnl"; + } + return base; +} + +uint32_t packRgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0xFF) { + return (static_cast(a) << 24) | + (static_cast(b) << 16) | + (static_cast(g) << 8) | + static_cast(r); +} + +} // namespace + +const WoweeMinimapLevels::Entry* +WoweeMinimapLevels::findById(uint32_t levelId) const { + for (const auto& e : entries) + if (e.levelId == levelId) return &e; + return nullptr; +} + +const WoweeMinimapLevels::Entry* +WoweeMinimapLevels::findContainingZ(uint32_t mapId, + uint32_t areaId, + float z) const { + for (const auto& e : entries) { + if (e.mapId != mapId || e.areaId != areaId) continue; + if (z >= e.minZ && z < e.maxZ) return &e; + } + return nullptr; +} + +std::vector +WoweeMinimapLevels::findByArea(uint32_t mapId, + uint32_t areaId) const { + std::vector out; + for (const auto& e : entries) { + if (e.mapId == mapId && e.areaId == areaId) + out.push_back(&e); + } + std::sort(out.begin(), out.end(), + [](const Entry* a, const Entry* b) { + return a->levelIndex < b->levelIndex; + }); + return out; +} + +bool WoweeMinimapLevelsLoader::save(const WoweeMinimapLevels& cat, + const std::string& basePath) { + std::ofstream os(normalizePath(basePath), std::ios::binary); + if (!os) return false; + os.write(kMagic, 4); + writePOD(os, kVersion); + writeStr(os, cat.name); + uint32_t entryCount = static_cast(cat.entries.size()); + writePOD(os, entryCount); + for (const auto& e : cat.entries) { + writePOD(os, e.levelId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.mapId); + writePOD(os, e.areaId); + writePOD(os, e.levelIndex); + writePOD(os, e.pad0); + writePOD(os, e.pad1); + writePOD(os, e.pad2); + writePOD(os, e.minZ); + writePOD(os, e.maxZ); + writeStr(os, e.texturePath); + writeStr(os, e.displayName); + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweeMinimapLevels WoweeMinimapLevelsLoader::load( + const std::string& basePath) { + WoweeMinimapLevels out; + std::ifstream is(normalizePath(basePath), std::ios::binary); + if (!is) return out; + char magic[4]; + is.read(magic, 4); + if (std::memcmp(magic, kMagic, 4) != 0) return out; + uint32_t version = 0; + if (!readPOD(is, version) || version != kVersion) return out; + if (!readStr(is, out.name)) return out; + uint32_t entryCount = 0; + if (!readPOD(is, entryCount)) return out; + if (entryCount > (1u << 20)) return out; + out.entries.resize(entryCount); + for (auto& e : out.entries) { + if (!readPOD(is, e.levelId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.mapId) || + !readPOD(is, e.areaId) || + !readPOD(is, e.levelIndex) || + !readPOD(is, e.pad0) || + !readPOD(is, e.pad1) || + !readPOD(is, e.pad2) || + !readPOD(is, e.minZ) || + !readPOD(is, e.maxZ)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.texturePath) || + !readStr(is, e.displayName)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeMinimapLevelsLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeMinimapLevels WoweeMinimapLevelsLoader::makeStormwind( + const std::string& catalogName) { + using M = WoweeMinimapLevels; + WoweeMinimapLevels c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t levelIndex, float minZ, float maxZ, + const char* texPath, const char* displayName, + const char* desc) { + M::Entry e; + e.levelId = id; e.name = name; e.description = desc; + e.mapId = 0; // Eastern Kingdoms + e.areaId = 1519; // Stormwind City + e.levelIndex = levelIndex; + e.minZ = minZ; e.maxZ = maxZ; + e.texturePath = texPath; + e.displayName = displayName; + e.iconColorRGBA = packRgba(140, 200, 255); // city blue + c.entries.push_back(e); + }; + add(1, "StormwindOldTown", 0, 0.0f, 80.0f, + "Interface\\Minimap\\Stormwind\\OldTown.blp", + "Old Town & Cathedral District", + "Stormwind ground level — Z 0-80. Old Town, " + "Trade District, Cathedral District, Mage Quarter " + "are all at this base elevation."); + add(2, "StormwindKeep", 1, 80.0f, 130.0f, + "Interface\\Minimap\\Stormwind\\KeepRamp.blp", + "Stormwind Keep Approach", + "Stormwind Keep ramp + outer courtyard — Z " + "80-130. Players above this elevation see the " + "keep-approach overlay."); + add(3, "StormwindThroneRoom", 2, 130.0f, 200.0f, + "Interface\\Minimap\\Stormwind\\ThroneRoom.blp", + "Throne Room & Royal Library", + "Stormwind Keep upper level (Throne Room, Royal " + "Library, secret tunnel) — Z 130-200. Highest " + "elevation in city map."); + return c; +} + +WoweeMinimapLevels WoweeMinimapLevelsLoader::makeDalaran( + const std::string& catalogName) { + using M = WoweeMinimapLevels; + WoweeMinimapLevels c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t levelIndex, float minZ, float maxZ, + const char* texPath, const char* displayName, + const char* desc) { + M::Entry e; + e.levelId = id; e.name = name; e.description = desc; + e.mapId = 571; // Northrend + e.areaId = 4395; // Dalaran + e.levelIndex = levelIndex; + e.minZ = minZ; e.maxZ = maxZ; + e.texturePath = texPath; + e.displayName = displayName; + e.iconColorRGBA = packRgba(180, 140, 220); // arcane purple + c.entries.push_back(e); + }; + add(100, "DalaranSewers", 0, 0.0f, 580.0f, + "Interface\\Minimap\\Dalaran\\Sewers.blp", + "The Underbelly", + "Dalaran sewer level (The Underbelly) — Z " + "0-580. Free-for-all PvP zone with rogue/druid " + "stash."); + add(101, "DalaranStreet", 1, 580.0f, 645.0f, + "Interface\\Minimap\\Dalaran\\Street.blp", + "Dalaran Street Level", + "Dalaran main street — Z 580-645. Most " + "vendors, Krasus' Landing, the Wonderworks. " + "Default level players spawn into."); + add(102, "DalaranAboveStreet", 2, 645.0f, 700.0f, + "Interface\\Minimap\\Dalaran\\AboveStreet.blp", + "Above Street", + "Dalaran upper buildings (rooftops, arcane " + "tower walkways) — Z 645-700. Reached via the " + "few rooftop ladders."); + add(103, "DalaranFloatingCathedral", 3, 700.0f, 800.0f, + "Interface\\Minimap\\Dalaran\\Cathedral.blp", + "Floating Cathedral", + "Dalaran top-tier (Violet Citadel + Floating " + "Cathedral) — Z 700-800. Highest reachable " + "point in the city."); + return c; +} + +WoweeMinimapLevels WoweeMinimapLevelsLoader::makeUndercity( + const std::string& catalogName) { + using M = WoweeMinimapLevels; + WoweeMinimapLevels c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t levelIndex, float minZ, float maxZ, + const char* texPath, const char* displayName, + const char* desc) { + M::Entry e; + e.levelId = id; e.name = name; e.description = desc; + e.mapId = 0; // Eastern Kingdoms + e.areaId = 1497; // Undercity + e.levelIndex = levelIndex; + e.minZ = minZ; e.maxZ = maxZ; + e.texturePath = texPath; + e.displayName = displayName; + e.iconColorRGBA = packRgba(80, 60, 140); // forsaken indigo + c.entries.push_back(e); + }; + // Undercity is the most vertically layered Eastern + // Kingdoms capital — has 5 distinct minimap layers. + add(200, "UndercitySewer", 0, -110.0f, -85.0f, + "Interface\\Minimap\\Undercity\\Sewer.blp", + "Sewer Outlet", + "Undercity sewer outlet — Z -110 to -85. The " + "lowest reachable point, where Tirisfal water " + "drains."); + add(201, "UndercityCanal", 1, -85.0f, -65.0f, + "Interface\\Minimap\\Undercity\\Canal.blp", + "Canal Walkway", + "Undercity canal walkway — Z -85 to -65. " + "Connects the Trade Quarter to the apothecary " + "labs via narrow walkways."); + add(202, "UndercityOuterRing", 2, -65.0f, -45.0f, + "Interface\\Minimap\\Undercity\\OuterRing.blp", + "Outer Ring", + "Undercity outer ring — Z -65 to -45. The " + "Trade Quarter, mailbox, auction house. Default " + "Forsaken arrival point."); + add(203, "UndercityInnerRing", 3, -45.0f, -20.0f, + "Interface\\Minimap\\Undercity\\InnerRing.blp", + "Inner Ring", + "Undercity inner ring — Z -45 to -20. The four " + "racial-trainer alcoves, Magic Quarter, " + "Apothecarium ramp."); + add(204, "UndercityThroneRoom", 4, -20.0f, 30.0f, + "Interface\\Minimap\\Undercity\\Throne.blp", + "Royal Quarter", + "Undercity Royal Quarter (Throne Room) — Z " + "-20 to 30. Sylvanas Windrunner's seat. Highest " + "elevation, opens via the central elevator."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index cb78bbff..4538218e 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -337,6 +337,8 @@ const char* const kArgRequired[] = { "--gen-rpr", "--gen-rpr-kaluak", "--gen-rpr-accord", "--info-wrpr", "--validate-wrpr", "--export-wrpr-json", "--import-wrpr-json", + "--gen-mnl", "--gen-mnl-dalaran", "--gen-mnl-undercity", + "--info-wmnl", "--validate-wmnl", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index 4fd1e94a..b76b2b05 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -154,6 +154,7 @@ #include "cli_pet_talents_catalog.hpp" #include "cli_heroic_scaling_catalog.hpp" #include "cli_reputation_rewards_catalog.hpp" +#include "cli_minimap_levels_catalog.hpp" #include "cli_catalog_pluck.hpp" #include "cli_catalog_find.hpp" #include "cli_catalog_by_name.hpp" @@ -352,6 +353,7 @@ constexpr DispatchFn kDispatchTable[] = { handlePetTalentsCatalog, handleHeroicScalingCatalog, handleReputationRewardsCatalog, + handleMinimapLevelsCatalog, handleCatalogPluck, handleCatalogFind, handleCatalogByName, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 19407c3d..f80064eb 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -112,6 +112,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','P','T','T'}, ".wptt", "pets", "--info-wptt", "Hunter pet talent tree catalog"}, {{'W','H','R','D'}, ".whrd", "raid", "--info-whrd", "Heroic loot scaling catalog"}, {{'W','R','P','R'}, ".wrpr", "factions", "--info-wrpr", "Reputation reward tier catalog"}, + {{'W','M','N','L'}, ".wmnl", "worldmap", "--info-wmnl", "Minimap multi-level catalog"}, {{'W','F','A','C'}, ".wfac", "factions", nullptr, "Faction catalog"}, {{'W','L','C','K'}, ".wlck", "locks", nullptr, "Lock catalog"}, {{'W','S','K','L'}, ".wskl", "skills", nullptr, "Skill catalog"}, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 0c0986fa..f783a42e 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -2293,6 +2293,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wrpr to a human-editable JSON sidecar (defaults to .wrpr.json; emits both unlockedItemIds and unlockedRecipeIds as JSON arrays of IDs; standingTier derived label \"Friendly/Honored/Revered/Exalted\" added for readability)\n"); std::printf(" --import-wrpr-json [out-base]\n"); std::printf(" Import a .wrpr.json sidecar back into binary .wrpr (grantsTabard/grantsMount accept bool OR int; both unlock arrays serialize as plain JSON int arrays; standingTier label is informational only — minStanding int is authoritative)\n"); + std::printf(" --gen-mnl [name]\n"); + std::printf(" Emit .wmnl 3 Stormwind minimap levels (Old Town Z 0-80 / Keep Approach Z 80-130 / Throne Room Z 130-200) at mapId 0 / areaId 1519\n"); + std::printf(" --gen-mnl-dalaran [name]\n"); + std::printf(" Emit .wmnl 4 Dalaran minimap levels (Sewers / Street / Above Street / Floating Cathedral) — Wrath's most vertical city\n"); + std::printf(" --gen-mnl-undercity [name]\n"); + std::printf(" Emit .wmnl 5 Undercity minimap levels (Sewer / Canal / Outer Ring / Inner Ring / Throne Room) — deepest layered city in EK\n"); + std::printf(" --info-wmnl [--json]\n"); + std::printf(" Print WMNL entries (id / map / area / levelIndex / Z-range / displayName / name)\n"); + std::printf(" --validate-wmnl [--json]\n"); + std::printf(" Static checks: id+name+areaId required, minZ [--json]\n"); std::printf(" Extract one entry by id from any registered catalog format. Auto-detects magic, dispatches to the per-format --info-* handler internally, then prints just the matching entry. Primary-key field is auto-detected (first *Id field, or first numeric)\n"); std::printf(" --catalog-find [--magic ] [--json]\n"); diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index 4d1b2dfd..9849a226 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -134,6 +134,7 @@ constexpr FormatRow kFormats[] = { {"WPTT", ".wptt", "pets", "PetTalent.dbc + PetTalentTab.dbc", "Hunter pet talent tree catalog (3 trees, grid+graph)"}, {"WHRD", ".whrd", "raid", "implicit Heroic-mode loot scaling", "Heroic loot scaling catalog (per instance+difficulty)"}, {"WRPR", ".wrpr", "factions", "npc_vendor reqstanding + rep gates", "Reputation reward tier catalog (per faction)"}, + {"WMNL", ".wmnl", "worldmap", "WorldMapTransforms.dbc + Overlay", "Minimap multi-level catalog (vertical zones)"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine diff --git a/tools/editor/cli_minimap_levels_catalog.cpp b/tools/editor/cli_minimap_levels_catalog.cpp new file mode 100644 index 00000000..a050a897 --- /dev/null +++ b/tools/editor/cli_minimap_levels_catalog.cpp @@ -0,0 +1,303 @@ +#include "cli_minimap_levels_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_minimap_levels.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWmnlExt(std::string base) { + stripExt(base, ".wmnl"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeMinimapLevels& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeMinimapLevelsLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wmnl\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeMinimapLevels& c, + const std::string& base) { + std::printf("Wrote %s.wmnl\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" levels : %zu\n", c.entries.size()); +} + +int handleGenStormwind(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StormwindMinimapLevels"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWmnlExt(base); + auto c = wowee::pipeline::WoweeMinimapLevelsLoader::makeStormwind(name); + if (!saveOrError(c, base, "gen-mnl")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenDalaran(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "DalaranMinimapLevels"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWmnlExt(base); + auto c = wowee::pipeline::WoweeMinimapLevelsLoader::makeDalaran(name); + if (!saveOrError(c, base, "gen-mnl-dalaran")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenUndercity(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "UndercityMinimapLevels"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWmnlExt(base); + auto c = wowee::pipeline::WoweeMinimapLevelsLoader::makeUndercity(name); + if (!saveOrError(c, base, "gen-mnl-undercity")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleInfo(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWmnlExt(base); + if (!wowee::pipeline::WoweeMinimapLevelsLoader::exists(base)) { + std::fprintf(stderr, "WMNL not found: %s.wmnl\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeMinimapLevelsLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wmnl"] = base + ".wmnl"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"levelId", e.levelId}, + {"name", e.name}, + {"description", e.description}, + {"mapId", e.mapId}, + {"areaId", e.areaId}, + {"levelIndex", e.levelIndex}, + {"minZ", e.minZ}, + {"maxZ", e.maxZ}, + {"texturePath", e.texturePath}, + {"displayName", e.displayName}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WMNL: %s.wmnl\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" levels : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id map area idx minZ maxZ name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %4u %4u %2u %7.1f %7.1f %s\n", + e.levelId, e.mapId, e.areaId, + e.levelIndex, e.minZ, e.maxZ, + e.displayName.c_str()); + std::printf(" %s\n", e.name.c_str()); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWmnlExt(base); + if (!wowee::pipeline::WoweeMinimapLevelsLoader::exists(base)) { + std::fprintf(stderr, + "validate-wmnl: WMNL not found: %s.wmnl\n", + base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeMinimapLevelsLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::set idsSeen; + // Group entries by (mapId, areaId) so we can check + // per-area constraints (Z-range overlap, levelIndex + // gap). + std::map> + byArea; + for (size_t k = 0; k < c.entries.size(); ++k) { + const auto& e = c.entries[k]; + std::string ctx = "entry " + std::to_string(k) + + " (id=" + std::to_string(e.levelId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.levelId == 0) + errors.push_back(ctx + ": levelId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.areaId == 0) { + errors.push_back(ctx + + ": areaId is 0 — level is unbound to " + "any WMS sub-area"); + } + if (e.minZ >= e.maxZ) { + errors.push_back(ctx + ": minZ " + + std::to_string(e.minZ) + + " >= maxZ " + std::to_string(e.maxZ) + + " — Z-range is empty or inverted"); + } + if (e.texturePath.empty()) { + warnings.push_back(ctx + + ": texturePath is empty — minimap " + "overlay layer would render untextured"); + } + if (e.displayName.empty()) { + warnings.push_back(ctx + + ": displayName is empty — UI level " + "picker would show blank entry"); + } + if (!idsSeen.insert(e.levelId).second) { + errors.push_back(ctx + ": duplicate levelId"); + } + if (e.areaId != 0) { + uint64_t key = (static_cast(e.mapId) + << 32) | e.areaId; + byArea[key].push_back(&e); + } + } + // Per-area cross-checks: levelIndex uniqueness and + // Z-range non-overlap. Two levels at the same + // levelIndex would confuse the picker UI; overlapping + // Z-ranges would cause minimap-flicker as the player + // crosses the overlap region. + for (auto& [key, levels] : byArea) { + if (levels.size() < 2) continue; + uint32_t mapId = static_cast(key >> 32); + uint32_t areaId = static_cast(key & 0xFFFFFFFFu); + std::sort(levels.begin(), levels.end(), + [](auto* a, auto* b) { + return a->levelIndex < b->levelIndex; + }); + std::set indicesSeen; + for (const auto* L : levels) { + if (!indicesSeen.insert(L->levelIndex).second) { + errors.push_back("area (mapId=" + + std::to_string(mapId) + ", areaId=" + + std::to_string(areaId) + + "): two levels at levelIndex " + + std::to_string(L->levelIndex) + + " — picker would show duplicate slot"); + } + } + // Z-overlap check: for every pair of levels in + // this area, their [minZ, maxZ) intervals must + // not overlap. + for (size_t a = 0; a < levels.size(); ++a) { + for (size_t b = a + 1; b < levels.size(); ++b) { + const auto* La = levels[a]; + const auto* Lb = levels[b]; + if (La->minZ < Lb->maxZ && + Lb->minZ < La->maxZ) { + errors.push_back("area (mapId=" + + std::to_string(mapId) + + ", areaId=" + + std::to_string(areaId) + + "): Z-range overlap between " + "levelIndex " + + std::to_string(La->levelIndex) + + " (Z " + + std::to_string(La->minZ) + "-" + + std::to_string(La->maxZ) + + ") and levelIndex " + + std::to_string(Lb->levelIndex) + + " (Z " + + std::to_string(Lb->minZ) + "-" + + std::to_string(Lb->maxZ) + + ") — minimap renderer would " + "flicker between layers in the " + "overlap region"); + } + } + } + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wmnl"] = base + ".wmnl"; + j["ok"] = ok; + j["errors"] = errors; + j["warnings"] = warnings; + std::printf("%s\n", j.dump(2).c_str()); + return ok ? 0 : 1; + } + std::printf("validate-wmnl: %s.wmnl\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu levels, all levelIds + " + "per-area levelIndices unique, no " + "Z-range overlaps\n", + c.entries.size()); + return 0; + } + if (!warnings.empty()) { + std::printf(" warnings (%zu):\n", warnings.size()); + for (const auto& w : warnings) + std::printf(" - %s\n", w.c_str()); + } + if (!errors.empty()) { + std::printf(" ERRORS (%zu):\n", errors.size()); + for (const auto& e : errors) + std::printf(" - %s\n", e.c_str()); + } + return ok ? 0 : 1; +} + +} // namespace + +bool handleMinimapLevelsCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-mnl") == 0 && i + 1 < argc) { + outRc = handleGenStormwind(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-mnl-dalaran") == 0 && + i + 1 < argc) { + outRc = handleGenDalaran(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-mnl-undercity") == 0 && + i + 1 < argc) { + outRc = handleGenUndercity(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wmnl") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wmnl") == 0 && i + 1 < argc) { + outRc = handleValidate(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_minimap_levels_catalog.hpp b/tools/editor/cli_minimap_levels_catalog.hpp new file mode 100644 index 00000000..7badc9be --- /dev/null +++ b/tools/editor/cli_minimap_levels_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleMinimapLevelsCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee