From 054f44e4aacdfd324615543973c862f28584fc80 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 10 May 2026 00:47:02 -0700 Subject: [PATCH] =?UTF-8?q?feat(editor):=20add=20WMSP=20(Master=20Server?= =?UTF-8?q?=20Profile)=20=E2=80=94=20100th=20open=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Novel replacement for the hardcoded realmlist that the WoW client receives via SMSG_REALM_LIST during login. Each entry is one selectable realm: name, network address (host:port), realm type (Normal/PvP/RP/RPPvP/Test), realm category (Public/Private/Beta/Dev), expansion gating (Vanilla 1.12.1 / TBC 2.4.3 / WotLK 3.3.5a / Cata 4.3.4), population indicator (Low/Medium/High/Full/Locked), char- acter cap, GM-only flag, timezone hint, and per-realm version+build numbers. 100th open format — milestone marker for the catalog ecosystem. WMSP is a TOP-LEVEL bootstrap catalog (read by the login server before any character is loaded), so it deliberately has no cross-references to other catalogs; all other social/world/spell catalogs depend on a player session that doesn't exist until WMSP has been consulted. Three preset emitters covering common deployment shapes: makeSingleRealm (1 default WoweeMain WotLK Public), makePvPCluster (3 realms — PvE/PvP/RP — sharing one login address so players pick rule-set without changing servers), makeMultiExpansion (4 progression realms across all expansion gates with their canonical build numbers from the matching client). Validator catches several real misconfigurations: empty address (login server cannot route session), realmType out of {0,1,4,6,8} (the WoW client's RealmType enum is non-contiguous — 2/3/5/7 are unused values that crash the picker), characterCap=0 (players can't make characters), duplicate realm names (picker requires unique display names), missing port in address. Format count 99 -> 100. CLI flag count 1119 -> 1124. --- CMakeLists.txt | 3 + include/pipeline/wowee_realm_list.hpp | 155 +++++++++++ src/pipeline/wowee_realm_list.cpp | 284 ++++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_format_table.cpp | 1 + tools/editor/cli_help.cpp | 10 + tools/editor/cli_list_formats.cpp | 1 + tools/editor/cli_realm_list_catalog.cpp | 338 ++++++++++++++++++++++++ tools/editor/cli_realm_list_catalog.hpp | 12 + 10 files changed, 808 insertions(+) create mode 100644 include/pipeline/wowee_realm_list.hpp create mode 100644 src/pipeline/wowee_realm_list.cpp create mode 100644 tools/editor/cli_realm_list_catalog.cpp create mode 100644 tools/editor/cli_realm_list_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6036944a..8b869e99 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -688,6 +688,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_hearth_binds.cpp src/pipeline/wowee_server_broadcasts.cpp src/pipeline/wowee_combat_maneuvers.cpp + src/pipeline/wowee_realm_list.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1539,6 +1540,7 @@ add_executable(wowee_editor tools/editor/cli_hearth_binds_catalog.cpp tools/editor/cli_server_broadcasts_catalog.cpp tools/editor/cli_combat_maneuvers_catalog.cpp + tools/editor/cli_realm_list_catalog.cpp tools/editor/cli_catalog_pluck.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp @@ -1706,6 +1708,7 @@ add_executable(wowee_editor src/pipeline/wowee_hearth_binds.cpp src/pipeline/wowee_server_broadcasts.cpp src/pipeline/wowee_combat_maneuvers.cpp + src/pipeline/wowee_realm_list.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_realm_list.hpp b/include/pipeline/wowee_realm_list.hpp new file mode 100644 index 00000000..16d7b3a4 --- /dev/null +++ b/include/pipeline/wowee_realm_list.hpp @@ -0,0 +1,155 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Master Server Profile catalog (.wmsp) — +// novel replacement for the hardcoded realmlist that the +// WoW client receives via SMSG_REALM_LIST during login. +// Each entry is one selectable realm: name, network +// address, type (Normal / PvP / RP / RPPvP / Test), +// expansion gating, population indicator, character cap, +// and access flags. +// +// 100th open format — milestone marker. Server admins use +// this catalog as the single source of truth for which +// realms appear on the realm picker; loading it replaces +// the hardcoded `realmlist.wtf` lookup that vanilla +// servers have nailed into their login daemon. +// +// Cross-references with previously-added formats: +// No catalog cross-references; WMSP is a TOP-LEVEL +// bootstrap catalog read before any in-world data. +// The realmlist is consumed by the login server before +// any character is loaded, so it cannot reference +// anything that depends on having a logged-in player. +// +// Binary layout (little-endian): +// magic[4] = "WMSP" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// realmId (uint32) +// nameLen + name +// descLen + description +// addrLen + address (host:port) +// realmType (uint8) — Normal / PvP / RP / +// RPPvP / Test +// realmCategory (uint8) — Public / Private / +// Beta / Dev +// expansion (uint8) — Vanilla / TBC / WotLK / +// Cata +// population (uint8) — Low / Medium / High / +// Full / Locked +// characterCap (uint8) +// gmOnly (uint8) — 0/1 bool +// timezone (uint8) +// pad0 (uint8) +// versionMajor (uint8) / versionMinor (uint8) +// versionPatch (uint8) / pad1 (uint8) +// buildNumber (uint32) +// iconColorRGBA (uint32) +struct WoweeRealmList { + enum RealmType : uint8_t { + Normal = 0, + PvP = 1, + RP = 6, + RPPvP = 8, + Test = 4, + }; + + enum RealmCategory : uint8_t { + Public = 0, + Private = 1, + Beta = 2, + Dev = 3, + }; + + enum Expansion : uint8_t { + Vanilla = 0, // 1.12 / 1.x + TBC = 1, // 2.4.3 + WotLK = 2, // 3.3.5a + Cata = 3, // 4.x (future) + }; + + enum Population : uint8_t { + Low = 0, + Medium = 1, + High = 2, + Full = 3, + Locked = 4, + }; + + struct Entry { + uint32_t realmId = 0; + std::string name; + std::string description; + std::string address; // "host:port" + uint8_t realmType = Normal; + uint8_t realmCategory = Public; + uint8_t expansion = WotLK; + uint8_t population = Medium; + uint8_t characterCap = 10; + uint8_t gmOnly = 0; + uint8_t timezone = 8; // East Coast US + uint8_t pad0 = 0; + uint8_t versionMajor = 3; + uint8_t versionMinor = 3; + uint8_t versionPatch = 5; + uint8_t pad1 = 0; + uint32_t buildNumber = 12340; // WotLK 3.3.5a + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t realmId) const; + + // Returns realms a player should see based on their + // installed expansion (Vanilla clients can only see + // Vanilla realms; WotLK clients see Vanilla/TBC/WotLK + // due to the realm picker being expansion-tolerant). + std::vector findByExpansion(uint8_t maxExpansion) const; + + // Returns realms of one type — used by the picker UI's + // "PvP only" / "RP only" filters. + std::vector findByType(uint8_t realmType) const; +}; + +class WoweeRealmListLoader { +public: + static bool save(const WoweeRealmList& cat, + const std::string& basePath); + static WoweeRealmList load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-msp* variants. + // + // makeSingleRealm — 1 entry: WoweeMain (WotLK + // Normal, Public, Medium pop, + // 10-char cap, US East TZ). + // makePvPCluster — 3 entries: WoweePvE / WoweePvP + // / WoweeRP (same login address, + // 3 realm types — players can + // pick rule-set without + // changing servers). + // makeMultiExpansion — 4 entries spanning all + // supported expansion gates + // (Vanilla 1.12 / TBC 2.4.3 / + // WotLK 3.3.5a / Cata 4.3.4) + // each with its own buildNumber. + static WoweeRealmList makeSingleRealm(const std::string& catalogName); + static WoweeRealmList makePvPCluster(const std::string& catalogName); + static WoweeRealmList makeMultiExpansion(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_realm_list.cpp b/src/pipeline/wowee_realm_list.cpp new file mode 100644 index 00000000..3a5c389c --- /dev/null +++ b/src/pipeline/wowee_realm_list.cpp @@ -0,0 +1,284 @@ +#include "pipeline/wowee_realm_list.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'M', 'S', 'P'}; +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) != ".wmsp") { + base += ".wmsp"; + } + 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 WoweeRealmList::Entry* +WoweeRealmList::findById(uint32_t realmId) const { + for (const auto& e : entries) + if (e.realmId == realmId) return &e; + return nullptr; +} + +std::vector +WoweeRealmList::findByExpansion(uint8_t maxExpansion) const { + std::vector out; + for (const auto& e : entries) + if (e.expansion <= maxExpansion) out.push_back(&e); + return out; +} + +std::vector +WoweeRealmList::findByType(uint8_t realmType) const { + std::vector out; + for (const auto& e : entries) + if (e.realmType == realmType) out.push_back(&e); + return out; +} + +bool WoweeRealmListLoader::save(const WoweeRealmList& 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.realmId); + writeStr(os, e.name); + writeStr(os, e.description); + writeStr(os, e.address); + writePOD(os, e.realmType); + writePOD(os, e.realmCategory); + writePOD(os, e.expansion); + writePOD(os, e.population); + writePOD(os, e.characterCap); + writePOD(os, e.gmOnly); + writePOD(os, e.timezone); + writePOD(os, e.pad0); + writePOD(os, e.versionMajor); + writePOD(os, e.versionMinor); + writePOD(os, e.versionPatch); + writePOD(os, e.pad1); + writePOD(os, e.buildNumber); + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweeRealmList WoweeRealmListLoader::load(const std::string& basePath) { + WoweeRealmList 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.realmId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || + !readStr(is, e.description) || + !readStr(is, e.address)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.realmType) || + !readPOD(is, e.realmCategory) || + !readPOD(is, e.expansion) || + !readPOD(is, e.population) || + !readPOD(is, e.characterCap) || + !readPOD(is, e.gmOnly) || + !readPOD(is, e.timezone) || + !readPOD(is, e.pad0) || + !readPOD(is, e.versionMajor) || + !readPOD(is, e.versionMinor) || + !readPOD(is, e.versionPatch) || + !readPOD(is, e.pad1) || + !readPOD(is, e.buildNumber) || + !readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeRealmListLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeRealmList WoweeRealmListLoader::makeSingleRealm( + const std::string& catalogName) { + using R = WoweeRealmList; + WoweeRealmList c; + c.name = catalogName; + R::Entry e; + e.realmId = 1; + e.name = "WoweeMain"; + e.description = + "Default Wowee server realm — WotLK 3.3.5a, Normal " + "PvE rule-set, public category, US East timezone, " + "10-character cap per account."; + e.address = "logon.wowee.example.com:8085"; + e.realmType = R::Normal; + e.realmCategory = R::Public; + e.expansion = R::WotLK; + e.population = R::Medium; + e.characterCap = 10; + e.gmOnly = 0; + e.timezone = 8; + e.versionMajor = 3; e.versionMinor = 3; e.versionPatch = 5; + e.buildNumber = 12340; + e.iconColorRGBA = packRgba(140, 200, 255); // realm blue + c.entries.push_back(e); + return c; +} + +WoweeRealmList WoweeRealmListLoader::makePvPCluster( + const std::string& catalogName) { + using R = WoweeRealmList; + WoweeRealmList c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t type, + uint8_t pop, uint32_t color, const char* desc) { + R::Entry e; + e.realmId = id; e.name = name; e.description = desc; + e.address = "cluster.wowee.example.com:8085"; + e.realmType = type; + e.realmCategory = R::Public; + e.expansion = R::WotLK; + e.population = pop; + e.characterCap = 10; + e.gmOnly = 0; + e.timezone = 8; + e.versionMajor = 3; e.versionMinor = 3; e.versionPatch = 5; + e.buildNumber = 12340; + e.iconColorRGBA = color; + c.entries.push_back(e); + }; + add(1, "WoweePvE", R::Normal, R::Medium, + packRgba(140, 200, 255), + "Cluster realm — Normal PvE rule-set. World PvP " + "is opt-in; ganking flagged as harassment."); + add(2, "WoweePvP", R::PvP, R::High, + packRgba(220, 80, 100), + "Cluster realm — PvP rule-set. Cross-faction " + "world combat is always-on outside of cities."); + add(3, "WoweeRP", R::RP, R::Low, + packRgba(180, 100, 240), + "Cluster realm — Roleplay. Naming policy " + "enforced; in-character chat encouraged."); + return c; +} + +WoweeRealmList WoweeRealmListLoader::makeMultiExpansion( + const std::string& catalogName) { + using R = WoweeRealmList; + WoweeRealmList c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, + uint8_t expansion, + uint8_t verMajor, uint8_t verMinor, + uint8_t verPatch, uint32_t build, + uint32_t color, const char* desc) { + R::Entry e; + e.realmId = id; e.name = name; e.description = desc; + e.address = "logon.wowee.example.com:8085"; + e.realmType = R::Normal; + e.realmCategory = R::Public; + e.expansion = expansion; + e.population = R::Medium; + e.characterCap = 10; + e.gmOnly = 0; + e.timezone = 8; + e.versionMajor = verMajor; + e.versionMinor = verMinor; + e.versionPatch = verPatch; + e.buildNumber = build; + e.iconColorRGBA = color; + c.entries.push_back(e); + }; + add(1, "Wowee-Vanilla", R::Vanilla, 1, 12, 1, 5875, + packRgba(220, 220, 100), + "Vanilla 1.12.1 progression realm — original " + "60-cap content, no Outland zones."); + add(2, "Wowee-TBC", R::TBC, 2, 4, 3, 8606, + packRgba(100, 220, 100), + "TBC 2.4.3 progression realm — Outland + 70-cap " + "content, Sunwell endgame."); + add(3, "Wowee-WotLK", R::WotLK, 3, 3, 5, 12340, + packRgba(140, 200, 255), + "WotLK 3.3.5a progression realm — Northrend + " + "80-cap content, ICC endgame."); + add(4, "Wowee-Cata", R::Cata, 4, 3, 4, 15595, + packRgba(220, 130, 80), + "Cata 4.3.4 progression realm — post-Shattering " + "world + 85-cap content, DS endgame. Currently " + "Beta access only."); + // Mark Cata as beta category (override the default + // Public set by the lambda). + if (!c.entries.empty()) { + c.entries.back().realmCategory = R::Beta; + c.entries.back().population = R::Low; + } + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index be5047c7..c7e1e906 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -307,6 +307,8 @@ const char* const kArgRequired[] = { "--gen-cmg", "--gen-cmg-druid", "--gen-cmg-all", "--info-wcmg", "--validate-wcmg", "--export-wcmg-json", "--import-wcmg-json", + "--gen-msp", "--gen-msp-cluster", "--gen-msp-multi", + "--info-wmsp", "--validate-wmsp", "--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 d642f1fa..c5fa9d81 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -144,6 +144,7 @@ #include "cli_hearth_binds_catalog.hpp" #include "cli_server_broadcasts_catalog.hpp" #include "cli_combat_maneuvers_catalog.hpp" +#include "cli_realm_list_catalog.hpp" #include "cli_catalog_pluck.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" @@ -330,6 +331,7 @@ constexpr DispatchFn kDispatchTable[] = { handleHearthBindsCatalog, handleServerBroadcastsCatalog, handleCombatManeuversCatalog, + handleRealmListCatalog, handleCatalogPluck, handleQuestObjective, handleQuestReward, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 6f66c065..352e3749 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -102,6 +102,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','H','R','T'}, ".whrt", "social", "--info-whrt", "Hearthstone bind point catalog"}, {{'W','S','C','B'}, ".wscb", "server", "--info-wscb", "Server channel broadcast catalog"}, {{'W','C','M','G'}, ".wcmg", "spells", "--info-wcmg", "Combat maneuver group catalog"}, + {{'W','M','S','P'}, ".wmsp", "server", "--info-wmsp", "Master server profile / realmlist 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 dfb5ff07..6cfa3821 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -2153,6 +2153,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wcmg to a human-editable JSON sidecar (defaults to .wcmg.json; emits both categoryKind int AND name string; members[] as JSON array of spell IDs)\n"); std::printf(" --import-wcmg-json [out-base]\n"); std::printf(" Import a .wcmg.json sidecar back into binary .wcmg (categoryKind int OR \"stance\"/\"form\"/\"aspect\"/\"presence\"/\"posture\"/\"sigil\"; exclusive bool OR int)\n"); + std::printf(" --gen-msp [name]\n"); + std::printf(" Emit .wmsp 1 default realm entry (WoweeMain — WotLK 3.3.5a Normal PvE Public Medium)\n"); + std::printf(" --gen-msp-cluster [name]\n"); + std::printf(" Emit .wmsp 3-realm cluster (WoweePvE/PvP/RP on same login address — players pick rule-set without changing login)\n"); + std::printf(" --gen-msp-multi [name]\n"); + std::printf(" Emit .wmsp 4 progression realms across all expansion gates (Vanilla 1.12.1 build 5875 / TBC 2.4.3 build 8606 / WotLK 3.3.5a build 12340 / Cata 4.3.4 build 15595)\n"); + std::printf(" --info-wmsp [--json]\n"); + std::printf(" Print WMSP entries (id / type / category / expansion / population / cap / GM-only / build / address / name)\n"); + std::printf(" --validate-wmsp [--json]\n"); + std::printf(" Static checks: id+name+address required, realmType in {0,1,4,6,8}, realmCategory 0..3, expansion 0..3, population 0..4, characterCap>0, no duplicate ids OR names; warns on no-port address, build<5000\n"); std::printf(" --catalog-pluck [--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(" --gen-weather-temperate [zoneName]\n"); diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index b7723ab4..9be2447d 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -124,6 +124,7 @@ constexpr FormatRow kFormats[] = { {"WHRT", ".whrt", "social", "SMSG_BINDPOINTUPDATE bind list", "Hearthstone bind point catalog"}, {"WSCB", ".wscb", "server", "MOTD + scheduled SMSG_NOTIFICATION","Server channel broadcast catalog"}, {"WCMG", ".wcmg", "spells", "Stance/Form/Aspect mutex tables", "Combat maneuver group catalog (mutex spells)"}, + {"WMSP", ".wmsp", "server", "realmlist + SMSG_REALM_LIST data", "Master server profile / realmlist catalog"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine diff --git a/tools/editor/cli_realm_list_catalog.cpp b/tools/editor/cli_realm_list_catalog.cpp new file mode 100644 index 00000000..ef184e86 --- /dev/null +++ b/tools/editor/cli_realm_list_catalog.cpp @@ -0,0 +1,338 @@ +#include "cli_realm_list_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_realm_list.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWmspExt(std::string base) { + stripExt(base, ".wmsp"); + return base; +} + +const char* realmTypeName(uint8_t t) { + using R = wowee::pipeline::WoweeRealmList; + switch (t) { + case R::Normal: return "normal"; + case R::PvP: return "pvp"; + case R::RP: return "rp"; + case R::RPPvP: return "rppvp"; + case R::Test: return "test"; + default: return "unknown"; + } +} + +const char* realmCategoryName(uint8_t c) { + using R = wowee::pipeline::WoweeRealmList; + switch (c) { + case R::Public: return "public"; + case R::Private: return "private"; + case R::Beta: return "beta"; + case R::Dev: return "dev"; + default: return "unknown"; + } +} + +const char* expansionName(uint8_t e) { + using R = wowee::pipeline::WoweeRealmList; + switch (e) { + case R::Vanilla: return "vanilla"; + case R::TBC: return "tbc"; + case R::WotLK: return "wotlk"; + case R::Cata: return "cata"; + default: return "unknown"; + } +} + +const char* populationName(uint8_t p) { + using R = wowee::pipeline::WoweeRealmList; + switch (p) { + case R::Low: return "low"; + case R::Medium: return "medium"; + case R::High: return "high"; + case R::Full: return "full"; + case R::Locked: return "locked"; + default: return "unknown"; + } +} + +bool saveOrError(const wowee::pipeline::WoweeRealmList& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeRealmListLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wmsp\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeRealmList& c, + const std::string& base) { + std::printf("Wrote %s.wmsp\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" realms : %zu\n", c.entries.size()); +} + +int handleGenSingle(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "SingleRealm"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWmspExt(base); + auto c = wowee::pipeline::WoweeRealmListLoader::makeSingleRealm(name); + if (!saveOrError(c, base, "gen-msp")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenCluster(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "PvPCluster"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWmspExt(base); + auto c = wowee::pipeline::WoweeRealmListLoader::makePvPCluster(name); + if (!saveOrError(c, base, "gen-msp-cluster")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenMultiExp(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "MultiExpansion"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWmspExt(base); + auto c = wowee::pipeline::WoweeRealmListLoader::makeMultiExpansion(name); + if (!saveOrError(c, base, "gen-msp-multi")) 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 = stripWmspExt(base); + if (!wowee::pipeline::WoweeRealmListLoader::exists(base)) { + std::fprintf(stderr, "WMSP not found: %s.wmsp\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeRealmListLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wmsp"] = base + ".wmsp"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + char ver[24]; + std::snprintf(ver, sizeof(ver), "%u.%u.%u", + e.versionMajor, e.versionMinor, + e.versionPatch); + arr.push_back({ + {"realmId", e.realmId}, + {"name", e.name}, + {"description", e.description}, + {"address", e.address}, + {"realmType", e.realmType}, + {"realmTypeName", realmTypeName(e.realmType)}, + {"realmCategory", e.realmCategory}, + {"realmCategoryName", + realmCategoryName(e.realmCategory)}, + {"expansion", e.expansion}, + {"expansionName", expansionName(e.expansion)}, + {"population", e.population}, + {"populationName", populationName(e.population)}, + {"characterCap", e.characterCap}, + {"gmOnly", e.gmOnly != 0}, + {"timezone", e.timezone}, + {"versionMajor", e.versionMajor}, + {"versionMinor", e.versionMinor}, + {"versionPatch", e.versionPatch}, + {"versionString", ver}, + {"buildNumber", e.buildNumber}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WMSP: %s.wmsp\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" realms : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id type category expansion pop cap gm build address\n"); + for (const auto& e : c.entries) { + std::printf(" %3u %-7s %-7s %-9s %-7s %3u %s %5u %s\n", + e.realmId, + realmTypeName(e.realmType), + realmCategoryName(e.realmCategory), + expansionName(e.expansion), + populationName(e.population), + e.characterCap, + e.gmOnly ? "Y" : "n", + e.buildNumber, + e.address.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 = stripWmspExt(base); + if (!wowee::pipeline::WoweeRealmListLoader::exists(base)) { + std::fprintf(stderr, + "validate-wmsp: WMSP not found: %s.wmsp\n", + base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeRealmListLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + std::set namesSeen; + 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.realmId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.realmId == 0) + errors.push_back(ctx + ": realmId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.address.empty()) { + errors.push_back(ctx + + ": address is empty — login server cannot " + "route session to this realm"); + } + // Address must contain a colon-separated port. + if (!e.address.empty() && + e.address.find(':') == std::string::npos) { + warnings.push_back(ctx + ": address '" + e.address + + "' has no port — login client typically " + "expects 'host:port' form (defaults to " + "8085 if absent)"); + } + // Validate realmType against known enum values. + using R = wowee::pipeline::WoweeRealmList; + if (e.realmType != R::Normal && e.realmType != R::PvP && + e.realmType != R::RP && e.realmType != R::RPPvP && + e.realmType != R::Test) { + errors.push_back(ctx + ": realmType " + + std::to_string(e.realmType) + + " is not a known value (0/1/4/6/8)"); + } + if (e.realmCategory > 3) { + errors.push_back(ctx + ": realmCategory " + + std::to_string(e.realmCategory) + + " out of range (must be 0..3)"); + } + if (e.expansion > 3) { + errors.push_back(ctx + ": expansion " + + std::to_string(e.expansion) + + " out of range (must be 0..3)"); + } + if (e.population > 4) { + errors.push_back(ctx + ": population " + + std::to_string(e.population) + + " out of range (must be 0..4)"); + } + if (e.characterCap == 0) { + errors.push_back(ctx + + ": characterCap=0 — players can't create " + "any character on this realm"); + } + // Build number sanity check — known WoW build + // numbers are at least 5000. + if (e.buildNumber > 0 && e.buildNumber < 5000) { + warnings.push_back(ctx + ": buildNumber " + + std::to_string(e.buildNumber) + + " < 5000 — known WoW client builds start " + "at 5875 (Vanilla 1.12.1)"); + } + // Realm names must be unique on the picker. + if (!namesSeen.insert(e.name).second) { + errors.push_back(ctx + + ": duplicate realm name '" + e.name + + "' — picker requires unique display names"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.realmId) { + errors.push_back(ctx + ": duplicate realmId"); + break; + } + } + idsSeen.push_back(e.realmId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wmsp"] = base + ".wmsp"; + 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-wmsp: %s.wmsp\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu realms, all realmIds + names " + "unique\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 handleRealmListCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-msp") == 0 && i + 1 < argc) { + outRc = handleGenSingle(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-msp-cluster") == 0 && i + 1 < argc) { + outRc = handleGenCluster(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-msp-multi") == 0 && i + 1 < argc) { + outRc = handleGenMultiExp(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wmsp") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wmsp") == 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_realm_list_catalog.hpp b/tools/editor/cli_realm_list_catalog.hpp new file mode 100644 index 00000000..d6180581 --- /dev/null +++ b/tools/editor/cli_realm_list_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleRealmListCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee