feat(editor): add WHRT (Hearth Bind Point) open catalog format

Novel replacement for the hardcoded SMSG_BINDPOINTUPDATE
bind list. Each entry is one valid hearthstone bind
location: a tavern innkeeper, a capital-hall bind clerk,
a quest-given bind reward (Theramore, Wyrmrest), a guild-
hall bind clerk, or a special raid port (Karazhan,
Sunwell). Cross-references WMS for mapId/areaId, WCRT
for the innkeeper NPC, and WCHC for faction-mask bits.

Six bindKind enum values (Inn / Capital / Quest / Guild /
SpecialPort / Faction) and a 3-value factionMask
(AllianceOnly / HordeOnly / Both). Three preset emitters:
makeStarterCities (4 city innkeepers), makeCapitals (6
capital-hall bind clerks), makeStarterInns (8 starter-zone
inns spanning all races).

Validator checks id+name required, factionMask 1..3,
bindKind 0..5, no duplicate ids; warns on (0,0,0)
position (likely forgotten SetPosition; bind would
teleport player to world origin), Inn-kind with no
innkeeper NPC, Quest-kind with no level gate.

Format count 96 -> 97. CLI flag count 1097 -> 1102.
This commit is contained in:
Kelsi 2026-05-10 00:25:55 -07:00
parent 31b9b55ebd
commit c50d3cbae5
10 changed files with 773 additions and 0 deletions

View file

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

View file

@ -0,0 +1,133 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
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: <name>" 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<Entry> 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<const Entry*> findByFaction(uint8_t playerFaction) const;
// Returns all bind points within a given map (for the
// continent-level inn overlay).
std::vector<const Entry*> 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

View file

@ -0,0 +1,333 @@
#include "pipeline/wowee_hearth_binds.hpp"
#include <cstdio>
#include <cstring>
#include <fstream>
namespace wowee {
namespace pipeline {
namespace {
constexpr char kMagic[4] = {'W', 'H', 'R', 'T'};
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() < 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<uint32_t>(a) << 24) |
(static_cast<uint32_t>(b) << 16) |
(static_cast<uint32_t>(g) << 8) |
static_cast<uint32_t>(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<const WoweeHearthBinds::Entry*>
WoweeHearthBinds::findByFaction(uint8_t playerFaction) const {
std::vector<const Entry*> out;
for (const auto& e : entries) {
if (e.factionMask & playerFaction) out.push_back(&e);
}
return out;
}
std::vector<const WoweeHearthBinds::Entry*>
WoweeHearthBinds::findByMap(uint32_t mapId) const {
std::vector<const Entry*> 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<uint32_t>(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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2111,6 +2111,16 @@ void printUsage(const char* argv0) {
std::printf(" Export binary .wgrp to a human-editable JSON sidecar (defaults to <base>.wgrp.json; sizeCategory string is informational, ignored on import)\n");
std::printf(" --import-wgrp-json <json-path> [out-base]\n");
std::printf(" Import a .wgrp.json sidecar back into binary .wgrp (requireSpec accepts bool OR int)\n");
std::printf(" --gen-hrt <whrt-base> [name]\n");
std::printf(" Emit .whrt 4 starter-city innkeepers (Stormwind / Ironforge / Orgrimmar / Thunder Bluff) faction-gated\n");
std::printf(" --gen-hrt-capitals <whrt-base> [name]\n");
std::printf(" Emit .whrt 6 capital-hall bind clerks (Stormwind / Ironforge / Darnassus / Orgrimmar / Undercity / Thunder Bluff)\n");
std::printf(" --gen-hrt-inns <whrt-base> [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 <whrt-base> [--json]\n");
std::printf(" Print WHRT entries (id / map / area / faction / kind / npc / levelMin / name)\n");
std::printf(" --validate-whrt <whrt-base> [--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 <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

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