diff --git a/CMakeLists.txt b/CMakeLists.txt index 4948811c..03f7fdc6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -685,6 +685,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_stat_curves.cpp src/pipeline/wowee_action_bars.cpp src/pipeline/wowee_group_compositions.cpp + src/pipeline/wowee_hearth_binds.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1533,6 +1534,7 @@ add_executable(wowee_editor tools/editor/cli_stat_curves_catalog.cpp tools/editor/cli_action_bars_catalog.cpp tools/editor/cli_group_compositions_catalog.cpp + tools/editor/cli_hearth_binds_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1696,6 +1698,7 @@ add_executable(wowee_editor src/pipeline/wowee_stat_curves.cpp src/pipeline/wowee_action_bars.cpp src/pipeline/wowee_group_compositions.cpp + src/pipeline/wowee_hearth_binds.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_hearth_binds.hpp b/include/pipeline/wowee_hearth_binds.hpp new file mode 100644 index 00000000..19de5bc0 --- /dev/null +++ b/include/pipeline/wowee_hearth_binds.hpp @@ -0,0 +1,133 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Hearth Bind Point catalog (.whrt) — novel +// replacement for the hardcoded list of hearthstone bind +// locations used by the SMSG_BINDPOINTUPDATE flow. Each +// entry is one valid bind point: a tavern innkeeper, a +// special bind NPC (Karazhan port, Shattered Sun base, +// guild-hall bind clerk), or a city portal-room +// quartermaster. The client uses this catalog to pin +// hearth icons on the world map and to render the +// "Hearthstone bound to: " tooltip text. +// +// Cross-references with previously-added formats: +// WMS: mapId references the WMS map; areaId references +// the WMS sub-area entry. +// WCRT: npcId references the innkeeper / bind NPC in +// the WCRT creature catalog. +// WCHC: faction filter uses the WCHC faction-mask bits +// (1=Alliance, 2=Horde, 3=Both). +// +// Binary layout (little-endian): +// magic[4] = "WHRT" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// bindId (uint32) +// nameLen + name +// descLen + description +// mapId (uint32) / areaId (uint32) +// x (float) / y (float) / z (float) +// facing (float, radians) +// npcId (uint32) — 0 if no NPC bind clerk +// factionMask (uint8) — 1=A / 2=H / 3=Both +// bindKind (uint8) — Inn / Capital / Quest / +// Guild / SpecialPort +// levelMin (uint8) — earliest level allowed +// to bind here (0 = any) +// pad0 (uint8) +// iconColorRGBA (uint32) +struct WoweeHearthBinds { + enum BindKind : uint8_t { + Inn = 0, // tavern innkeeper + Capital = 1, // city portal-room or + // capital-hall bind clerk + Quest = 2, // quest-given bind reward + // (Theramore, Wyrmrest) + Guild = 3, // guild-hall bind point + SpecialPort = 4, // raid port (Karazhan, + // Karazhan Crypts, Tempest + // Keep) + Faction = 5, // faction-base bind (SSO + // Sunwell, Argent Tournament) + }; + + enum FactionMask : uint8_t { + AllianceOnly = 1, + HordeOnly = 2, + Both = 3, + }; + + struct Entry { + uint32_t bindId = 0; + std::string name; + std::string description; + uint32_t mapId = 0; + uint32_t areaId = 0; + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + float facing = 0.0f; + uint32_t npcId = 0; + uint8_t factionMask = Both; + uint8_t bindKind = Inn; + uint8_t levelMin = 0; + uint8_t pad0 = 0; + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t bindId) const; + + // Returns all bind points available to a player of the + // given faction (Alliance=1, Horde=2). Bindings with + // factionMask=3 (Both) are returned for either query. + // Used by the world-map UI to filter the inn-icon + // overlay layer per character. + std::vector findByFaction(uint8_t playerFaction) const; + + // Returns all bind points within a given map (for the + // continent-level inn overlay). + std::vector findByMap(uint32_t mapId) const; +}; + +class WoweeHearthBindsLoader { +public: + static bool save(const WoweeHearthBinds& cat, + const std::string& basePath); + static WoweeHearthBinds load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-hrt* variants. + // + // makeStarterCities — 4 entries (Stormwind / Ironforge + // / Orgrimmar / Thunder Bluff + // innkeepers, faction-gated). + // makeCapitals — 6 entries (Stormwind / Ironforge + // / Darnassus / Orgrimmar / + // Undercity / Thunder Bluff + // capital-hall bind clerks). + // makeStarterInns — 8 entries (mix of starter-zone + // inns: Goldshire / Brill / + // Razor Hill / Bloodhoof Village + // / Kharanos / Aldrassil / + // Shadowglen / Sun Rock Retreat). + static WoweeHearthBinds makeStarterCities(const std::string& catalogName); + static WoweeHearthBinds makeCapitals(const std::string& catalogName); + static WoweeHearthBinds makeStarterInns(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_hearth_binds.cpp b/src/pipeline/wowee_hearth_binds.cpp new file mode 100644 index 00000000..760edd68 --- /dev/null +++ b/src/pipeline/wowee_hearth_binds.cpp @@ -0,0 +1,333 @@ +#include "pipeline/wowee_hearth_binds.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'H', 'R', 'T'}; +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) != ".whrt") { + base += ".whrt"; + } + 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 WoweeHearthBinds::Entry* +WoweeHearthBinds::findById(uint32_t bindId) const { + for (const auto& e : entries) + if (e.bindId == bindId) return &e; + return nullptr; +} + +std::vector +WoweeHearthBinds::findByFaction(uint8_t playerFaction) const { + std::vector out; + for (const auto& e : entries) { + if (e.factionMask & playerFaction) out.push_back(&e); + } + return out; +} + +std::vector +WoweeHearthBinds::findByMap(uint32_t mapId) const { + std::vector out; + for (const auto& e : entries) + if (e.mapId == mapId) out.push_back(&e); + return out; +} + +bool WoweeHearthBindsLoader::save(const WoweeHearthBinds& 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.bindId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.mapId); + writePOD(os, e.areaId); + writePOD(os, e.x); + writePOD(os, e.y); + writePOD(os, e.z); + writePOD(os, e.facing); + writePOD(os, e.npcId); + writePOD(os, e.factionMask); + writePOD(os, e.bindKind); + writePOD(os, e.levelMin); + writePOD(os, e.pad0); + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweeHearthBinds WoweeHearthBindsLoader::load( + const std::string& basePath) { + WoweeHearthBinds 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.bindId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.mapId) || + !readPOD(is, e.areaId) || + !readPOD(is, e.x) || + !readPOD(is, e.y) || + !readPOD(is, e.z) || + !readPOD(is, e.facing) || + !readPOD(is, e.npcId) || + !readPOD(is, e.factionMask) || + !readPOD(is, e.bindKind) || + !readPOD(is, e.levelMin) || + !readPOD(is, e.pad0) || + !readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeHearthBindsLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeHearthBinds WoweeHearthBindsLoader::makeStarterCities( + const std::string& catalogName) { + using H = WoweeHearthBinds; + WoweeHearthBinds c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t map, + uint32_t area, float x, float y, float z, float f, + uint32_t npc, uint8_t faction, + const char* desc) { + H::Entry e; + e.bindId = id; e.name = name; e.description = desc; + e.mapId = map; e.areaId = area; + e.x = x; e.y = y; e.z = z; e.facing = f; + e.npcId = npc; + e.factionMask = faction; + e.bindKind = H::Inn; + e.levelMin = 1; + e.iconColorRGBA = packRgba(255, 220, 100); // gold inn + c.entries.push_back(e); + }; + // Eastern Kingdoms (mapId=0): Stormwind Inn (Old Town). + add(1, "StormwindInn", 0, 1519, + -8843.0f, 645.0f, 95.0f, 1.5f, + 6739, H::AllianceOnly, + "Pig and Whistle Tavern — Stormwind Old Town. " + "Allerian Holimion is the local innkeeper."); + // Eastern Kingdoms: Ironforge Inn (Forlorn Cavern). + add(2, "IronforgeInn", 0, 1537, + -4862.0f, -872.0f, 502.0f, 4.7f, + 6741, H::AllianceOnly, + "Stonefire Tavern — Ironforge Commons. Inn-keeper " + "Firebrew serves the Wildhammer dwarves passing " + "through."); + // Kalimdor (mapId=1): Orgrimmar Inn (Valley of Strength). + add(3, "OrgrimmarInn", 1, 1637, + 1665.0f, -4326.0f, 60.0f, 1.0f, + 6929, H::HordeOnly, + "Wreckin' Ball Tavern — Valley of Strength. " + "Innkeeper Gryshka serves the Horde travelers " + "arriving from the eastern continents."); + // Kalimdor: Thunder Bluff Inn (Lower Rise). + add(4, "ThunderBluffInn", 1, 1638, + -1290.0f, 161.0f, 130.0f, 4.7f, + 6746, H::HordeOnly, + "Thunder Bluff Inn — Lower Rise. Innkeeper Pala " + "serves the Tauren and visiting Horde."); + return c; +} + +WoweeHearthBinds WoweeHearthBindsLoader::makeCapitals( + const std::string& catalogName) { + using H = WoweeHearthBinds; + WoweeHearthBinds c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t map, + uint32_t area, float x, float y, float z, float f, + uint32_t npc, uint8_t faction, + const char* desc) { + H::Entry e; + e.bindId = id; e.name = name; e.description = desc; + e.mapId = map; e.areaId = area; + e.x = x; e.y = y; e.z = z; e.facing = f; + e.npcId = npc; + e.factionMask = faction; + e.bindKind = H::Capital; + e.levelMin = 10; + e.iconColorRGBA = packRgba(140, 200, 255); // capital blue + c.entries.push_back(e); + }; + add(100, "StormwindKeepBind", 0, 1519, + -8866.0f, 671.0f, 97.0f, 1.5f, + 7232, H::AllianceOnly, + "Stormwind Keep bind clerk — used by Alliance " + "officers and quest chains that grant capital-bind " + "as a reward."); + add(101, "IronforgeBind", 0, 1537, + -4924.0f, -955.0f, 501.0f, 0.0f, + 13283, H::AllianceOnly, + "Ironforge royal hall bind clerk — used by dwarven " + "Magni quest line."); + add(102, "DarnassusBind", 1, 1657, + 9947.0f, 2516.0f, 1330.0f, 4.5f, + 7301, H::AllianceOnly, + "Darnassus Temple of the Moon bind clerk — kaldorei " + "lore quest line completion reward."); + add(103, "OrgrimmarGrommashHold", 1, 1637, + 1633.0f, -4439.0f, 16.0f, 0.5f, + 7236, H::HordeOnly, + "Orgrimmar Grommash Hold bind clerk — Horde " + "officer hall, requires honored standing with " + "Orgrimmar."); + add(104, "UndercityBind", 0, 1497, + 1633.0f, 240.0f, -50.0f, 1.5f, + 13208, H::HordeOnly, + "Undercity Royal Quarters bind clerk — Forsaken " + "lore quest line reward."); + add(105, "ThunderBluffBind", 1, 1638, + -1271.0f, 80.0f, 128.0f, 5.0f, + 13284, H::HordeOnly, + "Thunder Bluff High Rise bind clerk — Tauren " + "elder lore quest reward."); + return c; +} + +WoweeHearthBinds WoweeHearthBindsLoader::makeStarterInns( + const std::string& catalogName) { + using H = WoweeHearthBinds; + WoweeHearthBinds c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint32_t map, + uint32_t area, float x, float y, float z, float f, + uint32_t npc, uint8_t faction, + const char* desc) { + H::Entry e; + e.bindId = id; e.name = name; e.description = desc; + e.mapId = map; e.areaId = area; + e.x = x; e.y = y; e.z = z; e.facing = f; + e.npcId = npc; + e.factionMask = faction; + e.bindKind = H::Inn; + e.levelMin = 1; + e.iconColorRGBA = packRgba(200, 160, 80); // tavern brown + c.entries.push_back(e); + }; + // Alliance starter inns. + add(200, "GoldshireLionsPride", 0, 9, + -9460.0f, 64.0f, 56.0f, 0.0f, + 6740, H::AllianceOnly, + "Lion's Pride Inn — Goldshire, Elwynn Forest. " + "Innkeeper Farley serves the human starter zone."); + add(201, "BrillGallowsEnd", 0, 85, + 2266.0f, 286.0f, 35.0f, 1.5f, + 6747, H::HordeOnly, + "Gallows' End Tavern — Brill, Tirisfal Glades. " + "Innkeeper Renee Renee serves the Forsaken " + "starter zone."); + add(202, "RazorHillInn", 1, 362, + 345.0f, -4710.0f, 16.0f, 0.0f, + 6748, H::HordeOnly, + "Razor Hill Inn — Durotar. Innkeeper Grosk " + "serves the orc/troll starter zone."); + add(203, "BloodhoofVillageInn", 1, 222, + -2370.0f, -370.0f, -10.0f, 4.5f, + 6929, H::HordeOnly, + "Bloodhoof Village Inn — Mulgore. Innkeeper " + "Kauth serves the Tauren starter zone."); + add(204, "KharanosThunderBrew", 0, 132, + -5605.0f, -480.0f, 400.0f, 1.5f, + 6735, H::AllianceOnly, + "Thunderbrew Distillery — Kharanos, Dun Morogh. " + "Innkeeper Belm serves the dwarf/gnome starter " + "zone."); + add(205, "AldrassilStarbreezeInn", 1, 188, + 10318.0f, 829.0f, 1326.0f, 1.0f, + 6736, H::AllianceOnly, + "Starbreeze Village Inn — Teldrassil. Innkeeper " + "Saelienne serves the night elf starter zone."); + add(206, "ShadowglenInn", 1, 188, + 10311.0f, 822.0f, 1326.0f, 1.0f, + 6737, H::AllianceOnly, + "Shadowglen Inn — Teldrassil. The first inn night " + "elf characters can bind to (level 5+)."); + add(207, "SunRockRetreatInn", 1, 405, + -2392.0f, -1992.0f, 95.0f, 0.5f, + 6738, H::HordeOnly, + "Sun Rock Retreat Inn — Stonetalon Mountains. " + "Innkeeper Heather serves the Tauren level 15-25 " + "Horde travelers."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index a30a6f69..8708f048 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -297,6 +297,8 @@ const char* const kArgRequired[] = { "--gen-grp", "--gen-grp-raid10", "--gen-grp-raid25", "--info-wgrp", "--validate-wgrp", "--export-wgrp-json", "--import-wgrp-json", + "--gen-hrt", "--gen-hrt-capitals", "--gen-hrt-inns", + "--info-whrt", "--validate-whrt", "--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 07225fa8..0e5b253f 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -141,6 +141,7 @@ #include "cli_stat_curves_catalog.hpp" #include "cli_action_bars_catalog.hpp" #include "cli_group_compositions_catalog.hpp" +#include "cli_hearth_binds_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -323,6 +324,7 @@ constexpr DispatchFn kDispatchTable[] = { handleStatCurvesCatalog, handleActionBarsCatalog, handleGroupCompositionsCatalog, + handleHearthBindsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index 8014f171..1da99103 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -99,6 +99,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','S','T','M'}, ".wstm", "stats", "--info-wstm", "Stat modifier curve catalog"}, {{'W','A','C','T'}, ".wact", "ui", "--info-wact", "Action bar layout catalog"}, {{'W','G','R','P'}, ".wgrp", "social", "--info-wgrp", "Group composition catalog"}, + {{'W','H','R','T'}, ".whrt", "social", "--info-whrt", "Hearthstone bind point 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_hearth_binds_catalog.cpp b/tools/editor/cli_hearth_binds_catalog.cpp new file mode 100644 index 00000000..caf64508 --- /dev/null +++ b/tools/editor/cli_hearth_binds_catalog.cpp @@ -0,0 +1,276 @@ +#include "cli_hearth_binds_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_hearth_binds.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWhrtExt(std::string base) { + stripExt(base, ".whrt"); + return base; +} + +const char* bindKindName(uint8_t k) { + using H = wowee::pipeline::WoweeHearthBinds; + switch (k) { + case H::Inn: return "inn"; + case H::Capital: return "capital"; + case H::Quest: return "quest"; + case H::Guild: return "guild"; + case H::SpecialPort: return "specialport"; + case H::Faction: return "faction"; + default: return "unknown"; + } +} + +const char* factionMaskName(uint8_t f) { + using H = wowee::pipeline::WoweeHearthBinds; + switch (f) { + case H::AllianceOnly: return "alliance"; + case H::HordeOnly: return "horde"; + case H::Both: return "both"; + default: return "unknown"; + } +} + +bool saveOrError(const wowee::pipeline::WoweeHearthBinds& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeHearthBindsLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.whrt\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeHearthBinds& c, + const std::string& base) { + std::printf("Wrote %s.whrt\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" binds : %zu\n", c.entries.size()); +} + +int handleGenStarterCities(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterCityBinds"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWhrtExt(base); + auto c = wowee::pipeline::WoweeHearthBindsLoader::makeStarterCities(name); + if (!saveOrError(c, base, "gen-hrt")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenCapitals(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "CapitalBinds"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWhrtExt(base); + auto c = wowee::pipeline::WoweeHearthBindsLoader::makeCapitals(name); + if (!saveOrError(c, base, "gen-hrt-capitals")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenStarterInns(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterInns"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWhrtExt(base); + auto c = wowee::pipeline::WoweeHearthBindsLoader::makeStarterInns(name); + if (!saveOrError(c, base, "gen-hrt-inns")) 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 = stripWhrtExt(base); + if (!wowee::pipeline::WoweeHearthBindsLoader::exists(base)) { + std::fprintf(stderr, "WHRT not found: %s.whrt\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeHearthBindsLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["whrt"] = base + ".whrt"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"bindId", e.bindId}, + {"name", e.name}, + {"description", e.description}, + {"mapId", e.mapId}, + {"areaId", e.areaId}, + {"x", e.x}, {"y", e.y}, {"z", e.z}, + {"facing", e.facing}, + {"npcId", e.npcId}, + {"factionMask", e.factionMask}, + {"factionMaskName", factionMaskName(e.factionMask)}, + {"bindKind", e.bindKind}, + {"bindKindName", bindKindName(e.bindKind)}, + {"levelMin", e.levelMin}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WHRT: %s.whrt\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" binds : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id map area faction kind npc lvl name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %4u %4u %-9s %-12s %5u %3u %s\n", + e.bindId, e.mapId, e.areaId, + factionMaskName(e.factionMask), + bindKindName(e.bindKind), + e.npcId, e.levelMin, 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 = stripWhrtExt(base); + if (!wowee::pipeline::WoweeHearthBindsLoader::exists(base)) { + std::fprintf(stderr, + "validate-whrt: WHRT not found: %s.whrt\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeHearthBindsLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + 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.bindId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.bindId == 0) + errors.push_back(ctx + ": bindId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.factionMask == 0 || e.factionMask > 3) { + errors.push_back(ctx + ": factionMask " + + std::to_string(e.factionMask) + + " out of range (must be 1=A / 2=H / 3=Both)"); + } + if (e.bindKind > 5) { + errors.push_back(ctx + ": bindKind " + + std::to_string(e.bindKind) + + " out of range (must be 0..5)"); + } + // Bind position must not be at origin (probably + // unset). Origin coords often indicate a forgotten + // SetPosition call in a content authoring tool. + if (e.x == 0.0f && e.y == 0.0f && e.z == 0.0f) { + warnings.push_back(ctx + + ": position is (0,0,0) — likely forgotten " + "SetPosition; bind would teleport player to " + "world origin"); + } + // Inn-kind bindings should have an NPC bind clerk + // (the innkeeper). SpecialPort bindings often + // don't. Warn if Inn and npcId=0. + using H = wowee::pipeline::WoweeHearthBinds; + if (e.bindKind == H::Inn && e.npcId == 0) { + warnings.push_back(ctx + + ": Inn bind has no NPC innkeeper (npcId=0). " + "Inn bindings should reference the WCRT " + "innkeeper entry."); + } + // Quest-given bindings without level gate are + // suspicious — quest binds usually require level + // (Theramore at 30+, Wyrmrest at 70+). + if (e.bindKind == H::Quest && e.levelMin == 0) { + warnings.push_back(ctx + + ": Quest bind has levelMin=0 — quest " + "bindings usually have a minimum level " + "gate; verify if intentional"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.bindId) { + errors.push_back(ctx + ": duplicate bindId"); + break; + } + } + idsSeen.push_back(e.bindId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["whrt"] = base + ".whrt"; + 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-whrt: %s.whrt\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu binds, all bindIds 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 handleHearthBindsCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-hrt") == 0 && i + 1 < argc) { + outRc = handleGenStarterCities(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-hrt-capitals") == 0 && i + 1 < argc) { + outRc = handleGenCapitals(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-hrt-inns") == 0 && i + 1 < argc) { + outRc = handleGenStarterInns(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-whrt") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-whrt") == 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_hearth_binds_catalog.hpp b/tools/editor/cli_hearth_binds_catalog.hpp new file mode 100644 index 00000000..4563d5f9 --- /dev/null +++ b/tools/editor/cli_hearth_binds_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleHearthBindsCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 64100633..cfadf1fc 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -2111,6 +2111,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wgrp to a human-editable JSON sidecar (defaults to .wgrp.json; sizeCategory string is informational, ignored on import)\n"); std::printf(" --import-wgrp-json [out-base]\n"); std::printf(" Import a .wgrp.json sidecar back into binary .wgrp (requireSpec accepts bool OR int)\n"); + std::printf(" --gen-hrt [name]\n"); + std::printf(" Emit .whrt 4 starter-city innkeepers (Stormwind / Ironforge / Orgrimmar / Thunder Bluff) faction-gated\n"); + std::printf(" --gen-hrt-capitals [name]\n"); + std::printf(" Emit .whrt 6 capital-hall bind clerks (Stormwind / Ironforge / Darnassus / Orgrimmar / Undercity / Thunder Bluff)\n"); + std::printf(" --gen-hrt-inns [name]\n"); + std::printf(" Emit .whrt 8 starter-zone inns (Goldshire / Brill / Razor Hill / Bloodhoof Village / Kharanos / Aldrassil / Shadowglen / Sun Rock Retreat)\n"); + std::printf(" --info-whrt [--json]\n"); + std::printf(" Print WHRT entries (id / map / area / faction / kind / npc / levelMin / name)\n"); + std::printf(" --validate-whrt [--json]\n"); + std::printf(" Static checks: id+name required, factionMask 1..3, bindKind 0..5, no duplicate ids; warns on (0,0,0) position, Inn with npcId=0, Quest with levelMin=0\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 4e2d00df..f7e99426 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -121,6 +121,7 @@ constexpr FormatRow kFormats[] = { {"WSTM", ".wstm", "stats", "gtChanceTo*.dbc + gtRegen*.dbc", "Stat modifier curve catalog"}, {"WACT", ".wact", "ui", "Hardcoded class default action bar","Action bar layout catalog"}, {"WGRP", ".wgrp", "social", "LFG group-composition rules", "Group composition catalog (role quotas)"}, + {"WHRT", ".whrt", "social", "SMSG_BINDPOINTUPDATE bind list", "Hearthstone bind point catalog"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine