Add per-expansion asset overlay system and fix CharSections DBC layout

Expansion overlays allow each expansion to supplement the base asset data
via an assetManifest field in expansion.json, loaded at priority 50 (below
HD packs). The asset extractor gains --reference-manifest for delta-only
extraction. Also fixes CharSections field indices (VariationIndex=4,
ColorIndex=5, Texture1=6) across all DBC layout references.
This commit is contained in:
Kelsi 2026-02-14 00:00:26 -08:00
parent a618827925
commit 7d115aaa70
13 changed files with 151 additions and 46 deletions

View file

@ -231,6 +231,16 @@ bool Application::initialize() {
}
hdPackManager_->applyToAssetManager(assetManager.get(), expansionId);
}
// Load expansion-specific asset overlay (priority 50, below HD packs at 100+)
if (expansionRegistry_) {
auto* activeProfile = expansionRegistry_->getActive();
if (activeProfile && !activeProfile->assetManifest.empty()) {
if (assetManager->addOverlayManifest(activeProfile->assetManifest, 50, "expansion_overlay")) {
LOG_INFO("Added expansion asset overlay: ", activeProfile->assetManifest);
}
}
}
} else {
LOG_WARNING("Failed to initialize asset manager - asset loading will be unavailable");
LOG_WARNING("Set WOW_DATA_PATH environment variable to your WoW Data directory");
@ -493,7 +503,19 @@ void Application::reloadExpansionData() {
if (assetManager && !profile->dataPath.empty()) {
assetManager->setExpansionDataPath(profile->dataPath);
assetManager->clearDBCCache();
// Swap expansion asset overlay
assetManager->removeOverlay("expansion_overlay");
if (!profile->assetManifest.empty()) {
if (assetManager->addOverlayManifest(profile->assetManifest, 50, "expansion_overlay")) {
LOG_INFO("Swapped expansion asset overlay: ", profile->assetManifest);
}
}
}
// Reset map name cache so it reloads from new expansion's Map.dbc
mapNameCacheLoaded_ = false;
mapNameById_.clear();
}
void Application::logoutToLogin() {
@ -1828,13 +1850,13 @@ void Application::spawnPlayerCharacter() {
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);
uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4);
uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5);
if (raceId != targetRaceId || sexId != targetSexId) continue;
// Section 0 = skin: match by colorIndex = skin byte
const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 4;
const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 6;
if (baseSection == 0 && !foundSkin && colorIndex == charSkinId) {
std::string tex1 = charSectionsDbc->getString(r, csTex1);
if (!tex1.empty()) {
@ -2409,28 +2431,26 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
// Resolve map folder name from Map.dbc (authoritative for world/instance maps).
// This is required for instances like DeeprunTram (map 369) that are not Azeroth/Kalimdor.
static bool mapNameCacheLoaded = false;
static std::unordered_map<uint32_t, std::string> mapNameById;
if (!mapNameCacheLoaded && assetManager) {
mapNameCacheLoaded = true;
if (!mapNameCacheLoaded_ && assetManager) {
mapNameCacheLoaded_ = true;
if (auto mapDbc = assetManager->loadDBC("Map.dbc"); mapDbc && mapDbc->isLoaded()) {
mapNameById.reserve(mapDbc->getRecordCount());
mapNameById_.reserve(mapDbc->getRecordCount());
const auto* mapL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Map") : nullptr;
for (uint32_t i = 0; i < mapDbc->getRecordCount(); i++) {
uint32_t id = mapDbc->getUInt32(i, mapL ? (*mapL)["ID"] : 0);
std::string internalName = mapDbc->getString(i, mapL ? (*mapL)["InternalName"] : 1);
if (!internalName.empty() && mapNameById.find(id) == mapNameById.end()) {
mapNameById[id] = std::move(internalName);
if (!internalName.empty() && mapNameById_.find(id) == mapNameById_.end()) {
mapNameById_[id] = std::move(internalName);
}
}
LOG_INFO("Loaded Map.dbc map-name cache: ", mapNameById.size(), " entries");
LOG_INFO("Loaded Map.dbc map-name cache: ", mapNameById_.size(), " entries");
} else {
LOG_WARNING("Map.dbc not available; using fallback map-id mapping");
}
}
std::string mapName;
if (auto it = mapNameById.find(mapId); it != mapNameById.end()) {
if (auto it = mapNameById_.find(mapId); it != mapNameById_.end()) {
mapName = it->second;
} else {
mapName = mapIdToName(mapId);
@ -3122,15 +3142,15 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
uint32_t raceId = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["RaceID"] : 1);
uint32_t sexId = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["SexID"] : 2);
uint32_t section = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["BaseSection"] : 3);
uint32_t variation = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["VariationIndex"] : 8);
uint32_t colorIdx = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["ColorIndex"] : 9);
uint32_t variation = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["VariationIndex"] : 4);
uint32_t colorIdx = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["ColorIndex"] : 5);
if (raceId != targetRace || sexId != targetSex) continue;
if (section != 3) continue; // Section 3 = hair
if (variation != static_cast<uint32_t>(extra.hairStyleId)) continue;
if (colorIdx != static_cast<uint32_t>(extra.hairColorId)) continue;
hairTexPath = charSectionsDbc->getString(r, csL2 ? (*csL2)["Texture1"] : 4);
hairTexPath = charSectionsDbc->getString(r, csL2 ? (*csL2)["Texture1"] : 6);
break;
}
@ -3667,7 +3687,7 @@ void Application::spawnOnlinePlayer(uint64_t guid,
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
uint32_t targetRaceId = raceId;
uint32_t targetSexId = genderId;
const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 4;
const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 6;
bool foundSkin = false;
bool foundUnderwear = false;
@ -3679,8 +3699,8 @@ void Application::spawnOnlinePlayer(uint64_t guid,
uint32_t rRace = charSectionsDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
uint32_t rSex = 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);
uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4);
uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5);
if (rRace != targetRaceId || rSex != targetSexId) continue;

