feat(pipeline): add WMS (Wowee Map / Area) catalog format

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.
This commit is contained in:
Kelsi 2026-05-09 16:40:00 -07:00
parent cc4b9a6fad
commit 82a8c3559e
8 changed files with 790 additions and 0 deletions

View file

@ -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

View file

@ -0,0 +1,137 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
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<Map> maps;
std::vector<Area> 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

318
src/pipeline/wowee_maps.cpp Normal file
View file

@ -0,0 +1,318 @@
#include "pipeline/wowee_maps.hpp"
#include <cstdio>
#include <cstring>
#include <fstream>
namespace wowee {
namespace pipeline {
namespace {
constexpr char kMagic[4] = {'W', 'M', 'S', 'X'};
constexpr uint32_t kVersion = 1;
template <typename T>
void writePOD(std::ofstream& os, const T& v) {
os.write(reinterpret_cast<const char*>(&v), sizeof(T));
}
template <typename T>
bool readPOD(std::ifstream& is, T& v) {
is.read(reinterpret_cast<char*>(&v), sizeof(T));
return is.gcount() == static_cast<std::streamsize>(sizeof(T));
}
void writeStr(std::ofstream& os, const std::string& s) {
uint32_t n = static_cast<uint32_t>(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<std::streamsize>(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<uint32_t>(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<const char*>(pad2), 2);
writePOD(os, m.maxPlayers);
os.write(reinterpret_cast<const char*>(pad2), 2);
}
uint32_t areaCount = static_cast<uint32_t>(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<const char*>(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<char*>(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<char*>(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<char*>(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

View file

@ -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",

View file

@ -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,

View file

@ -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 <wtal-base> [--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 <wms-base> [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 <wms-base> [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 <wms-base> [name]\n");
std::printf(" Emit .wms PvP maps: Alterac Valley (40-player BG) + Nagrand Arena (5v5)\n");
std::printf(" --info-wms <wms-base> [--json]\n");
std::printf(" Print WMS maps (id / type / expansion / max players) + areas (id / map / parent / level / faction / xp)\n");
std::printf(" --validate-wms <wms-base> [--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 <wow-base> [zoneName]\n");
std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n");
std::printf(" --gen-weather-arctic <wow-base> [zoneName]\n");

View file

@ -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 <nlohmann/json.hpp>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <string>
#include <vector>
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<std::string> errors;
std::vector<std::string> warnings;
if (c.maps.empty()) {
warnings.push_back("catalog has zero maps");
}
std::vector<uint32_t> 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<uint32_t> 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

View file

@ -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