feat(editor): add WBOS (Boss Encounter Definition) open catalog format

Open replacement for AzerothCore's instance_encounter SQL table
plus the per-boss script bindings. Defines raid boss encounter
metadata: which creature is the boss, which map and difficulty
variant it lives in, how many phases the encounter has, the
soft-enrage timer and berserk spell, recommended group size, and
item level.

One entry per (boss × difficulty) combination. Lord Marrowgar in
10-Normal ICC is one entry; Lord Marrowgar in 25-Heroic ICC is a
separate entry with a higher recommendedItemLevel and a different
difficultyId pointing into WCDF.

This format ties together five other catalogs into a coherent
encounter description:
  - WCRT for the boss creature template
  - WMS for the instance map
  - WCDF for difficulty routing (10/25/H10/H25 variants)
  - WSPL for the berserk spell that fires at enrage
  - WACR for achievement criteria like "kill The Lich King in
    25-Heroic" that point back via KillCreature targetId

findByMap(mapId) returns all encounters in one raid instance,
sorted by their catalog order — used by the Encounter Journal
UI and instance lockout logic. findByBossCreature(bossId)
returns all difficulty variants of one boss.

Three preset emitters: --gen-bos (3 5-man dungeon bosses with
no soft-enrage), --gen-bos-raid10 (4 ICC 10-Normal bosses
including 5-phase Lich King with 15min hard enrage via Fury of
Frostmourne 72546), --gen-bos-world (2 outdoor world bosses
with 25-player size + no difficulty).

Validation enforces id+name+boss+map+phases+size presence, no
duplicate ids; warns on:
  - non-standard requiredPartySize (canonical sizes are
    5/10/25/40)
  - berserkSpellId set without enrageTimerMs (spell never fires)
  - enrageTimerMs > 30 minutes (sanity check)

Wired through the cross-format table; WBOS appears in all 18
cross-format utilities. Format count 90 -> 91; CLI flag count
1055 -> 1060.
This commit is contained in:
Kelsi 2026-05-09 23:45:26 -07:00
parent 5a12f5d183
commit acaef78696
10 changed files with 677 additions and 0 deletions

View file

@ -679,6 +679,7 @@ set(WOWEE_SOURCES
src/pipeline/wowee_token_rewards.cpp
src/pipeline/wowee_spell_procs.cpp
src/pipeline/wowee_creature_patrols.cpp
src/pipeline/wowee_boss_encounters.cpp
src/pipeline/custom_zone_discovery.cpp
src/pipeline/dbc_layout.cpp
@ -1521,6 +1522,7 @@ add_executable(wowee_editor
tools/editor/cli_token_rewards_catalog.cpp
tools/editor/cli_spell_procs_catalog.cpp
tools/editor/cli_creature_patrols_catalog.cpp
tools/editor/cli_boss_encounters_catalog.cpp
tools/editor/cli_quest_objective.cpp
tools/editor/cli_quest_reward.cpp
tools/editor/cli_clone.cpp
@ -1678,6 +1680,7 @@ add_executable(wowee_editor
src/pipeline/wowee_token_rewards.cpp
src/pipeline/wowee_spell_procs.cpp
src/pipeline/wowee_creature_patrols.cpp
src/pipeline/wowee_boss_encounters.cpp
src/pipeline/custom_zone_discovery.cpp
src/pipeline/terrain_mesh.cpp

View file

@ -0,0 +1,114 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace wowee {
namespace pipeline {
// Wowee Open Boss Encounter catalog (.wbos) — novel
// replacement for AzerothCore's instance_encounter SQL
// table plus the per-boss script bindings. Defines raid
// boss encounter metadata: which creature is the boss,
// which map and difficulty variant it lives in, how many
// phases the encounter has, the soft-enrage timer and
// berserk spell, recommended group size, and item level.
//
// One entry per (boss × difficulty) combination. Lord
// Marrowgar in 10-Normal ICC is one entry; Lord Marrowgar
// in 25-Heroic ICC is a separate entry with a higher
// recommendedItemLevel and a different difficultyId.
//
// Cross-references with previously-added formats:
// WCRT: bossCreatureId references the WCRT creature
// template entry for the boss.
// WMS: mapId references the WMS map entry (instance).
// WCDF: difficultyId references the WCDF route that
// maps base creature -> 10/25/H10/H25 variants.
// WSPL: berserkSpellId references the WSPL spell that
// fires when the soft-enrage timer expires.
// WACR: achievement criteria with KillCreature targetId
// pointing at this boss reference back to it.
//
// Binary layout (little-endian):
// magic[4] = "WBOS"
// version (uint32) = current 1
// nameLen + name (catalog label)
// entryCount (uint32)
// entries (each):
// encounterId (uint32)
// nameLen + name
// descLen + description
// bossCreatureId (uint32)
// mapId (uint32)
// difficultyId (uint32)
// berserkSpellId (uint32)
// enrageTimerMs (uint32)
// phaseCount (uint8) / requiredPartySize (uint8) / pad[2]
// recommendedItemLevel (uint16) / pad[2]
// iconColorRGBA (uint32)
struct WoweeBossEncounter {
struct Entry {
uint32_t encounterId = 0;
std::string name;
std::string description;
uint32_t bossCreatureId = 0;
uint32_t mapId = 0;
uint32_t difficultyId = 0;
uint32_t berserkSpellId = 0;
uint32_t enrageTimerMs = 0; // 0 = no soft enrage
uint8_t phaseCount = 1;
uint8_t requiredPartySize = 10; // 5 / 10 / 25 / 40
uint8_t pad0 = 0;
uint8_t pad1 = 0;
uint16_t recommendedItemLevel = 0;
uint16_t pad2 = 0;
uint32_t iconColorRGBA = 0xFFFFFFFFu;
};
std::string name;
std::vector<Entry> entries;
bool isValid() const { return !entries.empty(); }
const Entry* findById(uint32_t encounterId) const;
// Returns all encounters bound to a given map id
// (typically all bosses in one raid instance), in the
// order they appear in the catalog. Used by the Encounter
// Journal UI and instance lockout logic.
std::vector<const Entry*> findByMap(uint32_t mapId) const;
// Returns all encounters bound to a given boss creature
// (typically the per-difficulty variants of one boss).
std::vector<const Entry*> findByBossCreature(
uint32_t bossCreatureId) const;
};
class WoweeBossEncounterLoader {
public:
static bool save(const WoweeBossEncounter& cat,
const std::string& basePath);
static WoweeBossEncounter load(const std::string& basePath);
static bool exists(const std::string& basePath);
// Preset emitters used by --gen-bos* variants.
//
// makeFiveMan — 3 5-man dungeon bosses (trash boss,
// mid boss, final boss) at recommended
// ilvl 200 with no soft-enrage.
// makeRaid10 — 4 ICC-style 10-man raid bosses
// (Marrowgar / Deathwhisper / Saurfang
// / Lich King) with multi-phase
// structure and soft-enrage timers.
// makeWorldBoss — 2 outdoor world bosses (Doom Lord
// Kazzak / Doomwalker) — single phase,
// no enrage timer, 25-player size.
static WoweeBossEncounter makeFiveMan(const std::string& catalogName);
static WoweeBossEncounter makeRaid10(const std::string& catalogName);
static WoweeBossEncounter makeWorldBoss(const std::string& catalogName);
};
} // namespace pipeline
} // namespace wowee

View file

@ -0,0 +1,267 @@
#include "pipeline/wowee_boss_encounters.hpp"
#include <cstdio>
#include <cstring>
#include <fstream>
namespace wowee {
namespace pipeline {
namespace {
constexpr char kMagic[4] = {'W', 'B', 'O', 'S'};
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) != ".wbos") {
base += ".wbos";
}
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 WoweeBossEncounter::Entry*
WoweeBossEncounter::findById(uint32_t encounterId) const {
for (const auto& e : entries)
if (e.encounterId == encounterId) return &e;
return nullptr;
}
std::vector<const WoweeBossEncounter::Entry*>
WoweeBossEncounter::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;
}
std::vector<const WoweeBossEncounter::Entry*>
WoweeBossEncounter::findByBossCreature(uint32_t bossCreatureId) const {
std::vector<const Entry*> out;
for (const auto& e : entries)
if (e.bossCreatureId == bossCreatureId) out.push_back(&e);
return out;
}
bool WoweeBossEncounterLoader::save(const WoweeBossEncounter& 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.encounterId);
writeStr(os, e.name);
writeStr(os, e.description);
writePOD(os, e.bossCreatureId);
writePOD(os, e.mapId);
writePOD(os, e.difficultyId);
writePOD(os, e.berserkSpellId);
writePOD(os, e.enrageTimerMs);
writePOD(os, e.phaseCount);
writePOD(os, e.requiredPartySize);
writePOD(os, e.pad0);
writePOD(os, e.pad1);
writePOD(os, e.recommendedItemLevel);
writePOD(os, e.pad2);
writePOD(os, e.iconColorRGBA);
}
return os.good();
}
WoweeBossEncounter WoweeBossEncounterLoader::load(
const std::string& basePath) {
WoweeBossEncounter 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.encounterId)) {
out.entries.clear(); return out;
}
if (!readStr(is, e.name) || !readStr(is, e.description)) {
out.entries.clear(); return out;
}
if (!readPOD(is, e.bossCreatureId) ||
!readPOD(is, e.mapId) ||
!readPOD(is, e.difficultyId) ||
!readPOD(is, e.berserkSpellId) ||
!readPOD(is, e.enrageTimerMs) ||
!readPOD(is, e.phaseCount) ||
!readPOD(is, e.requiredPartySize) ||
!readPOD(is, e.pad0) ||
!readPOD(is, e.pad1) ||
!readPOD(is, e.recommendedItemLevel) ||
!readPOD(is, e.pad2) ||
!readPOD(is, e.iconColorRGBA)) {
out.entries.clear(); return out;
}
}
return out;
}
bool WoweeBossEncounterLoader::exists(const std::string& basePath) {
std::ifstream is(normalizePath(basePath), std::ios::binary);
return is.good();
}
WoweeBossEncounter WoweeBossEncounterLoader::makeFiveMan(
const std::string& catalogName) {
using B = WoweeBossEncounter;
WoweeBossEncounter c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name, uint32_t boss,
uint32_t map, uint32_t diff, uint8_t phases,
uint16_t ilvl, const char* desc) {
B::Entry e;
e.encounterId = id; e.name = name; e.description = desc;
e.bossCreatureId = boss;
e.mapId = map;
e.difficultyId = diff;
e.phaseCount = phases;
e.requiredPartySize = 5;
e.recommendedItemLevel = ilvl;
e.iconColorRGBA = packRgba(180, 220, 100); // dungeon green
c.entries.push_back(e);
};
// 5-man dungeon bosses with no soft-enrage. mapId 600
// is illustrative for "Drak'Tharon Keep"-style WotLK
// 5-man instance.
add(1, "TrollChieftain", 31000, 600, 200, 2, 200,
"Troll Chieftain — 5-man dungeon, 2-phase encounter, "
"no enrage.");
add(2, "ShamanWraith", 31010, 600, 201, 1, 200,
"Shaman Wraith — 5-man mid boss, single phase.");
add(3, "DrakTharonFinal", 31030, 600, 203, 3, 210,
"Drak'Tharon final boss — 5-man, 3-phase mass-death "
"encounter.");
return c;
}
WoweeBossEncounter WoweeBossEncounterLoader::makeRaid10(
const std::string& catalogName) {
using B = WoweeBossEncounter;
WoweeBossEncounter c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name, uint32_t boss,
uint32_t diff, uint8_t phases,
uint32_t enrageMs, uint32_t berserkSpell,
uint16_t ilvl, const char* desc) {
B::Entry e;
e.encounterId = id; e.name = name; e.description = desc;
e.bossCreatureId = boss;
// mapId 631 = Icecrown Citadel.
e.mapId = 631;
e.difficultyId = diff;
e.phaseCount = phases;
e.requiredPartySize = 10;
e.enrageTimerMs = enrageMs;
e.berserkSpellId = berserkSpell;
e.recommendedItemLevel = ilvl;
e.iconColorRGBA = packRgba(220, 80, 100); // raid red
c.entries.push_back(e);
};
// Spell 26662 = Berserk (canonical raid soft-enrage spell).
add(100, "LordMarrowgar", 36612, 100, 2, 600000, 26662, 232,
"Lord Marrowgar 10N — 2-phase: tank-and-spank then "
"Bone Storm whirlwind. 10min soft enrage.");
add(101, "LadyDeathwhisper", 36855, 101, 2, 600000, 26662, 232,
"Lady Deathwhisper 10N — 2-phase: shield drop then "
"mind-control adds. 10min soft enrage.");
add(102, "DeathbringerSaurfang",37813, 102, 1, 480000, 26662, 232,
"Deathbringer Saurfang 10N — single phase, blood "
"beasts add wave. 8min soft enrage.");
add(103, "TheLichKing", 36597, 103, 5, 900000, 72546, 251,
"The Lich King 10N — 5-phase encounter (tank-spank, "
"platform jumps, Frostmourne soul realm). 15min hard "
"enrage via Fury of Frostmourne.");
return c;
}
WoweeBossEncounter WoweeBossEncounterLoader::makeWorldBoss(
const std::string& catalogName) {
using B = WoweeBossEncounter;
WoweeBossEncounter c;
c.name = catalogName;
auto add = [&](uint32_t id, const char* name, uint32_t boss,
uint32_t map, uint16_t ilvl, const char* desc) {
B::Entry e;
e.encounterId = id; e.name = name; e.description = desc;
e.bossCreatureId = boss;
e.mapId = map;
e.phaseCount = 1;
e.requiredPartySize = 25;
// World bosses don't use the difficulty system —
// single open-world spawn that scales to attackers.
e.difficultyId = 0;
// No soft-enrage — outdoor encounters can take
// arbitrarily long.
e.recommendedItemLevel = ilvl;
e.iconColorRGBA = packRgba(240, 100, 240); // world boss magenta
c.entries.push_back(e);
};
// mapId 530 = Outland (Hellfire Peninsula for Kazzak).
// mapId 530 also for Doomwalker (Shadowmoon Valley).
add(200, "DoomLordKazzak", 18728, 530, 134,
"Doom Lord Kazzak — Hellfire Peninsula world boss, "
"single phase, no enrage. 25-player encounter.");
add(201, "Doomwalker", 17711, 530, 132,
"Doomwalker — Shadowmoon Valley patrol, single phase, "
"rare 25-player tap encounter.");
return c;
}
} // namespace pipeline
} // namespace wowee

