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 aa16a687c2
commit 7092844b5e
51 changed files with 5258 additions and 887 deletions

View file

@ -5,6 +5,7 @@
#include "rendering/renderer.hpp"
#include "pipeline/asset_manager.hpp"
#include "audio/music_manager.hpp"
#include "game/expansion_profile.hpp"
#include <imgui.h>
#include <filesystem>
#include <sstream>
@ -148,9 +149,30 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
if (port < 1) port = 1;
if (port > 65535) port = 65535;
// Compatibility mode dropdown
const char* compatModes[] = { "3.3.5a" };
ImGui::Combo("Compatibility Mode", &compatibilityMode, compatModes, IM_ARRAYSIZE(compatModes));
// Expansion selector (populated from ExpansionRegistry)
auto* registry = core::Application::getInstance().getExpansionRegistry();
if (registry && !registry->getAllProfiles().empty()) {
auto& profiles = registry->getAllProfiles();
// Build combo items: "WotLK (3.3.5a)"
std::string preview;
if (expansionIndex >= 0 && expansionIndex < static_cast<int>(profiles.size())) {
preview = profiles[expansionIndex].shortName + " (" + profiles[expansionIndex].versionString() + ")";
}
if (ImGui::BeginCombo("Expansion", preview.c_str())) {
for (int i = 0; i < static_cast<int>(profiles.size()); ++i) {
std::string label = profiles[i].shortName + " (" + profiles[i].versionString() + ")";
bool selected = (expansionIndex == i);
if (ImGui::Selectable(label.c_str(), selected)) {
expansionIndex = i;
registry->setActive(profiles[i].id);
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
} else {
ImGui::Text("Expansion: WotLK 3.3.5a (default)");
}
ImGui::Spacing();
ImGui::Separator();
@ -291,6 +313,21 @@ void AuthScreen::attemptAuth(auth::AuthHandler& authHandler) {
failureReason = reason;
});
// Configure client version from active expansion profile
auto* reg = core::Application::getInstance().getExpansionRegistry();
if (reg) {
auto* profile = reg->getActive();
if (profile) {
auth::ClientInfo info;
info.majorVersion = profile->majorVersion;
info.minorVersion = profile->minorVersion;
info.patchVersion = profile->patchVersion;
info.build = profile->build;
info.protocolVersion = profile->protocolVersion;
authHandler.setClientInfo(info);
}
}
if (authHandler.connect(hostname, static_cast<uint16_t>(port))) {
authenticating = true;
authTimer = 0.0f;
@ -350,6 +387,11 @@ void AuthScreen::saveLoginInfo() {
if (!savedPasswordHash.empty()) {
out << "password_hash=" << savedPasswordHash << "\n";
}
// Save active expansion id
auto* expReg = core::Application::getInstance().getExpansionRegistry();
if (expReg && !expReg->getActiveId().empty()) {
out << "expansion=" << expReg->getActiveId() << "\n";
}
LOG_INFO("Login info saved to ", path);
}
@ -376,6 +418,15 @@ void AuthScreen::loadLoginInfo() {
username[sizeof(username) - 1] = '\0';
} else if (key == "password_hash" && !val.empty()) {
savedPasswordHash = val;
} else if (key == "expansion" && !val.empty()) {
auto* expReg = core::Application::getInstance().getExpansionRegistry();
if (expReg && expReg->setActive(val)) {
// Find matching index
auto& profiles = expReg->getAllProfiles();
for (int i = 0; i < static_cast<int>(profiles.size()); ++i) {
if (profiles[i].id == val) { expansionIndex = i; break; }
}
}
}
}

View file

@ -2,6 +2,7 @@
#include "rendering/character_preview.hpp"
#include "game/game_handler.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_layout.hpp"
#include <imgui.h>
#include <cstring>
@ -169,16 +170,17 @@ void CharacterCreateScreen::updateAppearanceRanges() {
uint32_t targetRaceId = static_cast<uint32_t>(allRaces[raceIndex]);
uint32_t targetSexId = (genderIndex == 1) ? 1u : 0u;
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
int skinMax = -1;
int hairStyleMax = -1;
for (uint32_t r = 0; r < dbc->getRecordCount(); r++) {
uint32_t raceId = dbc->getUInt32(r, 1);
uint32_t sexId = dbc->getUInt32(r, 2);
uint32_t raceId = dbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
uint32_t sexId = dbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
if (raceId != targetRaceId || sexId != targetSexId) continue;
uint32_t baseSection = dbc->getUInt32(r, 3);
uint32_t variationIndex = dbc->getUInt32(r, 8);
uint32_t colorIndex = dbc->getUInt32(r, 9);
uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8);
uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9);
if (baseSection == 0 && variationIndex == 0) {
skinMax = std::max(skinMax, static_cast<int>(colorIndex));
@ -199,13 +201,13 @@ void CharacterCreateScreen::updateAppearanceRanges() {
int faceMax = -1;
std::vector<uint8_t> hairColorIds;
for (uint32_t r = 0; r < dbc->getRecordCount(); r++) {
uint32_t raceId = dbc->getUInt32(r, 1);
uint32_t sexId = dbc->getUInt32(r, 2);
uint32_t raceId = dbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
uint32_t sexId = dbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
if (raceId != targetRaceId || sexId != targetSexId) continue;
uint32_t baseSection = dbc->getUInt32(r, 3);
uint32_t variationIndex = dbc->getUInt32(r, 8);
uint32_t colorIndex = dbc->getUInt32(r, 9);
uint32_t baseSection = dbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8);
uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9);
if (baseSection == 1 && colorIndex == static_cast<uint32_t>(skin)) {
faceMax = std::max(faceMax, static_cast<int>(variationIndex));
@ -232,12 +234,13 @@ void CharacterCreateScreen::updateAppearanceRanges() {
}
int facialMax = -1;
auto facialDbc = assetManager_->loadDBC("CharacterFacialHairStyles.dbc");
const auto* fhL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharacterFacialHairStyles") : nullptr;
if (facialDbc) {
for (uint32_t r = 0; r < facialDbc->getRecordCount(); r++) {
uint32_t raceId = facialDbc->getUInt32(r, 0);
uint32_t sexId = facialDbc->getUInt32(r, 1);
uint32_t raceId = facialDbc->getUInt32(r, fhL ? (*fhL)["RaceID"] : 0);
uint32_t sexId = facialDbc->getUInt32(r, fhL ? (*fhL)["SexID"] : 1);
if (raceId != targetRaceId || sexId != targetSexId) continue;
uint32_t variation = facialDbc->getUInt32(r, 2);
uint32_t variation = facialDbc->getUInt32(r, fhL ? (*fhL)["Variation"] : 2);
facialMax = std::max(facialMax, static_cast<int>(variation));
}
}

View file

@ -22,6 +22,9 @@
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_loader.hpp"
#include "pipeline/blp_loader.hpp"
#include "pipeline/dbc_layout.hpp"
#include "pipeline/hd_pack_manager.hpp"
#include "game/expansion_profile.hpp"
#include "core/logger.hpp"
#include <imgui.h>
#include <algorithm>
@ -2360,7 +2363,8 @@ void GameScreen::updateCharacterTextures(game::Inventory& inventory) {
int32_t recIdx = displayInfoDbc->findRecordById(cloakDisplayId);
if (recIdx >= 0) {
// DBC field 3 = modelTexture_1 (cape texture name)
std::string capeName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), 3);
const auto* dispL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
std::string capeName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), dispL ? (*dispL)["LeftModelTexture"] : 3);
if (!capeName.empty()) {
std::string capePath = "Item\\ObjectComponents\\Cape\\" + capeName + ".blp";
GLuint capeTex = charRenderer->loadTexture(capePath);
@ -2422,10 +2426,11 @@ GLuint GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManager* am) {
// Load SpellIcon.dbc: field 0 = ID, field 1 = icon path
auto iconDbc = am->loadDBC("SpellIcon.dbc");
const auto* iconL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SpellIcon") : nullptr;
if (iconDbc && iconDbc->isLoaded()) {
for (uint32_t i = 0; i < iconDbc->getRecordCount(); i++) {
uint32_t id = iconDbc->getUInt32(i, 0);
std::string path = iconDbc->getString(i, 1);
uint32_t id = iconDbc->getUInt32(i, iconL ? (*iconL)["ID"] : 0);
std::string path = iconDbc->getString(i, iconL ? (*iconL)["Path"] : 1);
if (!path.empty() && id > 0) {
spellIconPaths_[id] = path;
}
@ -2434,10 +2439,11 @@ GLuint GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManager* am) {
// Load Spell.dbc: field 133 = SpellIconID
auto spellDbc = am->loadDBC("Spell.dbc");
const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
if (spellDbc && spellDbc->isLoaded() && spellDbc->getFieldCount() > 133) {
for (uint32_t i = 0; i < spellDbc->getRecordCount(); i++) {
uint32_t id = spellDbc->getUInt32(i, 0);
uint32_t iconId = spellDbc->getUInt32(i, 133);
uint32_t id = spellDbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0);
uint32_t iconId = spellDbc->getUInt32(i, spellL ? (*spellL)["IconID"] : 133);
if (id > 0 && iconId > 0) {
spellIconIds_[id] = iconId;
}
@ -2530,8 +2536,9 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
if (assetMgr && assetMgr->isInitialized()) {
auto dbc = assetMgr->loadDBC("Spell.dbc");
if (dbc && dbc->isLoaded()) {
const auto* actionSpellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
uint32_t fieldCount = dbc->getFieldCount();
uint32_t nameField = 136;
uint32_t nameField = actionSpellL ? (*actionSpellL)["Name"] : 136;
if (fieldCount < 137) {
if (fieldCount > 10) {
nameField = fieldCount > 140 ? 136 : 1;
@ -2542,7 +2549,7 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
uint32_t count = dbc->getRecordCount();
actionSpellNames.reserve(count);
for (uint32_t r = 0; r < count; ++r) {
uint32_t id = dbc->getUInt32(r, 0);
uint32_t id = dbc->getUInt32(r, actionSpellL ? (*actionSpellL)["ID"] : 0);
std::string name = dbc->getString(r, nameField);
if (!name.empty() && id > 0) {
actionSpellNames[id] = name;
@ -4835,6 +4842,79 @@ void GameScreen::renderSettingsWindow() {
ImGui::EndTabItem();
}
// ============================================================
// HD TEXTURES TAB
// ============================================================
if (ImGui::BeginTabItem("HD Textures")) {
ImGui::Spacing();
auto& app = core::Application::getInstance();
auto* hdMgr = app.getHDPackManager();
if (hdMgr) {
const auto& packs = hdMgr->getAllPacks();
if (packs.empty()) {
ImGui::TextWrapped("No HD texture packs found.");
ImGui::Spacing();
ImGui::TextWrapped("Place packs in Data/hd/<pack_name>/ with a pack.json and manifest.json.");
} else {
ImGui::Text("Available HD Texture Packs:");
ImGui::Spacing();
bool changed = false;
for (const auto& pack : packs) {
bool enabled = pack.enabled;
if (ImGui::Checkbox(pack.name.c_str(), &enabled)) {
hdMgr->setPackEnabled(pack.id, enabled);
changed = true;
}
ImGui::SameLine(0, 10);
ImGui::TextDisabled("(%u MB)", pack.totalSizeMB);
if (!pack.group.empty()) {
ImGui::SameLine(0, 10);
ImGui::TextDisabled("[%s]", pack.group.c_str());
}
if (!pack.expansions.empty()) {
std::string expList;
for (const auto& e : pack.expansions) {
if (!expList.empty()) expList += ", ";
expList += e;
}
ImGui::TextDisabled(" Compatible: %s", expList.c_str());
}
}
if (changed) {
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (ImGui::Button("Apply HD Packs", ImVec2(-1, 0))) {
std::string expansionId = "wotlk";
if (app.getExpansionRegistry() && app.getExpansionRegistry()->getActive()) {
expansionId = app.getExpansionRegistry()->getActive()->id;
}
hdMgr->applyToAssetManager(app.getAssetManager(), expansionId);
// Save settings
std::string settingsDir;
const char* xdg = std::getenv("XDG_DATA_HOME");
if (xdg && *xdg) {
settingsDir = std::string(xdg) + "/wowee";
} else {
const char* home = std::getenv("HOME");
settingsDir = std::string(home ? home : ".") + "/.local/share/wowee";
}
hdMgr->saveSettings(settingsDir + "/settings.cfg");
}
}
}
} else {
ImGui::Text("HD Pack Manager not available.");
}
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}

View file

@ -7,6 +7,7 @@
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_loader.hpp"
#include "pipeline/blp_loader.hpp"
#include "pipeline/dbc_layout.hpp"
#include "core/logger.hpp"
#include <imgui.h>
#include <SDL2/SDL.h>
@ -60,7 +61,8 @@ GLuint InventoryScreen::getItemIcon(uint32_t displayInfoId) {
}
// Field 5 = inventoryIcon_1
std::string iconName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), 5);
const auto* dispL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
std::string iconName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), dispL ? (*dispL)["InventoryIcon"] : 5);
if (iconName.empty()) {
iconCache_[displayInfoId] = 0;
return 0;

View file

@ -4,6 +4,7 @@
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_loader.hpp"
#include "pipeline/blp_loader.hpp"
#include "pipeline/dbc_layout.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <map>
@ -30,17 +31,18 @@ void SpellbookScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
// WoW 3.3.5a Spell.dbc fields (0-based):
// 0 = SpellID, 4 = Attributes, 133 = SpellIconID, 136 = SpellName_enUS, 153 = RankText_enUS
const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
uint32_t count = dbc->getRecordCount();
for (uint32_t i = 0; i < count; ++i) {
uint32_t spellId = dbc->getUInt32(i, 0);
uint32_t spellId = dbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0);
if (spellId == 0) continue;
SpellInfo info;
info.spellId = spellId;
info.attributes = dbc->getUInt32(i, 4);
info.iconId = dbc->getUInt32(i, 133);
info.name = dbc->getString(i, 136);
info.rank = dbc->getString(i, 153);
info.attributes = dbc->getUInt32(i, spellL ? (*spellL)["Attributes"] : 4);
info.iconId = dbc->getUInt32(i, spellL ? (*spellL)["IconID"] : 133);
info.name = dbc->getString(i, spellL ? (*spellL)["Name"] : 136);
info.rank = dbc->getString(i, spellL ? (*spellL)["Rank"] : 153);
if (!info.name.empty()) {
spellData[spellId] = std::move(info);
@ -63,9 +65,10 @@ void SpellbookScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) {
return;
}
const auto* iconL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SpellIcon") : nullptr;
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
uint32_t id = dbc->getUInt32(i, 0);
std::string path = dbc->getString(i, 1);
uint32_t id = dbc->getUInt32(i, iconL ? (*iconL)["ID"] : 0);
std::string path = dbc->getString(i, iconL ? (*iconL)["Path"] : 1);
if (!path.empty() && id > 0) {
spellIconPaths[id] = path;
}
@ -82,11 +85,12 @@ void SpellbookScreen::loadSkillLineDBCs(pipeline::AssetManager* assetManager) {
// Load SkillLine.dbc: field 0 = ID, field 1 = categoryID, field 3 = name_enUS
auto skillLineDbc = assetManager->loadDBC("SkillLine.dbc");
const auto* slL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SkillLine") : nullptr;
if (skillLineDbc && skillLineDbc->isLoaded()) {
for (uint32_t i = 0; i < skillLineDbc->getRecordCount(); i++) {
uint32_t id = skillLineDbc->getUInt32(i, 0);
uint32_t category = skillLineDbc->getUInt32(i, 1);
std::string name = skillLineDbc->getString(i, 3);
uint32_t id = skillLineDbc->getUInt32(i, slL ? (*slL)["ID"] : 0);
uint32_t category = skillLineDbc->getUInt32(i, slL ? (*slL)["Category"] : 1);
std::string name = skillLineDbc->getString(i, slL ? (*slL)["Name"] : 3);
if (id > 0 && !name.empty()) {
skillLineNames[id] = name;
skillLineCategories[id] = category;
@ -99,10 +103,11 @@ void SpellbookScreen::loadSkillLineDBCs(pipeline::AssetManager* assetManager) {
// Load SkillLineAbility.dbc: field 0 = ID, field 1 = skillLineID, field 2 = spellID
auto slaDbc = assetManager->loadDBC("SkillLineAbility.dbc");
const auto* slaL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SkillLineAbility") : nullptr;
if (slaDbc && slaDbc->isLoaded()) {
for (uint32_t i = 0; i < slaDbc->getRecordCount(); i++) {
uint32_t skillLineId = slaDbc->getUInt32(i, 1);
uint32_t spellId = slaDbc->getUInt32(i, 2);
uint32_t skillLineId = slaDbc->getUInt32(i, slaL ? (*slaL)["SkillLineID"] : 1);
uint32_t spellId = slaDbc->getUInt32(i, slaL ? (*slaL)["SpellID"] : 2);
if (spellId > 0 && skillLineId > 0) {
spellToSkillLine[spellId] = skillLineId;
}

View file

@ -4,6 +4,7 @@
#include "core/logger.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/blp_loader.hpp"
#include "pipeline/dbc_layout.hpp"
#include <algorithm>
#include <GL/glew.h>
@ -448,15 +449,16 @@ void TalentScreen::loadSpellDBC(pipeline::AssetManager* assetManager) {
}
// WoW 3.3.5a Spell.dbc fields: 0=SpellID, 133=SpellIconID, 136=SpellName_enUS, 139=Tooltip_enUS
const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
uint32_t count = dbc->getRecordCount();
for (uint32_t i = 0; i < count; ++i) {
uint32_t spellId = dbc->getUInt32(i, 0);
uint32_t spellId = dbc->getUInt32(i, spellL ? (*spellL)["ID"] : 0);
if (spellId == 0) continue;
uint32_t iconId = dbc->getUInt32(i, 133);
uint32_t iconId = dbc->getUInt32(i, spellL ? (*spellL)["IconID"] : 133);
spellIconIds[spellId] = iconId;
std::string tooltip = dbc->getString(i, 139);
std::string tooltip = dbc->getString(i, spellL ? (*spellL)["Tooltip"] : 139);
if (!tooltip.empty()) {
spellTooltips[spellId] = tooltip;
}
@ -477,9 +479,10 @@ void TalentScreen::loadSpellIconDBC(pipeline::AssetManager* assetManager) {
return;
}
const auto* iconL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SpellIcon") : nullptr;
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
uint32_t id = dbc->getUInt32(i, 0);
std::string path = dbc->getString(i, 1);
uint32_t id = dbc->getUInt32(i, iconL ? (*iconL)["ID"] : 0);
std::string path = dbc->getString(i, iconL ? (*iconL)["Path"] : 1);
if (!path.empty() && id > 0) {
spellIconPaths[id] = path;
}