Add multi-expansion support with data-driven protocol layer

Replace hardcoded WotLK protocol constants with a data-driven architecture
supporting Classic 1.12.1, TBC 2.4.3, and WotLK 3.3.5a. Each expansion
has JSON profiles for opcodes, update fields, and DBC layouts, plus C++
polymorphic packet parsers for binary format differences (movement flags,
speed fields, transport data, spline format, char enum layout).

Key components:
- ExpansionRegistry: scans Data/expansions/*/expansion.json at startup
- OpcodeTable: logical enum <-> wire values loaded from JSON
- UpdateFieldTable: field indices loaded from JSON per expansion
- DBCLayout: schema-driven DBC field lookups replacing magic numbers
- PacketParsers: WotLK/TBC/Classic parsers with correct flag positions
- Multi-manifest AssetManager: layered manifests with priority ordering
- HDPackManager: overlay texture packs with expansion compatibility
- Auth screen expansion picker replacing hardcoded version dropdown
This commit is contained in:
Kelsi 2026-02-12 22:56:36 -08:00
parent 4b24736113
commit 90a1aa8a92
51 changed files with 5258 additions and 887 deletions

View file

@ -4,6 +4,7 @@
#include "pipeline/asset_manager.hpp"
#include "pipeline/m2_loader.hpp"
#include "pipeline/dbc_loader.hpp"
#include "pipeline/dbc_layout.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
@ -164,19 +165,21 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
bool foundHair = false;
bool foundUnderwear = false;
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
uint32_t raceId = charSectionsDbc->getUInt32(r, 1);
uint32_t sexId = charSectionsDbc->getUInt32(r, 2);
uint32_t baseSection = charSectionsDbc->getUInt32(r, 3);
uint32_t variationIndex = charSectionsDbc->getUInt32(r, 8);
uint32_t colorIndex = charSectionsDbc->getUInt32(r, 9);
uint32_t raceId = charSectionsDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
uint32_t sexId = charSectionsDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
uint32_t baseSection = charSectionsDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8);
uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9);
if (raceId != targetRaceId || sexId != targetSexId) continue;
// Section 0: Body skin (variation=0, colorIndex = skin color)
if (baseSection == 0 && !foundSkin &&
variationIndex == 0 && colorIndex == static_cast<uint32_t>(skin)) {
std::string tex1 = charSectionsDbc->getString(r, 4);
std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4);
if (!tex1.empty()) {
bodySkinPath_ = tex1;
foundSkin = true;
@ -186,8 +189,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
else if (baseSection == 1 && !foundFace &&
variationIndex == static_cast<uint32_t>(face) &&
colorIndex == static_cast<uint32_t>(skin)) {
std::string tex1 = charSectionsDbc->getString(r, 4);
std::string tex2 = charSectionsDbc->getString(r, 5);
std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4);
std::string tex2 = charSectionsDbc->getString(r, csL ? (*csL)["Texture2"] : 5);
if (!tex1.empty()) faceLowerPath = tex1;
if (!tex2.empty()) faceUpperPath = tex2;
foundFace = true;
@ -196,7 +199,7 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
else if (baseSection == 3 && !foundHair &&
variationIndex == static_cast<uint32_t>(hairStyle) &&
colorIndex == static_cast<uint32_t>(hairColor)) {
std::string tex1 = charSectionsDbc->getString(r, 4);
std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 4);
if (!tex1.empty()) {
hairScalpPath = tex1;
foundHair = true;
@ -205,7 +208,8 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
// Section 4: Underwear (variation=0, colorIndex = skin color)
else if (baseSection == 4 && !foundUnderwear &&
variationIndex == 0 && colorIndex == static_cast<uint32_t>(skin)) {
for (int f = 4; f <= 6; f++) {
uint32_t texBase = csL ? (*csL)["Texture1"] : 4;
for (uint32_t f = texBase; f <= texBase + 2; f++) {
std::string tex = charSectionsDbc->getString(r, f);
if (!tex.empty()) {
underwearPaths.push_back(tex);

View file

@ -1,6 +1,7 @@
#include "rendering/lighting_manager.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_loader.hpp"
#include "pipeline/dbc_layout.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <cmath>
@ -78,27 +79,30 @@ bool LightingManager::loadLightDbc(pipeline::AssetManager* assetManager) {
// 9: uint32 LightParamsID (underwater)
// ... more params for death, phases, etc.
const auto* activeLayout = pipeline::getActiveDBCLayout();
const auto* lL = activeLayout ? activeLayout->getLayout("Light") : nullptr;
for (uint32_t i = 0; i < recordCount; ++i) {
LightVolume volume;
volume.lightId = dbc->getUInt32(i, 0);
volume.mapId = dbc->getUInt32(i, 1);
volume.lightId = dbc->getUInt32(i, lL ? (*lL)["ID"] : 0);
volume.mapId = dbc->getUInt32(i, lL ? (*lL)["MapID"] : 1);
// Position (note: DBC stores as x,z,y - need to swap!)
float x = dbc->getFloat(i, 2);
float z = dbc->getFloat(i, 3);
float y = dbc->getFloat(i, 4);
float x = dbc->getFloat(i, lL ? (*lL)["X"] : 2);
float z = dbc->getFloat(i, lL ? (*lL)["Z"] : 3);
float y = dbc->getFloat(i, lL ? (*lL)["Y"] : 4);
volume.position = glm::vec3(x, y, z); // Convert to x,y,z
volume.innerRadius = dbc->getFloat(i, 5);
volume.outerRadius = dbc->getFloat(i, 6);
volume.innerRadius = dbc->getFloat(i, lL ? (*lL)["InnerRadius"] : 5);
volume.outerRadius = dbc->getFloat(i, lL ? (*lL)["OuterRadius"] : 6);
// LightParams IDs for different conditions
volume.lightParamsId = dbc->getUInt32(i, 7);
volume.lightParamsId = dbc->getUInt32(i, lL ? (*lL)["LightParamsID"] : 7);
if (dbc->getFieldCount() > 8) {
volume.lightParamsIdRain = dbc->getUInt32(i, 8);
volume.lightParamsIdRain = dbc->getUInt32(i, lL ? (*lL)["LightParamsIDRain"] : 8);
}
if (dbc->getFieldCount() > 9) {
volume.lightParamsIdUnderwater = dbc->getUInt32(i, 9);
volume.lightParamsIdUnderwater = dbc->getUInt32(i, lL ? (*lL)["LightParamsIDUnderwater"] : 9);
}
// Add to map-specific list
@ -126,8 +130,9 @@ bool LightingManager::loadLightParamsDbc(pipeline::AssetManager* assetManager) {
LOG_INFO("Loaded LightParams.dbc: ", recordCount, " profiles");
// Create profile entries (will be populated by band loading)
const auto* lpL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("LightParams") : nullptr;
for (uint32_t i = 0; i < recordCount; ++i) {
uint32_t paramId = dbc->getUInt32(i, 0);
uint32_t paramId = dbc->getUInt32(i, lpL ? (*lpL)["LightParamsID"] : 0);
LightParamsProfile profile;
profile.lightParamsId = paramId;
lightParamsProfiles_[paramId] = profile;
@ -147,8 +152,9 @@ bool LightingManager::loadLightBandDbcs(pipeline::AssetManager* assetManager) {
// Parse int bands
// Structure: ID, Entry (block index), NumValues, Time[16], Color[16]
// Block index = LightParamsID * 18 + channel
const auto* libL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("LightIntBand") : nullptr;
for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) {
uint32_t blockIndex = dbc->getUInt32(i, 1);
uint32_t blockIndex = dbc->getUInt32(i, libL ? (*libL)["BlockIndex"] : 1);
uint32_t lightParamsId = blockIndex / 18;
uint32_t channelIndex = blockIndex % 18;
@ -158,18 +164,20 @@ bool LightingManager::loadLightBandDbcs(pipeline::AssetManager* assetManager) {
if (channelIndex >= LightParamsProfile::COLOR_CHANNEL_COUNT) continue;
ColorBand& band = it->second.colorBands[channelIndex];
band.numKeyframes = dbc->getUInt32(i, 2);
band.numKeyframes = dbc->getUInt32(i, libL ? (*libL)["NumKeyframes"] : 2);
if (band.numKeyframes > 16) band.numKeyframes = 16;
// Read time keys (field 3-18) - stored as uint16 half-minutes
uint32_t timeKeyBase = libL ? (*libL)["TimeKey0"] : 3;
for (uint8_t k = 0; k < band.numKeyframes && k < 16; ++k) {
uint32_t timeValue = dbc->getUInt32(i, 3 + k);
uint32_t timeValue = dbc->getUInt32(i, timeKeyBase + k);
band.times[k] = static_cast<uint16_t>(timeValue % 2880); // Clamp to valid range
}
// Read color values (field 19-34) - stored as BGRA packed uint32
uint32_t valueBase = libL ? (*libL)["Value0"] : 19;
for (uint8_t k = 0; k < band.numKeyframes && k < 16; ++k) {
uint32_t colorBGRA = dbc->getUInt32(i, 19 + k);
uint32_t colorBGRA = dbc->getUInt32(i, valueBase + k);
band.colors[k] = dbcColorToVec3(colorBGRA);
}
}
@ -186,8 +194,9 @@ bool LightingManager::loadLightBandDbcs(pipeline::AssetManager* assetManager) {
// Parse float bands
// Structure: ID, Entry (block index), NumValues, Time[16], Value[16]
// Block index = LightParamsID * 6 + channel
const auto* lfbL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("LightFloatBand") : nullptr;
for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) {
uint32_t blockIndex = dbc->getUInt32(i, 1);
uint32_t blockIndex = dbc->getUInt32(i, lfbL ? (*lfbL)["BlockIndex"] : 1);
uint32_t lightParamsId = blockIndex / 6;
uint32_t channelIndex = blockIndex % 6;
@ -197,18 +206,20 @@ bool LightingManager::loadLightBandDbcs(pipeline::AssetManager* assetManager) {
if (channelIndex >= LightParamsProfile::FLOAT_CHANNEL_COUNT) continue;
FloatBand& band = it->second.floatBands[channelIndex];
band.numKeyframes = dbc->getUInt32(i, 2);
band.numKeyframes = dbc->getUInt32(i, lfbL ? (*lfbL)["NumKeyframes"] : 2);
if (band.numKeyframes > 16) band.numKeyframes = 16;
// Read time keys (field 3-18)
uint32_t timeKeyBase = lfbL ? (*lfbL)["TimeKey0"] : 3;
for (uint8_t k = 0; k < band.numKeyframes && k < 16; ++k) {
uint32_t timeValue = dbc->getUInt32(i, 3 + k);
uint32_t timeValue = dbc->getUInt32(i, timeKeyBase + k);
band.times[k] = static_cast<uint16_t>(timeValue % 2880); // Clamp to valid range
}
// Read float values (field 19-34)
uint32_t valueBase = lfbL ? (*lfbL)["Value0"] : 19;
for (uint8_t k = 0; k < band.numKeyframes && k < 16; ++k) {
band.values[k] = dbc->getFloat(i, 19 + k);
band.values[k] = dbc->getFloat(i, valueBase + k);
}
}
}

View file

@ -27,6 +27,7 @@
#include <algorithm>
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_loader.hpp"
#include "pipeline/dbc_layout.hpp"
#include "pipeline/m2_loader.hpp"
#include "pipeline/wmo_loader.hpp"
#include "pipeline/adt_loader.hpp"
@ -156,11 +157,16 @@ static void loadEmotesFromDbc() {
return;
}
const auto* activeLayout = pipeline::getActiveDBCLayout();
const auto* etdL = activeLayout ? activeLayout->getLayout("EmotesTextData") : nullptr;
const auto* emL = activeLayout ? activeLayout->getLayout("Emotes") : nullptr;
const auto* etL = activeLayout ? activeLayout->getLayout("EmotesText") : nullptr;
std::unordered_map<uint32_t, std::string> textData;
textData.reserve(emotesTextDataDbc->getRecordCount());
for (uint32_t r = 0; r < emotesTextDataDbc->getRecordCount(); ++r) {
uint32_t id = emotesTextDataDbc->getUInt32(r, 0);
std::string text = emotesTextDataDbc->getString(r, 1);
uint32_t id = emotesTextDataDbc->getUInt32(r, etdL ? (*etdL)["ID"] : 0);
std::string text = emotesTextDataDbc->getString(r, etdL ? (*etdL)["Text"] : 1);
if (!text.empty()) textData.emplace(id, std::move(text));
}
@ -168,8 +174,8 @@ static void loadEmotesFromDbc() {
if (auto emotesDbc = assetManager->loadDBC("Emotes.dbc"); emotesDbc && emotesDbc->isLoaded()) {
emoteIdToAnim.reserve(emotesDbc->getRecordCount());
for (uint32_t r = 0; r < emotesDbc->getRecordCount(); ++r) {
uint32_t emoteId = emotesDbc->getUInt32(r, 0);
uint32_t animId = emotesDbc->getUInt32(r, 2);
uint32_t emoteId = emotesDbc->getUInt32(r, emL ? (*emL)["ID"] : 0);
uint32_t animId = emotesDbc->getUInt32(r, emL ? (*emL)["AnimID"] : 2);
if (animId != 0) emoteIdToAnim[emoteId] = animId;
}
}
@ -177,10 +183,10 @@ static void loadEmotesFromDbc() {
EMOTE_TABLE.clear();
EMOTE_TABLE.reserve(emotesTextDbc->getRecordCount());
for (uint32_t r = 0; r < emotesTextDbc->getRecordCount(); ++r) {
std::string cmdRaw = emotesTextDbc->getString(r, 1);
std::string cmdRaw = emotesTextDbc->getString(r, etL ? (*etL)["Command"] : 1);
if (cmdRaw.empty()) continue;
uint32_t emoteRef = emotesTextDbc->getUInt32(r, 2);
uint32_t emoteRef = emotesTextDbc->getUInt32(r, etL ? (*etL)["EmoteRef"] : 2);
uint32_t animId = 0;
auto animIt = emoteIdToAnim.find(emoteRef);
if (animIt != emoteIdToAnim.end()) {
@ -189,8 +195,8 @@ static void loadEmotesFromDbc() {
animId = emoteRef; // fallback if EmotesText stores animation id directly
}
uint32_t senderTargetTextId = emotesTextDbc->getUInt32(r, 5); // unisex, target, sender
uint32_t senderNoTargetTextId = emotesTextDbc->getUInt32(r, 9); // unisex, no target, sender
uint32_t senderTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderTargetTextID"] : 5); // unisex, target, sender
uint32_t senderNoTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderNoTargetTextID"] : 9); // unisex, no target, sender
std::string textTarget;
std::string textNoTarget;

View file

@ -1,6 +1,7 @@
#include "rendering/world_map.hpp"
#include "rendering/shader.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_layout.hpp"
#include "core/coordinates.hpp"
#include "core/input.hpp"
#include "core/logger.hpp"
@ -182,13 +183,16 @@ void WorldMap::loadZonesFromDBC() {
if (!zones.empty() || !assetManager) return;
// Step 1: Resolve mapID from Map.dbc
const auto* activeLayout = pipeline::getActiveDBCLayout();
const auto* mapL = activeLayout ? activeLayout->getLayout("Map") : nullptr;
int mapID = -1;
auto mapDbc = assetManager->loadDBC("Map.dbc");
if (mapDbc && mapDbc->isLoaded()) {
for (uint32_t i = 0; i < mapDbc->getRecordCount(); i++) {
std::string dir = mapDbc->getString(i, 1);
std::string dir = mapDbc->getString(i, mapL ? (*mapL)["InternalName"] : 1);
if (dir == mapName) {
mapID = static_cast<int>(mapDbc->getUInt32(i, 0));
mapID = static_cast<int>(mapDbc->getUInt32(i, mapL ? (*mapL)["ID"] : 0));
LOG_INFO("WorldMap: Map.dbc '", mapName, "' -> mapID=", mapID);
break;
}
@ -207,12 +211,13 @@ void WorldMap::loadZonesFromDBC() {
}
// Step 2: Load AreaTable explore flags by areaID.
const auto* atL = activeLayout ? activeLayout->getLayout("AreaTable") : nullptr;
std::unordered_map<uint32_t, uint32_t> exploreFlagByAreaId;
auto areaDbc = assetManager->loadDBC("AreaTable.dbc");
if (areaDbc && areaDbc->isLoaded() && areaDbc->getFieldCount() > 3) {
for (uint32_t i = 0; i < areaDbc->getRecordCount(); i++) {
const uint32_t areaId = areaDbc->getUInt32(i, 0);
const uint32_t exploreFlag = areaDbc->getUInt32(i, 3);
const uint32_t areaId = areaDbc->getUInt32(i, atL ? (*atL)["ID"] : 0);
const uint32_t exploreFlag = areaDbc->getUInt32(i, atL ? (*atL)["ExploreFlag"] : 3);
if (areaId != 0) {
exploreFlagByAreaId[areaId] = exploreFlag;
}
@ -236,20 +241,22 @@ void WorldMap::loadZonesFromDBC() {
// 4: locLeft, 5: locRight, 6: locTop, 7: locBottom
// 8: displayMapID, 9: defaultDungeonFloor, 10: parentWorldMapID
const auto* wmaL = activeLayout ? activeLayout->getLayout("WorldMapArea") : nullptr;
for (uint32_t i = 0; i < wmaDbc->getRecordCount(); i++) {
uint32_t recMapID = wmaDbc->getUInt32(i, 1);
uint32_t recMapID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["MapID"] : 1);
if (static_cast<int>(recMapID) != mapID) continue;
WorldMapZone zone;
zone.wmaID = wmaDbc->getUInt32(i, 0);
zone.areaID = wmaDbc->getUInt32(i, 2);
zone.areaName = wmaDbc->getString(i, 3);
zone.locLeft = wmaDbc->getFloat(i, 4);
zone.locRight = wmaDbc->getFloat(i, 5);
zone.locTop = wmaDbc->getFloat(i, 6);
zone.locBottom = wmaDbc->getFloat(i, 7);
zone.displayMapID = wmaDbc->getUInt32(i, 8);
zone.parentWorldMapID = wmaDbc->getUInt32(i, 10);
zone.wmaID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["ID"] : 0);
zone.areaID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["AreaID"] : 2);
zone.areaName = wmaDbc->getString(i, wmaL ? (*wmaL)["AreaName"] : 3);
zone.locLeft = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocLeft"] : 4);
zone.locRight = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocRight"] : 5);
zone.locTop = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocTop"] : 6);
zone.locBottom = wmaDbc->getFloat(i, wmaL ? (*wmaL)["LocBottom"] : 7);
zone.displayMapID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["DisplayMapID"] : 8);
zone.parentWorldMapID = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["ParentWorldMapID"] : 10);
auto exploreIt = exploreFlagByAreaId.find(zone.areaID);
if (exploreIt != exploreFlagByAreaId.end()) {
zone.exploreFlag = exploreIt->second;
@ -258,10 +265,10 @@ void WorldMap::loadZonesFromDBC() {
int idx = static_cast<int>(zones.size());
// Debug: also log raw uint32 values for bounds fields
uint32_t raw4 = wmaDbc->getUInt32(i, 4);
uint32_t raw5 = wmaDbc->getUInt32(i, 5);
uint32_t raw6 = wmaDbc->getUInt32(i, 6);
uint32_t raw7 = wmaDbc->getUInt32(i, 7);
uint32_t raw4 = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["LocLeft"] : 4);
uint32_t raw5 = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["LocRight"] : 5);
uint32_t raw6 = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["LocTop"] : 6);
uint32_t raw7 = wmaDbc->getUInt32(i, wmaL ? (*wmaL)["LocBottom"] : 7);
LOG_INFO("WorldMap: zone[", idx, "] areaID=", zone.areaID,
" '", zone.areaName, "' L=", zone.locLeft,