mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 03:23:51 +00:00
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:
parent
5a12f5d183
commit
acaef78696
10 changed files with 677 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
114
include/pipeline/wowee_boss_encounters.hpp
Normal file
114
include/pipeline/wowee_boss_encounters.hpp
Normal 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
|
||||
267
src/pipeline/wowee_boss_encounters.cpp
Normal file
267
src/pipeline/wowee_boss_encounters.cpp
Normal 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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
265
tools/editor/cli_boss_encounters_catalog.cpp
Normal file
265
tools/editor/cli_boss_encounters_catalog.cpp
Normal 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
|
||||
12
tools/editor/cli_boss_encounters_catalog.hpp
Normal file
12
tools/editor/cli_boss_encounters_catalog.hpp
Normal 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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue