#include #include "pipeline/dbc_layout.hpp" #include #include #include // The spell code reads DBC fields by name, resolving them through the per-expansion // layout files. If a layout loses a field the code depends on, the lookup returns // "missing" and the behaviour silently degrades rather than failing: a Spell layout // with no RangeIndex makes every spell's range unknown, which turns off melee range // checks and self-cast detection across the board. Nothing else would catch that, so // pin the fields the spell logic actually depends on. using wowee::pipeline::DBCLayout; namespace { constexpr uint32_t kMissing = 0xFFFFFFFF; const std::vector& expansions() { static const std::vector kExpansions{"classic", "tbc", "wotlk", "turtle"}; return kExpansions; } std::string layoutPath(const std::string& expansion) { return (std::filesystem::path(WOWEE_SOURCE_DIR) / "Data" / "expansions" / expansion / "dbc_layouts.json").string(); } } // namespace TEST_CASE("Every expansion ships a loadable DBC layout", "[dbc][layout]") { for (const auto& expansion : expansions()) { INFO("expansion: " << expansion); DBCLayout layout; REQUIRE(layout.loadFromJson(layoutPath(expansion))); } } TEST_CASE("Spell layout exposes the fields the spell logic reads", "[dbc][layout]") { for (const auto& expansion : expansions()) { INFO("expansion: " << expansion); DBCLayout layout; REQUIRE(layout.loadFromJson(layoutPath(expansion))); const auto* spell = layout.getLayout("Spell"); REQUIRE(spell != nullptr); // Identity and display. REQUIRE(spell->field("ID") != kMissing); REQUIRE(spell->field("Name") != kMissing); // Rank drives superseded-rank resolution ("Rank 3" -> 3). REQUIRE(spell->field("Rank") != kMissing); // RangeIndex resolves against SpellRange.dbc and decides whether a spell is // self-cast (0 yards) or melee (Combat Range, 5 yards). Lose it and Battle // Shout reads as a melee ability again. REQUIRE(spell->field("RangeIndex") != kMissing); // Duration drives aura timers. REQUIRE(spell->field("DurationIndex") != kMissing); // School is per-expansion: a bitmask on TBC/WotLK, a 0-6 enum on Classic/Turtle. const bool hasSchool = spell->field("SchoolMask") != kMissing || spell->field("SchoolEnum") != kMissing; REQUIRE(hasSchool); } } TEST_CASE("SpellRange layout exposes MaxRange", "[dbc][layout]") { for (const auto& expansion : expansions()) { INFO("expansion: " << expansion); DBCLayout layout; REQUIRE(layout.loadFromJson(layoutPath(expansion))); const auto* range = layout.getLayout("SpellRange"); REQUIRE(range != nullptr); // Without MaxRange the RangeIndex cannot be resolved into yards, and every // spell falls back to "unknown range". REQUIRE(range->field("MaxRange") != kMissing); } } TEST_CASE("Spell field indices are distinct", "[dbc][layout]") { // A copy-paste slip that points two fields at the same column reads the wrong // data rather than failing, so check the ones that sit next to each other. for (const auto& expansion : expansions()) { INFO("expansion: " << expansion); DBCLayout layout; REQUIRE(layout.loadFromJson(layoutPath(expansion))); const auto* spell = layout.getLayout("Spell"); REQUIRE(spell != nullptr); const uint32_t id = spell->field("ID"); const uint32_t name = spell->field("Name"); const uint32_t rank = spell->field("Rank"); const uint32_t rangeIndex = spell->field("RangeIndex"); const uint32_t durationIndex = spell->field("DurationIndex"); REQUIRE(id != name); REQUIRE(name != rank); REQUIRE(rangeIndex != durationIndex); REQUIRE(rangeIndex != rank); } }