View file

@ -279,6 +279,8 @@ const char* const kArgRequired[] = {
"--gen-cmr", "--gen-cmr-city", "--gen-cmr-boss",
"--info-wcmr", "--validate-wcmr",
"--export-wcmr-json", "--import-wcmr-json",
"--gen-bos", "--gen-bos-raid10", "--gen-bos-world",
"--info-wbos", "--validate-wbos",
"--gen-weather-temperate", "--gen-weather-arctic",
"--gen-weather-desert", "--gen-weather-stormy",
"--gen-zone-atmosphere",

View file

@ -0,0 +1,265 @@
#include "cli_boss_encounters_catalog.hpp"
#include "cli_arg_parse.hpp"
#include "cli_box_emitter.hpp"
#include "pipeline/wowee_boss_encounters.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 stripWbosExt(std::string base) {
stripExt(base, ".wbos");
return base;
}
bool saveOrError(const wowee::pipeline::WoweeBossEncounter& c,
const std::string& base, const char* cmd) {
if (!wowee::pipeline::WoweeBossEncounterLoader::save(c, base)) {
std::fprintf(stderr, "%s: failed to save %s.wbos\n",
cmd, base.c_str());
return false;
}
return true;
}
void printGenSummary(const wowee::pipeline::WoweeBossEncounter& c,
const std::string& base) {
std::printf("Wrote %s.wbos\n", base.c_str());
std::printf(" catalog : %s\n", c.name.c_str());
std::printf(" encounters : %zu\n", c.entries.size());
}
int handleGenFiveMan(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "FiveManBosses";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWbosExt(base);
auto c = wowee::pipeline::WoweeBossEncounterLoader::makeFiveMan(name);
if (!saveOrError(c, base, "gen-bos")) return 1;
printGenSummary(c, base);
return 0;
}
int handleGenRaid10(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "ICC10NormalBosses";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWbosExt(base);
auto c = wowee::pipeline::WoweeBossEncounterLoader::makeRaid10(name);
if (!saveOrError(c, base, "gen-bos-raid10")) return 1;
printGenSummary(c, base);
return 0;
}
int handleGenWorldBoss(int& i, int argc, char** argv) {
std::string base = argv[++i];
std::string name = "WorldBosses";
if (parseOptArg(i, argc, argv)) name = argv[++i];
base = stripWbosExt(base);
auto c = wowee::pipeline::WoweeBossEncounterLoader::makeWorldBoss(name);
if (!saveOrError(c, base, "gen-bos-world")) 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 = stripWbosExt(base);
if (!wowee::pipeline::WoweeBossEncounterLoader::exists(base)) {
std::fprintf(stderr, "WBOS not found: %s.wbos\n", base.c_str());
return 1;
}
auto c = wowee::pipeline::WoweeBossEncounterLoader::load(base);
if (jsonOut) {
nlohmann::json j;
j["wbos"] = base + ".wbos";
j["name"] = c.name;
j["count"] = c.entries.size();
nlohmann::json arr = nlohmann::json::array();
for (const auto& e : c.entries) {
arr.push_back({
{"encounterId", e.encounterId},
{"name", e.name},
{"description", e.description},
{"bossCreatureId", e.bossCreatureId},
{"mapId", e.mapId},
{"difficultyId", e.difficultyId},
{"berserkSpellId", e.berserkSpellId},
{"enrageTimerMs", e.enrageTimerMs},
{"phaseCount", e.phaseCount},
{"requiredPartySize", e.requiredPartySize},
{"recommendedItemLevel", e.recommendedItemLevel},
{"iconColorRGBA", e.iconColorRGBA},
});
}
j["entries"] = arr;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("WBOS: %s.wbos\n", base.c_str());
std::printf(" catalog : %s\n", c.name.c_str());
std::printf(" encounters : %zu\n", c.entries.size());
if (c.entries.empty()) return 0;
std::printf(" id boss map diff phases size ilvl enrage(min) berserk name\n");
for (const auto& e : c.entries) {
char enrageBuf[16];
if (e.enrageTimerMs == 0) {
std::snprintf(enrageBuf, sizeof(enrageBuf), "-");
} else {
std::snprintf(enrageBuf, sizeof(enrageBuf), "%.1f",
e.enrageTimerMs / 60000.0);
}
std::printf(" %4u %5u %5u %4u %3u %3u %4u %-9s %5u %s\n",
e.encounterId, e.bossCreatureId,
e.mapId, e.difficultyId,
e.phaseCount, e.requiredPartySize,
e.recommendedItemLevel, enrageBuf,
e.berserkSpellId, 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 = stripWbosExt(base);
if (!wowee::pipeline::WoweeBossEncounterLoader::exists(base)) {
std::fprintf(stderr,
"validate-wbos: WBOS not found: %s.wbos\n", base.c_str());
return 1;
}
auto c = wowee::pipeline::WoweeBossEncounterLoader::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.encounterId);
if (!e.name.empty()) ctx += " " + e.name;
ctx += ")";
if (e.encounterId == 0)
errors.push_back(ctx + ": encounterId is 0");
if (e.name.empty())
errors.push_back(ctx + ": name is empty");
if (e.bossCreatureId == 0)
errors.push_back(ctx +
": bossCreatureId is 0 — encounter has no boss");
if (e.mapId == 0)
errors.push_back(ctx +
": mapId is 0 — encounter is unbound to a map");
if (e.phaseCount == 0)
errors.push_back(ctx +
": phaseCount is 0 — encounter has no phases");
if (e.requiredPartySize == 0)
errors.push_back(ctx +
": requiredPartySize is 0 — invalid group size");
if (e.requiredPartySize > 40)
warnings.push_back(ctx +
": requiredPartySize " +
std::to_string(e.requiredPartySize) +
" > 40 (max raid size)");
// Standard sizes are 5/10/25/40 — anything else is a
// server-custom raid size, worth flagging.
if (e.requiredPartySize != 5 && e.requiredPartySize != 10 &&
e.requiredPartySize != 25 && e.requiredPartySize != 40) {
warnings.push_back(ctx +
": non-standard requiredPartySize " +
std::to_string(e.requiredPartySize) +
" (canonical sizes are 5/10/25/40)");
}
// berserkSpellId without enrageTimerMs is contradictory
// (the spell never fires).
if (e.berserkSpellId != 0 && e.enrageTimerMs == 0) {
warnings.push_back(ctx +
": berserkSpellId=" +
std::to_string(e.berserkSpellId) +
" set but enrageTimerMs=0 — spell will never "
"fire (no enrage countdown)");
}
// Enrage > 30 minutes is suspicious — typical raid
// encounters cap at ~15-20 minutes.
if (e.enrageTimerMs > 1800000) {
warnings.push_back(ctx +
": enrageTimerMs " +
std::to_string(e.enrageTimerMs) +
" > 30 min (1800000ms) — exceptionally long "
"soft enrage, double-check");
}
for (uint32_t prev : idsSeen) {
if (prev == e.encounterId) {
errors.push_back(ctx + ": duplicate encounterId");
break;
}
}
idsSeen.push_back(e.encounterId);
}
bool ok = errors.empty();
if (jsonOut) {
nlohmann::json j;
j["wbos"] = base + ".wbos";
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-wbos: %s.wbos\n", base.c_str());
if (ok && warnings.empty()) {
std::printf(" OK — %zu encounters, all encounterIds 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 handleBossEncountersCatalog(int& i, int argc, char** argv,
int& outRc) {
if (std::strcmp(argv[i], "--gen-bos") == 0 && i + 1 < argc) {
outRc = handleGenFiveMan(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-bos-raid10") == 0 && i + 1 < argc) {
outRc = handleGenRaid10(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-bos-world") == 0 && i + 1 < argc) {
outRc = handleGenWorldBoss(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-wbos") == 0 && i + 1 < argc) {
outRc = handleInfo(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--validate-wbos") == 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 handleBossEncountersCatalog(int& i, int argc, char** argv,
int& outRc);
} // namespace cli
} // namespace editor
} // namespace wowee

View file

@ -135,6 +135,7 @@
#include "cli_token_rewards_catalog.hpp"
#include "cli_spell_procs_catalog.hpp"
#include "cli_creature_patrols_catalog.hpp"
#include "cli_boss_encounters_catalog.hpp"
#include "cli_quest_objective.hpp"
#include "cli_quest_reward.hpp"
#include "cli_clone.hpp"
@ -311,6 +312,7 @@ constexpr DispatchFn kDispatchTable[] = {
handleTokenRewardsCatalog,
handleSpellProcsCatalog,
handleCreaturePatrolsCatalog,
handleBossEncountersCatalog,
handleQuestObjective,
handleQuestReward,
handleClone,

View file

@ -93,6 +93,7 @@ constexpr FormatMagicEntry kFormats[] = {
{{'W','T','B','R'}, ".wtbr", "tokens", "--info-wtbr", "Token reward redemption catalog"},
{{'W','S','P','S'}, ".wsps", "spells", "--info-wsps", "Spell proc trigger catalog"},
{{'W','C','M','R'}, ".wcmr", "creatures", "--info-wcmr", "Creature patrol path catalog"},
{{'W','B','O','S'}, ".wbos", "raid", "--info-wbos", "Boss encounter definition 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

@ -2027,6 +2027,16 @@ void printUsage(const char* argv0) {
std::printf(" Export binary .wcmr to a human-editable JSON sidecar (defaults to <base>.wcmr.json). Waypoints exported as JSON arrays for variable-length editing\n");
std::printf(" --import-wcmr-json <json-path> [out-base]\n");
std::printf(" Import a .wcmr.json sidecar back into binary .wcmr (accepts pathKind/moveType int OR name; waypoint arrays length-preserving)\n");
std::printf(" --gen-bos <wbos-base> [name]\n");
std::printf(" Emit .wbos 3 5-man dungeon bosses (TrollChieftain 2-phase / ShamanWraith / DrakTharonFinal 3-phase) at recommended ilvl 200-210\n");
std::printf(" --gen-bos-raid10 <wbos-base> [name]\n");
std::printf(" Emit .wbos 4 ICC 10-Normal raid bosses (Marrowgar 2-phase / Deathwhisper 2-phase / Saurfang 1-phase / Lich King 5-phase) with soft-enrage timers\n");
std::printf(" --gen-bos-world <wbos-base> [name]\n");
std::printf(" Emit .wbos 2 outdoor world bosses (Doom Lord Kazzak / Doomwalker) — 25-player, no enrage, no difficultyId\n");
std::printf(" --info-wbos <wbos-base> [--json]\n");
std::printf(" Print WBOS entries (id / boss creature / map / difficulty / phases / size / ilvl / soft-enrage minutes / berserk spell / name)\n");
std::printf(" --validate-wbos <wbos-base> [--json]\n");
std::printf(" Static checks: id+name+boss+map+phases+size required, no duplicate ids; warns on non-standard party size, berserkSpellId set without enrageTimerMs, enrage > 30 min\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

@ -115,6 +115,7 @@ constexpr FormatRow kFormats[] = {
{"WTBR", ".wtbr", "tokens", "currency_token_reward SQL", "Token reward redemption catalog"},
{"WSPS", ".wsps", "spells", "spell_proc_event SQL + Spell.dbc", "Spell proc trigger catalog"},
{"WCMR", ".wcmr", "creatures", "creature_movement waypoints SQL", "Creature patrol path catalog"},
{"WBOS", ".wbos", "raid", "instance_encounter SQL", "Boss encounter definition catalog"},
// Additional pipeline catalogs without the alternating
// gen/info/validate CLI surface (loaded by the engine