mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 03:23:51 +00:00
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:
parent
31b9b55ebd
commit
c50d3cbae5
10 changed files with 773 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
133
include/pipeline/wowee_hearth_binds.hpp
Normal file
133
include/pipeline/wowee_hearth_binds.hpp
Normal 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
|
||||
333
src/pipeline/wowee_hearth_binds.cpp
Normal file
333
src/pipeline/wowee_hearth_binds.cpp
Normal 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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
276
tools/editor/cli_hearth_binds_catalog.cpp
Normal file
276
tools/editor/cli_hearth_binds_catalog.cpp
Normal 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
|
||||
12
tools/editor/cli_hearth_binds_catalog.hpp
Normal file
12
tools/editor/cli_hearth_binds_catalog.hpp
Normal 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
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue