From d332229a798650c5238f037ef1d75e410b5ea6b4 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 19:32:15 -0700 Subject: [PATCH] feat(pipeline): add WWUI (Wowee World-State UI) catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 48th open format — replaces WorldStateUI.dbc plus the AzerothCore-style world_state SQL data. Defines on-screen UI elements that surface server-side world-state variables: BG scoreboards (flag captures, base controls), Wintergrasp tank counters, Eye of the Storm flag-carrier indicator, dungeon boss progress, world-event collection trackers. Each entry binds a server-side variableIndex to a UI panel kind (counter / timer / flag-icon / progress-bar / two-sided score / custom) gated by mapId+areaId, with optional alwaysVisible and hideWhenZero flags and a chosen panel position (top / bottom / top-left / top-right / center). Cross-references with prior formats — mapId points at WMS.mapId and areaId points at WMS.areaId. CLI: --gen-wsui (3-entry BG scoreboard starter for WSG/AB/ EotS), --gen-wsui-wintergrasp (4-entry full Wintergrasp UI), --gen-wsui-dungeon (3-entry boss/keys/treasure hunt UI), --info-wwui, --validate-wwui with --json variants. Validator catches id=0/duplicates, kind/position out of range, variableIndex=0 warning, alwaysVisible+hideWhenZero conflict warning, and (mapId, variableIndex) collision warning when two entries would read the same server slot on the same map. Also extends --list-formats and --info-magic with WWUI. --- CMakeLists.txt | 3 + include/pipeline/wowee_world_state_ui.hpp | 116 +++++++++ src/pipeline/wowee_world_state_ui.cpp | 275 ++++++++++++++++++++ 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_world_state_ui_catalog.cpp | 258 ++++++++++++++++++ tools/editor/cli_world_state_ui_catalog.hpp | 11 + 10 files changed, 679 insertions(+) create mode 100644 include/pipeline/wowee_world_state_ui.hpp create mode 100644 src/pipeline/wowee_world_state_ui.cpp create mode 100644 tools/editor/cli_world_state_ui_catalog.cpp create mode 100644 tools/editor/cli_world_state_ui_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4902c595..3e790ef1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -635,6 +635,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_liquids.cpp src/pipeline/wowee_animations.cpp src/pipeline/wowee_spell_visuals.cpp + src/pipeline/wowee_world_state_ui.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1421,6 +1422,7 @@ add_executable(wowee_editor tools/editor/cli_format_table.cpp tools/editor/cli_summary_dir.cpp tools/editor/cli_rename_magic.cpp + tools/editor/cli_world_state_ui_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1534,6 +1536,7 @@ add_executable(wowee_editor src/pipeline/wowee_liquids.cpp src/pipeline/wowee_animations.cpp src/pipeline/wowee_spell_visuals.cpp + src/pipeline/wowee_world_state_ui.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_world_state_ui.hpp b/include/pipeline/wowee_world_state_ui.hpp new file mode 100644 index 00000000..0321c51f --- /dev/null +++ b/include/pipeline/wowee_world_state_ui.hpp @@ -0,0 +1,116 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open World State UI catalog (.wwui) — novel +// replacement for Blizzard's WorldStateUI.dbc plus the +// AzerothCore-style world_state SQL data. The 48th open +// format added to the editor. +// +// Defines on-screen UI elements that surface server-side +// world-state variables: battleground scoreboards (flag +// captures, base controls), Wintergrasp tank counters, +// Eye of the Storm flag-carrier indicator, dungeon boss +// progress, world-event collection trackers. Each entry +// binds a server-side variableIndex to a UI panel kind +// (counter / timer / progress bar / flag icon) gated by +// map / area, optionally always-visible, optionally +// hidden when the value is zero. +// +// Cross-references with previously-added formats: +// WWUI.entry.mapId → WMS.map.mapId +// WWUI.entry.areaId → WMS.area.areaId +// +// Binary layout (little-endian): +// magic[4] = "WWUI" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// worldStateId (uint32) +// nameLen + name +// descLen + description +// iconLen + iconPath +// displayKind (uint8) / panelPosition (uint8) / +// alwaysVisible (uint8) / hideWhenZero (uint8) +// mapId (uint32) +// areaId (uint32) +// variableIndex (uint32) +// defaultValue (int32) +// iconColorRGBA (uint32) +struct WoweeWorldStateUI { + enum DisplayKind : uint8_t { + Counter = 0, // numeric counter (e.g. flag captures) + Timer = 1, // mm:ss countdown / countup + FlagIcon = 2, // ally/horde icon (flag carrier) + ProgressBar = 3, // 0..max horizontal bar + TwoSidedScore = 4, // ally vs horde dual counter + Custom = 5, // engine-driven custom widget + }; + + enum PanelPosition : uint8_t { + Top = 0, + Bottom = 1, + TopLeft = 2, + TopRight = 3, + Center = 4, // big middle-screen banner + }; + + struct Entry { + uint32_t worldStateId = 0; + std::string name; + std::string description; + std::string iconPath; + uint8_t displayKind = Counter; + uint8_t panelPosition = Top; + uint8_t alwaysVisible = 0; // 1 = visible while in zone + uint8_t hideWhenZero = 0; // 1 = hide when value=0 + uint32_t mapId = 0; // WMS cross-ref + uint32_t areaId = 0; // WMS cross-ref + uint32_t variableIndex = 0; // server-side var slot + int32_t defaultValue = 0; + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t worldStateId) const; + + static const char* displayKindName(uint8_t k); + static const char* panelPositionName(uint8_t p); +}; + +class WoweeWorldStateUILoader { +public: + static bool save(const WoweeWorldStateUI& cat, + const std::string& basePath); + static WoweeWorldStateUI load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-wsui* variants. + // + // makeStarter — 3 BG scoreboards (Warsong Gulch + // flag captures, Arathi Basin + // resource counters, Eye of the + // Storm flag carrier). + // makeWintergrasp — 4 Wintergrasp UI (alliance + horde + // tank counts, time remaining, + // towers controlled). + // makeDungeon — 3 dungeon UI (boss progress bar, + // key fragments collected, + // treasure hunt counter). + static WoweeWorldStateUI makeStarter(const std::string& catalogName); + static WoweeWorldStateUI makeWintergrasp(const std::string& catalogName); + static WoweeWorldStateUI makeDungeon(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_world_state_ui.cpp b/src/pipeline/wowee_world_state_ui.cpp new file mode 100644 index 00000000..973bbc07 --- /dev/null +++ b/src/pipeline/wowee_world_state_ui.cpp @@ -0,0 +1,275 @@ +#include "pipeline/wowee_world_state_ui.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'W', 'U', 'I'}; +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) != ".wwui") { + base += ".wwui"; + } + return base; +} + +} // namespace + +const WoweeWorldStateUI::Entry* +WoweeWorldStateUI::findById(uint32_t worldStateId) const { + for (const auto& e : entries) + if (e.worldStateId == worldStateId) return &e; + return nullptr; +} + +const char* WoweeWorldStateUI::displayKindName(uint8_t k) { + switch (k) { + case Counter: return "counter"; + case Timer: return "timer"; + case FlagIcon: return "flag-icon"; + case ProgressBar: return "progress-bar"; + case TwoSidedScore: return "two-sided"; + case Custom: return "custom"; + default: return "unknown"; + } +} + +const char* WoweeWorldStateUI::panelPositionName(uint8_t p) { + switch (p) { + case Top: return "top"; + case Bottom: return "bottom"; + case TopLeft: return "top-left"; + case TopRight: return "top-right"; + case Center: return "center"; + default: return "unknown"; + } +} + +bool WoweeWorldStateUILoader::save(const WoweeWorldStateUI& 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.worldStateId); + writeStr(os, e.name); + writeStr(os, e.description); + writeStr(os, e.iconPath); + writePOD(os, e.displayKind); + writePOD(os, e.panelPosition); + writePOD(os, e.alwaysVisible); + writePOD(os, e.hideWhenZero); + writePOD(os, e.mapId); + writePOD(os, e.areaId); + writePOD(os, e.variableIndex); + writePOD(os, e.defaultValue); + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweeWorldStateUI WoweeWorldStateUILoader::load( + const std::string& basePath) { + WoweeWorldStateUI 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.worldStateId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description) || + !readStr(is, e.iconPath)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.displayKind) || + !readPOD(is, e.panelPosition) || + !readPOD(is, e.alwaysVisible) || + !readPOD(is, e.hideWhenZero) || + !readPOD(is, e.mapId) || + !readPOD(is, e.areaId) || + !readPOD(is, e.variableIndex) || + !readPOD(is, e.defaultValue) || + !readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeWorldStateUILoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeWorldStateUI WoweeWorldStateUILoader::makeStarter( + const std::string& catalogName) { + WoweeWorldStateUI c; + c.name = catalogName; + { + // Warsong Gulch — two-sided flag capture counter. + WoweeWorldStateUI::Entry e; + e.worldStateId = 1; e.name = "WSG Flag Captures"; + e.description = "Two-sided counter for flag captures in WSG."; + e.iconPath = "Interface/WorldStateFrame/wsg_flag.blp"; + e.displayKind = WoweeWorldStateUI::TwoSidedScore; + e.panelPosition = WoweeWorldStateUI::Top; + e.alwaysVisible = 1; + e.mapId = 489; // Warsong Gulch + e.areaId = 3277; + e.variableIndex = 1; + c.entries.push_back(e); + } + { + // Arathi Basin — resource counter (5 bases, 0..1600). + WoweeWorldStateUI::Entry e; + e.worldStateId = 2; e.name = "AB Resources"; + e.description = "Two-sided resource counter (0..1600)."; + e.iconPath = "Interface/WorldStateFrame/ab_resource.blp"; + e.displayKind = WoweeWorldStateUI::TwoSidedScore; + e.panelPosition = WoweeWorldStateUI::Top; + e.alwaysVisible = 1; + e.mapId = 529; // Arathi Basin + e.areaId = 3358; + e.variableIndex = 2; + c.entries.push_back(e); + } + { + // EotS — flag carrier icon (single-sided, top-right). + WoweeWorldStateUI::Entry e; + e.worldStateId = 3; e.name = "EotS Flag Carrier"; + e.description = "Flag carrier icon (Alliance/Horde) shown " + "while a player holds the EotS flag."; + e.iconPath = "Interface/WorldStateFrame/eots_flag.blp"; + e.displayKind = WoweeWorldStateUI::FlagIcon; + e.panelPosition = WoweeWorldStateUI::TopRight; + e.hideWhenZero = 1; + e.mapId = 566; // Eye of the Storm + e.areaId = 3820; + e.variableIndex = 3; + c.entries.push_back(e); + } + return c; +} + +WoweeWorldStateUI WoweeWorldStateUILoader::makeWintergrasp( + const std::string& catalogName) { + WoweeWorldStateUI c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t kind, + uint8_t pos, uint32_t variableIndex, + int32_t defaultVal, uint8_t alwaysVis, + const char* desc) { + WoweeWorldStateUI::Entry e; + e.worldStateId = id; e.name = name; e.description = desc; + e.iconPath = std::string("Interface/WorldStateFrame/wg_") + + name + ".blp"; + e.displayKind = kind; + e.panelPosition = pos; + e.variableIndex = variableIndex; + e.defaultValue = defaultVal; + e.alwaysVisible = alwaysVis; + e.mapId = 571; // Northrend + e.areaId = 4197; // Wintergrasp + c.entries.push_back(e); + }; + add(100, "AllianceTanks", WoweeWorldStateUI::Counter, + WoweeWorldStateUI::TopLeft, 10, 0, 1, + "Number of Alliance siege tanks deployed."); + add(101, "HordeTanks", WoweeWorldStateUI::Counter, + WoweeWorldStateUI::TopRight, 11, 0, 1, + "Number of Horde siege tanks deployed."); + add(102, "TimeRemaining", WoweeWorldStateUI::Timer, + WoweeWorldStateUI::Top, 12, 1800, 1, + "Seconds remaining in the current battle."); + add(103, "TowersControlled", WoweeWorldStateUI::TwoSidedScore, + WoweeWorldStateUI::Bottom, 13, 0, 1, + "Towers controlled by each faction (0..3)."); + return c; +} + +WoweeWorldStateUI WoweeWorldStateUILoader::makeDungeon( + const std::string& catalogName) { + WoweeWorldStateUI c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t kind, + uint32_t mapId, uint32_t variableIndex, + int32_t defaultVal, uint8_t hideZero, + const char* desc) { + WoweeWorldStateUI::Entry e; + e.worldStateId = id; e.name = name; e.description = desc; + e.iconPath = std::string("Interface/WorldStateFrame/dungeon_") + + name + ".blp"; + e.displayKind = kind; + e.panelPosition = WoweeWorldStateUI::TopRight; + e.mapId = mapId; + e.variableIndex = variableIndex; + e.defaultValue = defaultVal; + e.hideWhenZero = hideZero; + c.entries.push_back(e); + }; + add(200, "BossProgress", WoweeWorldStateUI::ProgressBar, + 533, 20, 0, 0, // Naxxramas + "Progress bar — bosses defeated this run."); + add(201, "KeyFragments", WoweeWorldStateUI::Counter, + 540, 21, 0, 1, // Shattered Halls + "Key fragments collected for the dungeon door."); + add(202, "TreasureHunt", WoweeWorldStateUI::Counter, + 429, 22, 0, 1, // Dire Maul + "Hidden treasures collected during the run."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 6197446f..50c84d5d 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -141,6 +141,8 @@ const char* const kArgRequired[] = { "--gen-svk", "--gen-svk-combat", "--gen-svk-utility", "--info-wsvk", "--validate-wsvk", "--export-wsvk-json", "--import-wsvk-json", + "--gen-wsui", "--gen-wsui-wintergrasp", "--gen-wsui-dungeon", + "--info-wwui", "--validate-wwui", "--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 09e2c7b5..e5323f04 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -79,6 +79,7 @@ #include "cli_spell_visuals_catalog.hpp" #include "cli_summary_dir.hpp" #include "cli_rename_magic.hpp" +#include "cli_world_state_ui_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -199,6 +200,7 @@ constexpr DispatchFn kDispatchTable[] = { handleSpellVisualsCatalog, handleSummaryDir, handleRenameMagic, + handleWorldStateUICatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 1e3cf2f7..66f303fe 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -49,6 +49,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','L','I','Q'}, ".wliq", "liquids", "--info-wliq", "Liquid catalog"}, {{'W','A','N','I'}, ".wani", "anim", "--info-wani", "Animation catalog"}, {{'W','S','V','K'}, ".wsvk", "spellfx", "--info-wsvk", "Spell visual kit catalog"}, + {{'W','W','U','I'}, ".wwui", "ui", "--info-wwui", "World-state UI 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 f103945e..dcf7dbd3 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1383,6 +1383,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wsvk to a human-editable JSON sidecar (defaults to .wsvk.json)\n"); std::printf(" --import-wsvk-json [out-base]\n"); std::printf(" Import a .wsvk.json sidecar back into binary .wsvk\n"); + std::printf(" --gen-wsui [name]\n"); + std::printf(" Emit .wwui starter: 3 BG scoreboards (WSG flag captures, AB resources, EotS flag carrier)\n"); + std::printf(" --gen-wsui-wintergrasp [name]\n"); + std::printf(" Emit .wwui 4 Wintergrasp UI (alliance+horde tank counts, time remaining, towers controlled)\n"); + std::printf(" --gen-wsui-dungeon [name]\n"); + std::printf(" Emit .wwui 3 dungeon UI (boss progress bar, key fragments collected, treasure hunt counter)\n"); + std::printf(" --info-wwui [--json]\n"); + std::printf(" Print WWUI entries (id / kind / panel position / always-visible / hide-when-zero / map+area / variableIndex / default / name)\n"); + std::printf(" --validate-wwui [--json]\n"); + std::printf(" Static checks: id>0+unique, name not empty, kind 0..5, position 0..4, alwaysVis+hideZero conflict, (mapId, varIdx) collision warning\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_list_formats.cpp b/tools/editor/cli_list_formats.cpp index 7a3cae46..10dc91b0 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -71,6 +71,7 @@ constexpr FormatRow kFormats[] = { {"WLIQ", ".wliq", "liquids", "LiquidType.dbc", "Liquid material catalog (water/lava/slime)"}, {"WANI", ".wani", "anim", "AnimationData.dbc", "Animation ID + fallback + weapon-flag catalog"}, {"WSVK", ".wsvk", "spellfx", "SpellVisualKit.dbc + SpellVisFx", "Spell visual kit (cast/proj/impact effects)"}, + {"WWUI", ".wwui", "ui", "WorldStateUI.dbc + world_state", "World-state UI (BG scoreboards / siege counters)"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine diff --git a/tools/editor/cli_world_state_ui_catalog.cpp b/tools/editor/cli_world_state_ui_catalog.cpp new file mode 100644 index 00000000..bb64405b --- /dev/null +++ b/tools/editor/cli_world_state_ui_catalog.cpp @@ -0,0 +1,258 @@ +#include "cli_world_state_ui_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_world_state_ui.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWwuiExt(std::string base) { + stripExt(base, ".wwui"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeWorldStateUI& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeWorldStateUILoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wwui\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeWorldStateUI& c, + const std::string& base) { + std::printf("Wrote %s.wwui\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" states : %zu\n", c.entries.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterWorldStateUI"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWwuiExt(base); + auto c = wowee::pipeline::WoweeWorldStateUILoader::makeStarter(name); + if (!saveOrError(c, base, "gen-wsui")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenWintergrasp(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "WintergraspUI"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWwuiExt(base); + auto c = wowee::pipeline::WoweeWorldStateUILoader::makeWintergrasp(name); + if (!saveOrError(c, base, "gen-wsui-wintergrasp")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenDungeon(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "DungeonUI"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWwuiExt(base); + auto c = wowee::pipeline::WoweeWorldStateUILoader::makeDungeon(name); + if (!saveOrError(c, base, "gen-wsui-dungeon")) 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 = stripWwuiExt(base); + if (!wowee::pipeline::WoweeWorldStateUILoader::exists(base)) { + std::fprintf(stderr, "WWUI not found: %s.wwui\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeWorldStateUILoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wwui"] = base + ".wwui"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"worldStateId", e.worldStateId}, + {"name", e.name}, + {"description", e.description}, + {"iconPath", e.iconPath}, + {"displayKind", e.displayKind}, + {"displayKindName", wowee::pipeline::WoweeWorldStateUI::displayKindName(e.displayKind)}, + {"panelPosition", e.panelPosition}, + {"panelPositionName", wowee::pipeline::WoweeWorldStateUI::panelPositionName(e.panelPosition)}, + {"alwaysVisible", e.alwaysVisible}, + {"hideWhenZero", e.hideWhenZero}, + {"mapId", e.mapId}, + {"areaId", e.areaId}, + {"variableIndex", e.variableIndex}, + {"defaultValue", e.defaultValue}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WWUI: %s.wwui\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" states : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id kind pos always hideZ map area var default name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %-13s %-9s %u %u %5u %5u %3u %6d %s\n", + e.worldStateId, + wowee::pipeline::WoweeWorldStateUI::displayKindName(e.displayKind), + wowee::pipeline::WoweeWorldStateUI::panelPositionName(e.panelPosition), + e.alwaysVisible, e.hideWhenZero, + e.mapId, e.areaId, e.variableIndex, + e.defaultValue, 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 = stripWwuiExt(base); + if (!wowee::pipeline::WoweeWorldStateUILoader::exists(base)) { + std::fprintf(stderr, + "validate-wwui: WWUI not found: %s.wwui\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeWorldStateUILoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + std::vector varsSeen; + 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.worldStateId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.worldStateId == 0) + errors.push_back(ctx + ": worldStateId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.displayKind > wowee::pipeline::WoweeWorldStateUI::Custom) { + errors.push_back(ctx + ": displayKind " + + std::to_string(e.displayKind) + " not in 0..5"); + } + if (e.panelPosition > wowee::pipeline::WoweeWorldStateUI::Center) { + errors.push_back(ctx + ": panelPosition " + + std::to_string(e.panelPosition) + " not in 0..4"); + } + if (e.variableIndex == 0) { + warnings.push_back(ctx + ": variableIndex=0 " + "(not bound to a server-side variable)"); + } + // alwaysVisible + hideWhenZero is contradictory — + // hide-when-zero implicitly negates always-visible + // when the value happens to be 0. + if (e.alwaysVisible && e.hideWhenZero) { + warnings.push_back(ctx + + ": both alwaysVisible and hideWhenZero set " + "(hideWhenZero wins when value=0)"); + } + // Two world-state entries sharing the same + // (mapId, variableIndex) pair conflict — they'd both + // try to read the same server slot at the same time. + for (size_t m = 0; m < k; ++m) { + const auto& other = c.entries[m]; + if (other.mapId == e.mapId && e.mapId != 0 && + other.variableIndex == e.variableIndex && + e.variableIndex != 0) { + warnings.push_back(ctx + + ": shares (mapId=" + std::to_string(e.mapId) + + ", variableIndex=" + + std::to_string(e.variableIndex) + + ") with entry " + std::to_string(m) + + " — values will collide"); + break; + } + } + for (uint32_t prev : idsSeen) { + if (prev == e.worldStateId) { + errors.push_back(ctx + ": duplicate worldStateId"); + break; + } + } + idsSeen.push_back(e.worldStateId); + varsSeen.push_back(e.variableIndex); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wwui"] = base + ".wwui"; + 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-wwui: %s.wwui\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu world states, all worldStateIds 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 handleWorldStateUICatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-wsui") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-wsui-wintergrasp") == 0 && + i + 1 < argc) { + outRc = handleGenWintergrasp(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-wsui-dungeon") == 0 && + i + 1 < argc) { + outRc = handleGenDungeon(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wwui") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wwui") == 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_world_state_ui_catalog.hpp b/tools/editor/cli_world_state_ui_catalog.hpp new file mode 100644 index 00000000..f3de609c --- /dev/null +++ b/tools/editor/cli_world_state_ui_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleWorldStateUICatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee