From 82a8c3559e7f025b176f8715b30e13eccde19d30 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 16:40:00 -0700 Subject: [PATCH] feat(pipeline): add WMS (Wowee Map / Area) catalog format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Novel open replacement for Blizzard's Map.dbc + AreaTable.dbc + the AzerothCore-style world_zone SQL tables. The 26th open format added to the editor. Defines two related kinds of locator in one catalog: • Maps — top-level worlds (continents / instances / raids / battlegrounds / arenas) with a friendly name, type, expansion tag, and player-count cap. • Areas — sub-zones within maps with friendly names, parent- area chain, recommended level range, faction- territory marker (alliance / horde / contested / both), exploration XP, and an ambient-sound cross-reference into WSND. The runtime uses Areas for minimap labels, location strings under the player frame, "Discover Sub-zone" XP gains, and ambient-music selection on zone entry. Cross-references with previously-added formats: WMS.area.ambienceSoundId -> WSND.entry.soundId WMS.area.parentAreaId -> WMS.area.areaId (intra-format sub-zone hierarchy) WSPN entries are tied to WMS.area boundaries by world position (no direct ID — the runtime resolves position -> area at lookup time) Format: • magic "WMSX", version 1, little-endian • maps[] (each): mapId / name / shortName / mapType / expansionId / maxPlayers • areas[] (each): areaId / mapId / parentAreaId / name / minLevel..maxLevel / factionGroup / explorationXP / ambienceSoundId Enums: • MapType (5): Continent / Instance / Raid / Battleground / Arena • ExpansionId (5): Classic / Tbc / Wotlk / Cata / Mop • FactionGroup: Both / Alliance / Horde / Contested (PvP-flagging zone) API: WoweeMapsLoader::save / load / exists + WoweeMaps::findMap / findArea. Three preset emitters showcase the catalog shape: • makeStarter — 1 continent + 3 areas with parent chain (Goldshire is a sub-zone of Elwynn Forest) • makeClassic — 2 continents + Deadmines instance + 6 areas (Stormwind/Elwynn/Goldshire/Westfall/ Duskwood/Teldrassil/Deadmines) with WSND ambient-sound refs • makeBgArena — Alterac Valley (40-player BG) + Nagrand Arena (5v5 with maxPlayers=10) CLI added (5 flags, 578 documented total now): --gen-maps / --gen-maps-classic / --gen-maps-bgarena --info-wms / --validate-wms Validator catches: empty map name, unknown mapType / expansion, BG/Arena with maxPlayers=0 (no participant cap), area ids=0 + duplicates, empty area name, maxLevel < minLevel, areas referencing non-existent maps, parentAreaId chains crossing maps (sub-zones must be on the same world), self-parent. --- CMakeLists.txt | 3 + include/pipeline/wowee_maps.hpp | 137 +++++++++++++ src/pipeline/wowee_maps.cpp | 318 ++++++++++++++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_help.cpp | 10 + tools/editor/cli_maps_catalog.cpp | 307 ++++++++++++++++++++++++++++ tools/editor/cli_maps_catalog.hpp | 11 ++ 8 files changed, 790 insertions(+) create mode 100644 include/pipeline/wowee_maps.hpp create mode 100644 src/pipeline/wowee_maps.cpp create mode 100644 tools/editor/cli_maps_catalog.cpp create mode 100644 tools/editor/cli_maps_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index febc1e5a..5db0dabe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -613,6 +613,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_gossip.cpp src/pipeline/wowee_taxi.cpp src/pipeline/wowee_talents.cpp + src/pipeline/wowee_maps.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1372,6 +1373,7 @@ add_executable(wowee_editor tools/editor/cli_gossip_catalog.cpp tools/editor/cli_taxi_catalog.cpp tools/editor/cli_talents_catalog.cpp + tools/editor/cli_maps_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1463,6 +1465,7 @@ add_executable(wowee_editor src/pipeline/wowee_gossip.cpp src/pipeline/wowee_taxi.cpp src/pipeline/wowee_talents.cpp + src/pipeline/wowee_maps.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_maps.hpp b/include/pipeline/wowee_maps.hpp new file mode 100644 index 00000000..553eca47 --- /dev/null +++ b/include/pipeline/wowee_maps.hpp @@ -0,0 +1,137 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Map / Area catalog (.wms) — novel replacement +// for Blizzard's Map.dbc + AreaTable.dbc + the AzerothCore- +// style world_zone SQL tables. The 26th open format added +// to the editor. +// +// Defines two related kinds of locator: +// • Maps — top-level worlds (continents, instances, BGs). +// Each map has a friendly name, type, expansion +// tag, and player-count cap. +// • Areas — sub-zones within maps with friendly names, +// parent-area chain, recommended level range, +// faction-territory marker, exploration XP, and +// an ambient-sound cross-reference into WSND. +// +// One file holds both arrays. The runtime uses Areas for +// minimap labels, location strings under the player frame, +// "Discover Sub-zone" XP gains, and ambient music selection. +// +// Cross-references with previously-added formats: +// WMS.area.ambienceSoundId → WSND.entry.soundId +// WMS.area.parentAreaId → WMS.area.areaId (intra-format) +// WSPN entries are tied to WMS.area boundaries by +// world position (no direct id) +// +// Binary layout (little-endian): +// magic[4] = "WMSX" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// mapCount (uint32) +// maps (each): +// mapId (uint32) +// nameLen + name +// shortLen + shortName +// mapType (uint8) / expansionId (uint8) / pad[2] +// maxPlayers (uint16) / pad[2] +// areaCount (uint32) +// areas (each): +// areaId (uint32) +// mapId (uint32) +// parentAreaId (uint32) +// nameLen + name +// minLevel (uint16) / maxLevel (uint16) +// factionGroup (uint8) / pad[3] +// explorationXP (uint32) +// ambienceSoundId (uint32) +struct WoweeMaps { + enum MapType : uint8_t { + Continent = 0, + Instance = 1, + Raid = 2, + Battleground = 3, + Arena = 4, + }; + + enum ExpansionId : uint8_t { + Classic = 0, + Tbc = 1, + Wotlk = 2, + Cata = 3, + Mop = 4, + }; + + enum FactionGroup : uint8_t { + FactionBoth = 0, + FactionAlliance = 1, + FactionHorde = 2, + FactionContested = 3, // PvP-flagging zone + }; + + struct Map { + uint32_t mapId = 0; + std::string name; + std::string shortName; // e.g. "EK", "Kalim", "DM" + uint8_t mapType = Continent; + uint8_t expansionId = Classic; + uint16_t maxPlayers = 0; // 0 = unlimited (continent) + }; + + struct Area { + uint32_t areaId = 0; + uint32_t mapId = 0; + uint32_t parentAreaId = 0; // 0 = top-level + std::string name; + uint16_t minLevel = 1; + uint16_t maxLevel = 1; + uint8_t factionGroup = FactionBoth; + uint32_t explorationXP = 0; + uint32_t ambienceSoundId = 0; // WSND cross-ref, 0 = none + }; + + std::string name; + std::vector maps; + std::vector areas; + + bool isValid() const { return !maps.empty(); } + + const Map* findMap(uint32_t mapId) const; + const Area* findArea(uint32_t areaId) const; + + static const char* mapTypeName(uint8_t t); + static const char* expansionName(uint8_t e); + static const char* factionGroupName(uint8_t f); +}; + +class WoweeMapsLoader { +public: + static bool save(const WoweeMaps& cat, + const std::string& basePath); + static WoweeMaps load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-maps* variants. + // + // makeStarter — 1 map (continent) + 3 areas (capital, + // starter zone, neighboring zone with + // parent chain). + // makeClassic — 2 continents + a small dungeon instance + // + 6 areas wiring sub-zones to parents + // (Stormwind > City Trade District etc). + // makeBgArena — 2 maps showcasing Battleground (40 players) + // and Arena (5v5) types. + static WoweeMaps makeStarter(const std::string& catalogName); + static WoweeMaps makeClassic(const std::string& catalogName); + static WoweeMaps makeBgArena(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_maps.cpp b/src/pipeline/wowee_maps.cpp new file mode 100644 index 00000000..c3b8f4b7 --- /dev/null +++ b/src/pipeline/wowee_maps.cpp @@ -0,0 +1,318 @@ +#include "pipeline/wowee_maps.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'M', 'S', 'X'}; +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() < 4 || base.substr(base.size() - 4) != ".wms") { + base += ".wms"; + } + return base; +} + +} // namespace + +const WoweeMaps::Map* WoweeMaps::findMap(uint32_t mapId) const { + for (const auto& m : maps) if (m.mapId == mapId) return &m; + return nullptr; +} + +const WoweeMaps::Area* WoweeMaps::findArea(uint32_t areaId) const { + for (const auto& a : areas) if (a.areaId == areaId) return &a; + return nullptr; +} + +const char* WoweeMaps::mapTypeName(uint8_t t) { + switch (t) { + case Continent: return "continent"; + case Instance: return "instance"; + case Raid: return "raid"; + case Battleground: return "battleground"; + case Arena: return "arena"; + default: return "unknown"; + } +} + +const char* WoweeMaps::expansionName(uint8_t e) { + switch (e) { + case Classic: return "classic"; + case Tbc: return "tbc"; + case Wotlk: return "wotlk"; + case Cata: return "cata"; + case Mop: return "mop"; + default: return "unknown"; + } +} + +const char* WoweeMaps::factionGroupName(uint8_t f) { + switch (f) { + case FactionBoth: return "both"; + case FactionAlliance: return "alliance"; + case FactionHorde: return "horde"; + case FactionContested: return "contested"; + default: return "unknown"; + } +} + +bool WoweeMapsLoader::save(const WoweeMaps& 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 mapCount = static_cast(cat.maps.size()); + writePOD(os, mapCount); + for (const auto& m : cat.maps) { + writePOD(os, m.mapId); + writeStr(os, m.name); + writeStr(os, m.shortName); + writePOD(os, m.mapType); + writePOD(os, m.expansionId); + uint8_t pad2[2] = {0, 0}; + os.write(reinterpret_cast(pad2), 2); + writePOD(os, m.maxPlayers); + os.write(reinterpret_cast(pad2), 2); + } + uint32_t areaCount = static_cast(cat.areas.size()); + writePOD(os, areaCount); + for (const auto& a : cat.areas) { + writePOD(os, a.areaId); + writePOD(os, a.mapId); + writePOD(os, a.parentAreaId); + writeStr(os, a.name); + writePOD(os, a.minLevel); + writePOD(os, a.maxLevel); + writePOD(os, a.factionGroup); + uint8_t pad3[3] = {0, 0, 0}; + os.write(reinterpret_cast(pad3), 3); + writePOD(os, a.explorationXP); + writePOD(os, a.ambienceSoundId); + } + return os.good(); +} + +WoweeMaps WoweeMapsLoader::load(const std::string& basePath) { + WoweeMaps 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 mapCount = 0; + if (!readPOD(is, mapCount)) return out; + if (mapCount > (1u << 20)) return out; + out.maps.resize(mapCount); + for (auto& m : out.maps) { + if (!readPOD(is, m.mapId)) { out.maps.clear(); return out; } + if (!readStr(is, m.name) || !readStr(is, m.shortName)) { + out.maps.clear(); return out; + } + if (!readPOD(is, m.mapType) || !readPOD(is, m.expansionId)) { + out.maps.clear(); return out; + } + uint8_t pad2[2]; + is.read(reinterpret_cast(pad2), 2); + if (is.gcount() != 2) { out.maps.clear(); return out; } + if (!readPOD(is, m.maxPlayers)) { + out.maps.clear(); return out; + } + is.read(reinterpret_cast(pad2), 2); + if (is.gcount() != 2) { out.maps.clear(); return out; } + } + uint32_t areaCount = 0; + if (!readPOD(is, areaCount)) { + out.maps.clear(); return out; + } + if (areaCount > (1u << 20)) { + out.maps.clear(); return out; + } + out.areas.resize(areaCount); + for (auto& a : out.areas) { + if (!readPOD(is, a.areaId) || + !readPOD(is, a.mapId) || + !readPOD(is, a.parentAreaId)) { + out.maps.clear(); out.areas.clear(); return out; + } + if (!readStr(is, a.name)) { + out.maps.clear(); out.areas.clear(); return out; + } + if (!readPOD(is, a.minLevel) || + !readPOD(is, a.maxLevel) || + !readPOD(is, a.factionGroup)) { + out.maps.clear(); out.areas.clear(); return out; + } + uint8_t pad3[3]; + is.read(reinterpret_cast(pad3), 3); + if (is.gcount() != 3) { + out.maps.clear(); out.areas.clear(); return out; + } + if (!readPOD(is, a.explorationXP) || + !readPOD(is, a.ambienceSoundId)) { + out.maps.clear(); out.areas.clear(); return out; + } + } + return out; +} + +bool WoweeMapsLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeMaps WoweeMapsLoader::makeStarter(const std::string& catalogName) { + WoweeMaps c; + c.name = catalogName; + { + WoweeMaps::Map m; + m.mapId = 0; m.name = "Eastern Kingdoms"; + m.shortName = "EK"; + m.mapType = WoweeMaps::Continent; + c.maps.push_back(m); + } + { + WoweeMaps::Area a; + a.areaId = 1; a.mapId = 0; + a.name = "Stormwind City"; + a.minLevel = 1; a.maxLevel = 70; + a.factionGroup = WoweeMaps::FactionAlliance; + a.explorationXP = 200; + c.areas.push_back(a); + } + { + WoweeMaps::Area a; + a.areaId = 2; a.mapId = 0; + a.name = "Elwynn Forest"; + a.minLevel = 1; a.maxLevel = 10; + a.factionGroup = WoweeMaps::FactionAlliance; + a.explorationXP = 100; + a.ambienceSoundId = 100; // WSND.makeAmbient bird-loop + c.areas.push_back(a); + } + { + WoweeMaps::Area a; + a.areaId = 3; a.mapId = 0; a.parentAreaId = 2; + a.name = "Goldshire"; + a.minLevel = 5; a.maxLevel = 10; + a.factionGroup = WoweeMaps::FactionAlliance; + a.explorationXP = 50; + c.areas.push_back(a); + } + return c; +} + +WoweeMaps WoweeMapsLoader::makeClassic(const std::string& catalogName) { + WoweeMaps c; + c.name = catalogName; + auto addMap = [&](uint32_t id, const char* name, + const char* shortName, uint8_t type, + uint16_t maxPlayers) { + WoweeMaps::Map m; + m.mapId = id; m.name = name; m.shortName = shortName; + m.mapType = type; m.expansionId = WoweeMaps::Classic; + m.maxPlayers = maxPlayers; + c.maps.push_back(m); + }; + addMap(0, "Eastern Kingdoms", "EK", WoweeMaps::Continent, 0); + addMap(1, "Kalimdor", "Kalim", WoweeMaps::Continent, 0); + addMap(36, "Deadmines", "DM", WoweeMaps::Instance, 5); + auto addArea = [&](uint32_t id, uint32_t mapId, + uint32_t parent, const char* name, + uint16_t minLvl, uint16_t maxLvl, + uint8_t faction, uint32_t xp, + uint32_t soundId = 0) { + WoweeMaps::Area a; + a.areaId = id; a.mapId = mapId; a.parentAreaId = parent; + a.name = name; a.minLevel = minLvl; a.maxLevel = maxLvl; + a.factionGroup = faction; a.explorationXP = xp; + a.ambienceSoundId = soundId; + c.areas.push_back(a); + }; + // Top-level zones in EK + sub-zones (parent chain). + addArea(12, 0, 0, "Elwynn Forest", 1, 10, + WoweeMaps::FactionAlliance, 100, 100); + addArea(87, 0, 12, "Goldshire", 5, 10, + WoweeMaps::FactionAlliance, 50); + addArea(40, 0, 0, "Westfall", 10, 20, + WoweeMaps::FactionContested, 200); + addArea(39, 0, 0, "Duskwood", 18, 30, + WoweeMaps::FactionContested, 250); + // Kalimdor. + addArea(141, 1, 0, "Teldrassil", 1, 10, + WoweeMaps::FactionAlliance, 100); + // Instance areas. + addArea(2017, 36, 0, "The Deadmines", 17, 22, + WoweeMaps::FactionBoth, 0, 200); // fire-crackle ambient + return c; +} + +WoweeMaps WoweeMapsLoader::makeBgArena(const std::string& catalogName) { + WoweeMaps c; + c.name = catalogName; + { + WoweeMaps::Map m; + m.mapId = 30; m.name = "Alterac Valley"; + m.shortName = "AV"; + m.mapType = WoweeMaps::Battleground; + m.expansionId = WoweeMaps::Classic; + m.maxPlayers = 40; + c.maps.push_back(m); + } + { + WoweeMaps::Map m; + m.mapId = 559; m.name = "Nagrand Arena"; + m.shortName = "Naga"; + m.mapType = WoweeMaps::Arena; + m.expansionId = WoweeMaps::Tbc; + m.maxPlayers = 10; // 5v5 cap + c.maps.push_back(m); + } + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 204e6207..5385dc4e 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -73,6 +73,8 @@ const char* const kArgRequired[] = { "--export-wtax-json", "--import-wtax-json", "--gen-talents", "--gen-talents-warrior", "--gen-talents-mage", "--info-wtal", "--validate-wtal", + "--gen-maps", "--gen-maps-classic", "--gen-maps-bgarena", + "--info-wms", "--validate-wms", "--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 2b39d424..7fd15904 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -53,6 +53,7 @@ #include "cli_gossip_catalog.hpp" #include "cli_taxi_catalog.hpp" #include "cli_talents_catalog.hpp" +#include "cli_maps_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -147,6 +148,7 @@ constexpr DispatchFn kDispatchTable[] = { handleGossipCatalog, handleTaxiCatalog, handleTalentsCatalog, + handleMapsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 8313fe5d..3fe0a010 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1061,6 +1061,16 @@ void printUsage(const char* argv0) { std::printf(" Print WTAL trees + per-talent grid position / max rank / prereq chain / rank-1 spellId\n"); std::printf(" --validate-wtal [--json]\n"); std::printf(" Static checks: tree+talent ids>0+unique, maxRank 1..5, prereq references resolve, no self-prereq\n"); + std::printf(" --gen-maps [name]\n"); + std::printf(" Emit .wms starter: 1 map (Eastern Kingdoms) + 3 areas (Stormwind / Elwynn / Goldshire) with parent chain\n"); + std::printf(" --gen-maps-classic [name]\n"); + std::printf(" Emit .wms classic set: 2 continents + Deadmines instance + 6 areas with sub-zone parent chains + WSND refs\n"); + std::printf(" --gen-maps-bgarena [name]\n"); + std::printf(" Emit .wms PvP maps: Alterac Valley (40-player BG) + Nagrand Arena (5v5)\n"); + std::printf(" --info-wms [--json]\n"); + std::printf(" Print WMS maps (id / type / expansion / max players) + areas (id / map / parent / level / faction / xp)\n"); + std::printf(" --validate-wms [--json]\n"); + std::printf(" Static checks: ids unique, areas reference real maps, parent areas exist + same map, BG/Arena needs maxPlayers\n"); std::printf(" --gen-weather-temperate [zoneName]\n"); std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n"); std::printf(" --gen-weather-arctic [zoneName]\n"); diff --git a/tools/editor/cli_maps_catalog.cpp b/tools/editor/cli_maps_catalog.cpp new file mode 100644 index 00000000..366288d6 --- /dev/null +++ b/tools/editor/cli_maps_catalog.cpp @@ -0,0 +1,307 @@ +#include "cli_maps_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_maps.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWmsExt(std::string base) { + stripExt(base, ".wms"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeMaps& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeMapsLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wms\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeMaps& c, + const std::string& base) { + std::printf("Wrote %s.wms\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" maps : %zu areas : %zu\n", + c.maps.size(), c.areas.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterMaps"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWmsExt(base); + auto c = wowee::pipeline::WoweeMapsLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-maps")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenClassic(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "ClassicMaps"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWmsExt(base); + auto c = wowee::pipeline::WoweeMapsLoader::makeClassic(name); + if (!saveOrError(c, base, "gen-maps-classic")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenBgArena(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "BgArenaMaps"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWmsExt(base); + auto c = wowee::pipeline::WoweeMapsLoader::makeBgArena(name); + if (!saveOrError(c, base, "gen-maps-bgarena")) 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 = stripWmsExt(base); + if (!wowee::pipeline::WoweeMapsLoader::exists(base)) { + std::fprintf(stderr, "WMS not found: %s.wms\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeMapsLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wms"] = base + ".wms"; + j["name"] = c.name; + j["mapCount"] = c.maps.size(); + j["areaCount"] = c.areas.size(); + nlohmann::json ma = nlohmann::json::array(); + for (const auto& m : c.maps) { + ma.push_back({ + {"mapId", m.mapId}, + {"name", m.name}, + {"shortName", m.shortName}, + {"mapType", m.mapType}, + {"mapTypeName", wowee::pipeline::WoweeMaps::mapTypeName(m.mapType)}, + {"expansionId", m.expansionId}, + {"expansionName", wowee::pipeline::WoweeMaps::expansionName(m.expansionId)}, + {"maxPlayers", m.maxPlayers}, + }); + } + j["maps"] = ma; + nlohmann::json aa = nlohmann::json::array(); + for (const auto& a : c.areas) { + aa.push_back({ + {"areaId", a.areaId}, + {"mapId", a.mapId}, + {"parentAreaId", a.parentAreaId}, + {"name", a.name}, + {"minLevel", a.minLevel}, + {"maxLevel", a.maxLevel}, + {"factionGroup", a.factionGroup}, + {"factionGroupName", wowee::pipeline::WoweeMaps::factionGroupName(a.factionGroup)}, + {"explorationXP", a.explorationXP}, + {"ambienceSoundId", a.ambienceSoundId}, + }); + } + j["areas"] = aa; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WMS: %s.wms\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" maps : %zu areas : %zu\n", + c.maps.size(), c.areas.size()); + if (!c.maps.empty()) { + std::printf("\n Maps:\n"); + std::printf(" id type expansion max short name\n"); + for (const auto& m : c.maps) { + std::printf(" %4u %-12s %-9s %3u %-5s %s\n", + m.mapId, + wowee::pipeline::WoweeMaps::mapTypeName(m.mapType), + wowee::pipeline::WoweeMaps::expansionName(m.expansionId), + m.maxPlayers, m.shortName.c_str(), + m.name.c_str()); + } + } + if (!c.areas.empty()) { + std::printf("\n Areas:\n"); + std::printf(" id map parent level faction xp sound name\n"); + for (const auto& a : c.areas) { + std::printf(" %5u %3u %5u %2u-%-2u %-10s %4u %4u %s\n", + a.areaId, a.mapId, a.parentAreaId, + a.minLevel, a.maxLevel, + wowee::pipeline::WoweeMaps::factionGroupName(a.factionGroup), + a.explorationXP, a.ambienceSoundId, + a.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 = stripWmsExt(base); + if (!wowee::pipeline::WoweeMapsLoader::exists(base)) { + std::fprintf(stderr, + "validate-wms: WMS not found: %s.wms\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeMapsLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.maps.empty()) { + warnings.push_back("catalog has zero maps"); + } + std::vector mapIdsSeen; + for (size_t k = 0; k < c.maps.size(); ++k) { + const auto& m = c.maps[k]; + std::string ctx = "map " + std::to_string(k) + + " (id=" + std::to_string(m.mapId); + if (!m.name.empty()) ctx += " " + m.name; + ctx += ")"; + if (m.name.empty()) { + errors.push_back(ctx + ": name is empty"); + } + if (m.mapType > wowee::pipeline::WoweeMaps::Arena) { + errors.push_back(ctx + ": mapType " + + std::to_string(m.mapType) + " not in 0..4"); + } + if (m.expansionId > wowee::pipeline::WoweeMaps::Mop) { + errors.push_back(ctx + ": expansionId " + + std::to_string(m.expansionId) + " not in 0..4"); + } + // Battleground / Arena need a player cap; continent/instance + // can leave it 0 (unlimited / set by instance template). + if ((m.mapType == wowee::pipeline::WoweeMaps::Battleground || + m.mapType == wowee::pipeline::WoweeMaps::Arena) && + m.maxPlayers == 0) { + warnings.push_back(ctx + + ": Battleground/Arena with maxPlayers=0 (no participant cap)"); + } + for (uint32_t prev : mapIdsSeen) { + if (prev == m.mapId) { + errors.push_back(ctx + ": duplicate mapId"); + break; + } + } + mapIdsSeen.push_back(m.mapId); + } + std::vector areaIdsSeen; + for (size_t k = 0; k < c.areas.size(); ++k) { + const auto& a = c.areas[k]; + std::string ctx = "area " + std::to_string(k) + + " (id=" + std::to_string(a.areaId); + if (!a.name.empty()) ctx += " " + a.name; + ctx += ")"; + if (a.areaId == 0) { + errors.push_back(ctx + ": areaId is 0"); + } + if (a.name.empty()) { + errors.push_back(ctx + ": name is empty"); + } + if (a.maxLevel < a.minLevel) { + errors.push_back(ctx + ": maxLevel < minLevel"); + } + if (a.factionGroup > wowee::pipeline::WoweeMaps::FactionContested) { + errors.push_back(ctx + ": factionGroup " + + std::to_string(a.factionGroup) + " not in 0..3"); + } + // Area must reference a real map. + if (!c.findMap(a.mapId)) { + errors.push_back(ctx + ": mapId " + + std::to_string(a.mapId) + + " does not exist in this catalog"); + } + // parentAreaId (if non-zero) must reference a real area + // and must be on the same map. + if (a.parentAreaId != 0) { + const auto* parent = c.findArea(a.parentAreaId); + if (!parent) { + errors.push_back(ctx + ": parentAreaId " + + std::to_string(a.parentAreaId) + + " does not exist"); + } else if (parent->mapId != a.mapId) { + errors.push_back(ctx + ": parent area " + + std::to_string(a.parentAreaId) + + " is on a different map"); + } else if (a.parentAreaId == a.areaId) { + errors.push_back(ctx + ": area lists itself as parent"); + } + } + for (uint32_t prev : areaIdsSeen) { + if (prev == a.areaId) { + errors.push_back(ctx + ": duplicate areaId"); + break; + } + } + areaIdsSeen.push_back(a.areaId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wms"] = base + ".wms"; + 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-wms: %s.wms\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu maps, %zu areas, all IDs unique\n", + c.maps.size(), c.areas.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 handleMapsCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-maps") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-maps-classic") == 0 && i + 1 < argc) { + outRc = handleGenClassic(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-maps-bgarena") == 0 && i + 1 < argc) { + outRc = handleGenBgArena(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wms") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wms") == 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_maps_catalog.hpp b/tools/editor/cli_maps_catalog.hpp new file mode 100644 index 00000000..364cd44d --- /dev/null +++ b/tools/editor/cli_maps_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleMapsCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee