Files
WoWee/tests/test_dbc_layouts.cpp
Kelsi 1985535178 test(dbc): pin the Spell/SpellRange layout fields the spell logic reads
Layout files are looked up by name, so a field that goes missing degrades silently
rather than failing: a Spell layout with no RangeIndex makes every spell's range
unknown, which quietly disables melee range checks and self-cast detection across
all four expansions.

Verified load-bearing: dropping RangeIndex from the WotLK layout fails the test at
the assertion that guards Battle Shout.
2026-07-14 14:47:47 -07:00

109 lines
4.0 KiB
C++

#include <catch_amalgamated.hpp>
#include "pipeline/dbc_layout.hpp"
#include <filesystem>
#include <string>
#include <vector>
// 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<std::string>& expansions() {
static const std::vector<std::string> 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);
}
}