View file

@ -176,6 +176,12 @@ bool ExpansionRegistry::loadProfile(const std::string& jsonPath, const std::stri
v = jsonValue(json, "locale"); if (!v.empty()) p.locale = v;
p.timezone = static_cast<uint32_t>(jsonInt(json, "timezone", static_cast<int>(p.timezone)));
}
// Expansion-specific asset manifest (overlay for base data)
p.assetManifest = jsonValue(json, "assetManifest");
if (!p.assetManifest.empty() && p.assetManifest[0] != '/') {
p.assetManifest = dirPath + "/" + p.assetManifest;
}
p.maxLevel = static_cast<uint32_t>(jsonInt(json, "maxLevel", 60));
p.races = jsonUintArray(json, "races");
p.classes = jsonUintArray(json, "classes");

View file

@ -274,6 +274,7 @@ void GameHandler::update(float deltaTime) {
auto pkt = InspectPacket::build(guid);
socket->send(pkt);
inspectRateLimit_ = 0.75f; // keep it gentle
LOG_INFO("Sent CMSG_INSPECT for player 0x", std::hex, guid, std::dec);
}
}
@ -5543,6 +5544,7 @@ void GameHandler::updateOtherPlayerVisibleItems(uint64_t guid, const std::map<ui
// Layout not detected yet — queue this player for inspect as fallback.
if (socket && state == WorldState::IN_WORLD) {
pendingAutoInspect_.insert(guid);
LOG_INFO("Queued player 0x", std::hex, guid, std::dec, " for auto-inspect (layout not detected)");
}
return;
}

View file

@ -24,8 +24,8 @@ void DBCLayout::loadWotlkDefaults() {
// CharSections.dbc
layouts_["CharSections"] = {{{ "RaceID", 1 }, { "SexID", 2 }, { "BaseSection", 3 },
{ "Texture1", 4 }, { "Texture2", 5 }, { "Texture3", 6 },
{ "VariationIndex", 8 }, { "ColorIndex", 9 }}};
{ "VariationIndex", 4 }, { "ColorIndex", 5 },
{ "Texture1", 6 }, { "Texture2", 7 }, { "Texture3", 8 }}};
// SpellIcon.dbc (Icon.dbc in code but actually SpellIcon)
layouts_["SpellIcon"] = {{{ "ID", 0 }, { "Path", 1 }}};

View file

@ -167,19 +167,24 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
uint32_t fRace = csL ? (*csL)["RaceID"] : 1;
uint32_t fSex = csL ? (*csL)["SexID"] : 2;
uint32_t fBase = csL ? (*csL)["BaseSection"] : 3;
uint32_t fVar = csL ? (*csL)["VariationIndex"] : 4;
uint32_t fColor = csL ? (*csL)["ColorIndex"] : 5;
for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
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);
uint32_t raceId = charSectionsDbc->getUInt32(r, fRace);
uint32_t sexId = charSectionsDbc->getUInt32(r, fSex);
uint32_t baseSection = charSectionsDbc->getUInt32(r, fBase);
uint32_t variationIndex = charSectionsDbc->getUInt32(r, fVar);
uint32_t colorIndex = charSectionsDbc->getUInt32(r, fColor);
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, csL ? (*csL)["Texture1"] : 4);
std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 6);
if (!tex1.empty()) {
bodySkinPath_ = tex1;
foundSkin = true;
@ -189,8 +194,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, csL ? (*csL)["Texture1"] : 4);
std::string tex2 = charSectionsDbc->getString(r, csL ? (*csL)["Texture2"] : 5);
std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 6);
std::string tex2 = charSectionsDbc->getString(r, csL ? (*csL)["Texture2"] : 7);
if (!tex1.empty()) faceLowerPath = tex1;
if (!tex2.empty()) faceUpperPath = tex2;
foundFace = true;
@ -199,7 +204,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, csL ? (*csL)["Texture1"] : 4);
std::string tex1 = charSectionsDbc->getString(r, csL ? (*csL)["Texture1"] : 6);
if (!tex1.empty()) {
hairScalpPath = tex1;
foundHair = true;
@ -208,7 +213,7 @@ 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)) {
uint32_t texBase = csL ? (*csL)["Texture1"] : 4;
uint32_t texBase = csL ? (*csL)["Texture1"] : 6;
for (uint32_t f = texBase; f <= texBase + 2; f++) {
std::string tex = charSectionsDbc->getString(r, f);
if (!tex.empty()) {
@ -220,14 +225,9 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
}
}
LOG_INFO("CharPreview tex lookup: bodySkin=", bodySkinPath_, " faceLower=", faceLowerPath,
" faceUpper=", faceUpperPath, " hairScalp=", hairScalpPath,
" underwear=", underwearPaths.size());
// Assign texture filenames on model before GPU upload
for (size_t ti = 0; ti < model.textures.size(); ti++) {
auto& tex = model.textures[ti];
LOG_INFO(" model.textures[", ti, "]: type=", tex.type, " filename=", tex.filename);
if (tex.type == 1 && tex.filename.empty() && !bodySkinPath_.empty()) {
tex.filename = bodySkinPath_;
} else if (tex.type == 6 && tex.filename.empty() && !hairScalpPath.empty()) {
@ -256,9 +256,6 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
LOG_WARNING("CharacterPreview: failed to load model to GPU");
return false;
}
LOG_INFO("CharPreview: model loaded to GPU, textureLookup size=", model.textureLookup.size(),
" materials=", model.materials.size(), " batches=", model.batches.size());
// Composite body skin + face + underwear overlays
if (!bodySkinPath_.empty()) {
std::vector<std::string> layers;

View file

@ -179,8 +179,8 @@ void CharacterCreateScreen::updateAppearanceRanges() {
if (raceId != targetRaceId || sexId != targetSexId) continue;
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);
uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4);
uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5);
if (baseSection == 0 && variationIndex == 0) {
skinMax = std::max(skinMax, static_cast<int>(colorIndex));
@ -206,8 +206,8 @@ void CharacterCreateScreen::updateAppearanceRanges() {
if (raceId != targetRaceId || sexId != targetSexId) continue;
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);
uint32_t variationIndex = dbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4);
uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5);
if (baseSection == 1 && colorIndex == static_cast<uint32_t>(skin)) {
faceMax = std::max(faceMax, static_cast<int>(variationIndex));