mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 11:33:52 +00:00
feat(editor): add WBAB (Buff & Aura Book) — 102nd open format
Novel replacement for the implicit rank-chain relationships that vanilla WoW encoded by burying nextRank/prevRank pointers inside Spell.dbc with no explicit graph structure. Each WBAB entry is one long- duration class buff at one specific rank, with explicit edges to adjacent ranks via previousRankId and nextRankId fields. The graph-shaped data is novel among the 100+ catalog set: most catalogs have flat rows; WBAB is genuinely a graph where rows are nodes and the rank fields are edges. Both directions are stored explicitly so the spellbook UI's "upgrade to next rank" button can traverse without scanning the full table. Helper methods walkChainBack- ToRoot() returns the full chain root->tip for the rank- picker widget; findChainTip() returns the highest rank for auto-cast logic. Three preset emitters demonstrating the pattern: makeMage (Arcane Intellect ranks 1-4 with chain edges), makeDruid (Mark of the Wild ranks 1-5 with chain edges), makeRaidMax (6 max-rank standalone raid buffs — one per buffing class — with no chain edges to show the standalone case). Validator catches several rank-chain-specific bugs: self-referencing edges (entry.next == entry.id would create a 1-element cycle), missing referenced entries (next/prev pointing to non-existent ids), and most importantly back-edge symmetry — if A.nextRankId=B then B.previousRankId MUST equal A.buffId or the spellbook upgrade traversal will derail. Symmetric back-edge check is unique to graph-shaped catalogs. Also fixed a crash in --catalog-find where the recursive directory iterator threw on permission-denied subdirs (common when walking /tmp). Now uses the skip_permission_denied directory_options + per-step error_code clearing for defensive resumption. Format count 101 -> 102. CLI flag count 1134 -> 1139.
This commit is contained in:
parent
471ddfef07
commit
abf264abfe
11 changed files with 883 additions and 3 deletions
327
src/pipeline/wowee_buff_book.cpp
Normal file
327
src/pipeline/wowee_buff_book.cpp
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
#include "pipeline/wowee_buff_book.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kMagic[4] = {'W', 'B', 'A', 'B'};
|
||||
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) != ".wbab") {
|
||||
base += ".wbab";
|
||||
}
|
||||
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 WoweeBuffBook::Entry*
|
||||
WoweeBuffBook::findById(uint32_t buffId) const {
|
||||
for (const auto& e : entries)
|
||||
if (e.buffId == buffId) return &e;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<const WoweeBuffBook::Entry*>
|
||||
WoweeBuffBook::walkChainBackToRoot(uint32_t buffId) const {
|
||||
std::vector<const Entry*> out;
|
||||
std::unordered_set<uint32_t> visited;
|
||||
const Entry* cur = findById(buffId);
|
||||
while (cur != nullptr && visited.insert(cur->buffId).second) {
|
||||
out.push_back(cur);
|
||||
if (cur->previousRankId == 0) break;
|
||||
cur = findById(cur->previousRankId);
|
||||
}
|
||||
// Reverse so output flows root → tip.
|
||||
for (size_t a = 0, b = out.size(); a + 1 < b; ++a, --b) {
|
||||
std::swap(out[a], out[b - 1]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const WoweeBuffBook::Entry*
|
||||
WoweeBuffBook::findChainTip(uint32_t buffId) const {
|
||||
const Entry* cur = findById(buffId);
|
||||
std::unordered_set<uint32_t> visited;
|
||||
while (cur != nullptr && cur->nextRankId != 0 &&
|
||||
visited.insert(cur->buffId).second) {
|
||||
cur = findById(cur->nextRankId);
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
bool WoweeBuffBookLoader::save(const WoweeBuffBook& 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.buffId);
|
||||
writeStr(os, e.name);
|
||||
writeStr(os, e.description);
|
||||
writePOD(os, e.spellId);
|
||||
writePOD(os, e.castClassMask);
|
||||
writePOD(os, e.targetTypeMask);
|
||||
writePOD(os, e.statBonusKind);
|
||||
writePOD(os, e.rank);
|
||||
writePOD(os, e.maxStackCount);
|
||||
writePOD(os, e.statBonusAmount);
|
||||
writePOD(os, e.duration);
|
||||
writePOD(os, e.previousRankId);
|
||||
writePOD(os, e.nextRankId);
|
||||
writePOD(os, e.iconColorRGBA);
|
||||
}
|
||||
return os.good();
|
||||
}
|
||||
|
||||
WoweeBuffBook WoweeBuffBookLoader::load(const std::string& basePath) {
|
||||
WoweeBuffBook 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.buffId)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
if (!readStr(is, e.name) || !readStr(is, e.description)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
if (!readPOD(is, e.spellId) ||
|
||||
!readPOD(is, e.castClassMask) ||
|
||||
!readPOD(is, e.targetTypeMask) ||
|
||||
!readPOD(is, e.statBonusKind) ||
|
||||
!readPOD(is, e.rank) ||
|
||||
!readPOD(is, e.maxStackCount) ||
|
||||
!readPOD(is, e.statBonusAmount) ||
|
||||
!readPOD(is, e.duration) ||
|
||||
!readPOD(is, e.previousRankId) ||
|
||||
!readPOD(is, e.nextRankId) ||
|
||||
!readPOD(is, e.iconColorRGBA)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool WoweeBuffBookLoader::exists(const std::string& basePath) {
|
||||
std::ifstream is(normalizePath(basePath), std::ios::binary);
|
||||
return is.good();
|
||||
}
|
||||
|
||||
WoweeBuffBook WoweeBuffBookLoader::makeMage(
|
||||
const std::string& catalogName) {
|
||||
using B = WoweeBuffBook;
|
||||
WoweeBuffBook c;
|
||||
c.name = catalogName;
|
||||
auto add = [&](uint32_t id, const char* name,
|
||||
uint32_t spellId, uint8_t rank,
|
||||
int32_t statAmount, uint32_t prevId,
|
||||
uint32_t nextId, const char* desc) {
|
||||
B::Entry e;
|
||||
e.buffId = id; e.name = name; e.description = desc;
|
||||
e.spellId = spellId;
|
||||
e.castClassMask = 128; // Mage
|
||||
e.targetTypeMask = B::TargetSelf | B::TargetParty;
|
||||
e.statBonusKind = B::Intellect;
|
||||
e.rank = rank;
|
||||
e.maxStackCount = 1;
|
||||
e.statBonusAmount = statAmount;
|
||||
e.duration = 1800; // 30 minutes
|
||||
e.previousRankId = prevId;
|
||||
e.nextRankId = nextId;
|
||||
e.iconColorRGBA = packRgba(140, 200, 255); // mage blue
|
||||
c.entries.push_back(e);
|
||||
};
|
||||
// Arcane Intellect rank chain — spell IDs from
|
||||
// Spell.dbc 3.3.5a; intellect bonus per rank.
|
||||
add(1, "ArcaneIntellect_R1", 1459, 1, 3, 0, 2,
|
||||
"Arcane Intellect Rank 1 — +3 Intellect, "
|
||||
"30 min party-wide. Trained at level 8.");
|
||||
add(2, "ArcaneIntellect_R2", 1460, 2, 7, 1, 3,
|
||||
"Arcane Intellect Rank 2 — +7 Intellect. "
|
||||
"Trained at level 22.");
|
||||
add(3, "ArcaneIntellect_R3", 1461, 3, 15, 2, 4,
|
||||
"Arcane Intellect Rank 3 — +15 Intellect. "
|
||||
"Trained at level 36.");
|
||||
add(4, "ArcaneIntellect_R4", 10157, 4, 25, 3, 0,
|
||||
"Arcane Intellect Rank 4 — +25 Intellect. "
|
||||
"Trained at level 50. (Max rank in this preset; "
|
||||
"real WoTLK has higher ranks via Brilliance "
|
||||
"variant.)");
|
||||
return c;
|
||||
}
|
||||
|
||||
WoweeBuffBook WoweeBuffBookLoader::makeDruid(
|
||||
const std::string& catalogName) {
|
||||
using B = WoweeBuffBook;
|
||||
WoweeBuffBook c;
|
||||
c.name = catalogName;
|
||||
auto add = [&](uint32_t id, const char* name,
|
||||
uint32_t spellId, uint8_t rank,
|
||||
int32_t statAmount, uint32_t prevId,
|
||||
uint32_t nextId, const char* desc) {
|
||||
B::Entry e;
|
||||
e.buffId = id; e.name = name; e.description = desc;
|
||||
e.spellId = spellId;
|
||||
e.castClassMask = 1024; // Druid
|
||||
e.targetTypeMask = B::TargetSelf |
|
||||
B::TargetParty |
|
||||
B::TargetFriendly;
|
||||
e.statBonusKind = B::AllStats;
|
||||
e.rank = rank;
|
||||
e.maxStackCount = 1;
|
||||
e.statBonusAmount = statAmount;
|
||||
e.duration = 1800;
|
||||
e.previousRankId = prevId;
|
||||
e.nextRankId = nextId;
|
||||
e.iconColorRGBA = packRgba(255, 125, 10); // druid orange
|
||||
c.entries.push_back(e);
|
||||
};
|
||||
add(100, "MarkOfTheWild_R1", 1126, 1, 3, 0, 101,
|
||||
"Mark of the Wild Rank 1 — +3 to all stats. "
|
||||
"Trained at level 1.");
|
||||
add(101, "MarkOfTheWild_R2", 5232, 2, 6, 100, 102,
|
||||
"Mark of the Wild Rank 2 — +6 to all stats. "
|
||||
"Trained at level 10.");
|
||||
add(102, "MarkOfTheWild_R3", 6756, 3, 10, 101, 103,
|
||||
"Mark of the Wild Rank 3 — +10 to all stats. "
|
||||
"Trained at level 20.");
|
||||
add(103, "MarkOfTheWild_R4", 5234, 4, 14, 102, 104,
|
||||
"Mark of the Wild Rank 4 — +14 to all stats. "
|
||||
"Trained at level 30.");
|
||||
add(104, "MarkOfTheWild_R5", 8907, 5, 18, 103, 0,
|
||||
"Mark of the Wild Rank 5 — +18 to all stats. "
|
||||
"Trained at level 40. (Top rank in this preset.)");
|
||||
return c;
|
||||
}
|
||||
|
||||
WoweeBuffBook WoweeBuffBookLoader::makeRaidMax(
|
||||
const std::string& catalogName) {
|
||||
using B = WoweeBuffBook;
|
||||
WoweeBuffBook c;
|
||||
c.name = catalogName;
|
||||
auto add = [&](uint32_t id, const char* name,
|
||||
uint32_t spellId, uint32_t classMask,
|
||||
uint8_t targetMask, uint8_t statKind,
|
||||
uint8_t rank, int32_t statAmount,
|
||||
uint32_t duration, uint32_t color,
|
||||
const char* desc) {
|
||||
B::Entry e;
|
||||
e.buffId = id; e.name = name; e.description = desc;
|
||||
e.spellId = spellId;
|
||||
e.castClassMask = classMask;
|
||||
e.targetTypeMask = targetMask;
|
||||
e.statBonusKind = statKind;
|
||||
e.rank = rank;
|
||||
e.maxStackCount = 1;
|
||||
e.statBonusAmount = statAmount;
|
||||
e.duration = duration;
|
||||
// No rank chain — these are max-rank standalone
|
||||
// entries pulled from each class's top buff.
|
||||
e.previousRankId = 0;
|
||||
e.nextRankId = 0;
|
||||
e.iconColorRGBA = color;
|
||||
c.entries.push_back(e);
|
||||
};
|
||||
add(200, "MarkOfTheWild_Max", 26990, 1024,
|
||||
B::TargetSelf | B::TargetRaid, B::AllStats,
|
||||
7, 35, 1800,
|
||||
packRgba(255, 125, 10),
|
||||
"Druid raid buff — Mark of the Wild rank 7, "
|
||||
"+35 to all stats, 30min, raid-wide.");
|
||||
add(201, "PowerWordFortitude_Max", 25389, 16,
|
||||
B::TargetSelf | B::TargetRaid, B::Stamina,
|
||||
8, 79, 1800,
|
||||
packRgba(255, 255, 255),
|
||||
"Priest raid buff — Prayer of Fortitude rank 4, "
|
||||
"+79 Stamina, 60min, raid-wide.");
|
||||
add(202, "ArcaneIntellect_Max", 27126, 128,
|
||||
B::TargetSelf | B::TargetRaid, B::Intellect,
|
||||
6, 60, 1800,
|
||||
packRgba(140, 200, 255),
|
||||
"Mage raid buff — Arcane Brilliance rank 2, "
|
||||
"+60 Intellect, 60min, raid-wide.");
|
||||
add(203, "BlessingOfKings", 25898, 2,
|
||||
B::TargetSelf | B::TargetRaid, B::AllStats,
|
||||
1, 10, 1800,
|
||||
packRgba(220, 220, 100),
|
||||
"Paladin raid buff — Greater Blessing of Kings, "
|
||||
"+10% to all stats, 60min, raid-wide.");
|
||||
add(204, "BattleShout_Max", 47436, 1,
|
||||
B::TargetSelf | B::TargetParty, B::AttackPower,
|
||||
9, 553, 120,
|
||||
packRgba(220, 60, 60),
|
||||
"Warrior raid buff — Battle Shout rank 9, "
|
||||
"+553 Attack Power, 2min, party-wide.");
|
||||
add(205, "TrueshotAura_Max", 19506, 4,
|
||||
B::TargetSelf | B::TargetRaid, B::Other,
|
||||
3, 0, 0,
|
||||
packRgba(170, 210, 100),
|
||||
"Hunter raid buff — Trueshot Aura, +10% AP "
|
||||
"for all party/raid (until cancel). statKind="
|
||||
"Other because it's a percentage modifier, not a "
|
||||
"flat stat.");
|
||||
return c;
|
||||
}
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
Loading…
Add table
Add a link
Reference in a new issue