fix: auto-detect CharSections.dbc layout and add Blood Elf/Draenei NPC voices

CharSections.dbc has different field layouts between stock WotLK (textures
at field 4-6) and Classic/TBC/Turtle/HD-textured WotLK (VariationIndex at
field 4). Add detectCharSectionsFields() that probes field-4 values at
runtime to determine the correct layout, so both stock and modded clients
work without JSON changes.

Also add BLOODELF_MALE/FEMALE and DRAENEI_MALE/FEMALE voice types to the
NPC voice system — previously all Blood Elf and Draenei NPCs fell through
to GENERIC (random dwarf/gnome/night elf/orc mix).
This commit is contained in:
Kelsi 2026-03-23 11:00:49 -07:00
parent d873f27070
commit 503f9ed650
7 changed files with 242 additions and 87 deletions

View file

@ -38,6 +38,10 @@ enum class VoiceType {
GNOME_FEMALE,
GOBLIN_MALE,
GOBLIN_FEMALE,
BLOODELF_MALE,
BLOODELF_FEMALE,
DRAENEI_MALE,
DRAENEI_FEMALE,
GENERIC, // Fallback
};

View file

@ -57,5 +57,40 @@ inline uint32_t dbcField(const std::string& dbcName, const std::string& fieldNam
return fm ? fm->field(fieldName) : 0xFFFFFFFF;
}
// Forward declaration
class DBCFile;
/**
* Resolved CharSections.dbc field indices.
*
* Stock WotLK 3.3.5a uses: Texture1=4, Texture2=5, Texture3=6, Flags=7,
* VariationIndex=8, ColorIndex=9 (textures first).
* Classic/TBC/Turtle and HD-texture WotLK use: VariationIndex=4, ColorIndex=5,
* Texture1=6, Texture2=7, Texture3=8, Flags=9 (variation first).
*
* detectCharSectionsFields() auto-detects which layout the actual DBC uses
* by sampling field-4 values: small integers (0-15) => variation-first,
* large values (string offsets) => texture-first.
*/
struct CharSectionsFields {
uint32_t raceId = 1;
uint32_t sexId = 2;
uint32_t baseSection = 3;
uint32_t variationIndex = 4;
uint32_t colorIndex = 5;
uint32_t texture1 = 6;
uint32_t texture2 = 7;
uint32_t texture3 = 8;
uint32_t flags = 9;
};
/**
* Detect the actual CharSections.dbc field layout by probing record data.
* @param dbc Loaded CharSections.dbc file (must not be null).
* @param csL JSON-derived field map (may be null defaults used).
* @return Resolved field indices for this particular DBC binary.
*/
CharSectionsFields detectCharSectionsFields(const DBCFile* dbc, const DBCFieldMap* csL);
} // namespace pipeline
} // namespace wowee

View file

@ -178,6 +178,30 @@ void NpcVoiceManager::loadVoiceSounds() {
loadCategory(vendorLibrary_, VoiceType::UNDEAD_FEMALE, "UndeadFemaleStandardNPC", "Vendor", 2);
loadCategory(pissedLibrary_, VoiceType::UNDEAD_FEMALE, "UndeadFemaleStandardNPC", "Pissed", 6);
// Blood Elf Male (TBC+ NPCBloodElfMaleStandard, sparse numbering up to 12)
loadCategory(greetingLibrary_, VoiceType::BLOODELF_MALE, "NPCBloodElfMaleStandard", "Greeting", 12);
loadCategory(farewellLibrary_, VoiceType::BLOODELF_MALE, "NPCBloodElfMaleStandard", "Farewell", 12);
loadCategory(vendorLibrary_, VoiceType::BLOODELF_MALE, "NPCBloodElfMaleStandard", "Vendor", 6);
loadCategory(pissedLibrary_, VoiceType::BLOODELF_MALE, "NPCBloodElfMaleStandard", "Pissed", 10);
// Blood Elf Female
loadCategory(greetingLibrary_, VoiceType::BLOODELF_FEMALE, "NPCBloodElfFemaleStandard", "Greeting", 12);
loadCategory(farewellLibrary_, VoiceType::BLOODELF_FEMALE, "NPCBloodElfFemaleStandard", "Farewell", 12);
loadCategory(vendorLibrary_, VoiceType::BLOODELF_FEMALE, "NPCBloodElfFemaleStandard", "Vendor", 6);
loadCategory(pissedLibrary_, VoiceType::BLOODELF_FEMALE, "NPCBloodElfFemaleStandard", "Pissed", 10);
// Draenei Male
loadCategory(greetingLibrary_, VoiceType::DRAENEI_MALE, "NPCDraeneiMaleStandard", "Greeting", 12);
loadCategory(farewellLibrary_, VoiceType::DRAENEI_MALE, "NPCDraeneiMaleStandard", "Farewell", 12);
loadCategory(vendorLibrary_, VoiceType::DRAENEI_MALE, "NPCDraeneiMaleStandard", "Vendor", 6);
loadCategory(pissedLibrary_, VoiceType::DRAENEI_MALE, "NPCDraeneiMaleStandard", "Pissed", 10);
// Draenei Female
loadCategory(greetingLibrary_, VoiceType::DRAENEI_FEMALE, "NPCDraeneiFemaleStandard", "Greeting", 12);
loadCategory(farewellLibrary_, VoiceType::DRAENEI_FEMALE, "NPCDraeneiFemaleStandard", "Farewell", 12);
loadCategory(vendorLibrary_, VoiceType::DRAENEI_FEMALE, "NPCDraeneiFemaleStandard", "Vendor", 6);
loadCategory(pissedLibrary_, VoiceType::DRAENEI_FEMALE, "NPCDraeneiFemaleStandard", "Pissed", 10);
// Load combat sounds from Character vocal files
// These use a different path structure: Sound\Character\{Race}\{Race}Vocal{Gender}\{Race}{Gender}{Sound}.wav
auto loadCombatCategory = [this](
@ -251,6 +275,38 @@ void NpcVoiceManager::loadVoiceSounds() {
loadCombatCategory(aggroLibrary_, VoiceType::TROLL_FEMALE, "Troll", "TrollFemale", "AttackMyTarget", 3);
loadCombatCategory(fleeLibrary_, VoiceType::TROLL_FEMALE, "Troll", "TrollFemale", "Flee", 2);
// Blood Elf and Draenei combat sounds (flat folder structure, no VocalMale/Female subfolder)
auto loadCombatFlat = [this](
std::unordered_map<VoiceType, std::vector<VoiceSample>>& library,
VoiceType type,
const std::string& raceFolder,
const std::string& raceGender,
const std::string& soundType,
int count) {
auto& samples = library[type];
for (int i = 1; i <= count; ++i) {
std::string num = (i < 10) ? ("0" + std::to_string(i)) : std::to_string(i);
std::string path = "Sound\\Character\\" + raceFolder + "\\" + raceGender + soundType + num + ".wav";
VoiceSample sample;
if (loadSound(path, sample)) samples.push_back(std::move(sample));
}
};
// Blood Elf combat sounds
loadCombatFlat(aggroLibrary_, VoiceType::BLOODELF_MALE, "BloodElf", "BloodElfMale", "AttackMyTarget", 3);
loadCombatFlat(fleeLibrary_, VoiceType::BLOODELF_MALE, "BloodElf", "BloodElfMale", "Flee", 3);
loadCombatFlat(aggroLibrary_, VoiceType::BLOODELF_FEMALE, "BloodElf", "BloodElfFemale", "AttackMyTarget", 3);
loadCombatFlat(fleeLibrary_, VoiceType::BLOODELF_FEMALE, "BloodElf", "BloodElfFemale", "Flee", 3);
// Draenei combat sounds
loadCombatFlat(aggroLibrary_, VoiceType::DRAENEI_MALE, "Draenei", "DraeneiMale", "AttackMyTarget", 3);
loadCombatFlat(fleeLibrary_, VoiceType::DRAENEI_MALE, "Draenei", "DraeneiMale", "Flee", 3);
loadCombatFlat(aggroLibrary_, VoiceType::DRAENEI_FEMALE, "Draenei", "DraeneiFemale", "AttackMyTarget", 3);
loadCombatFlat(fleeLibrary_, VoiceType::DRAENEI_FEMALE, "Draenei", "DraeneiFemale", "Flee", 3);
}
bool NpcVoiceManager::loadSound(const std::string& path, VoiceSample& sample) {

View file

@ -3666,23 +3666,23 @@ void Application::spawnPlayerCharacter() {
if (charSectionsDbc) {
LOG_INFO("CharSections.dbc loaded: ", charSectionsDbc->getRecordCount(), " records");
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
auto csF = pipeline::detectCharSectionsFields(charSectionsDbc.get(), csL);
bool foundSkin = false;
bool foundUnderwear = false;
bool foundFaceLower = false;
bool foundHair = false;
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"] : 4);
uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5);
uint32_t raceId = charSectionsDbc->getUInt32(r, csF.raceId);
uint32_t sexId = charSectionsDbc->getUInt32(r, csF.sexId);
uint32_t baseSection = charSectionsDbc->getUInt32(r, csF.baseSection);
uint32_t variationIndex = charSectionsDbc->getUInt32(r, csF.variationIndex);
uint32_t colorIndex = charSectionsDbc->getUInt32(r, csF.colorIndex);
if (raceId != targetRaceId || sexId != targetSexId) continue;
// Section 0 = skin: match by colorIndex = skin byte
const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 6;
if (baseSection == 0 && !foundSkin && colorIndex == charSkinId) {
std::string tex1 = charSectionsDbc->getString(r, csTex1);
std::string tex1 = charSectionsDbc->getString(r, csF.texture1);
if (!tex1.empty()) {
bodySkinPath = tex1;
foundSkin = true;
@ -3692,7 +3692,7 @@ void Application::spawnPlayerCharacter() {
// Section 3 = hair: match variation=hairStyle, color=hairColor
else if (baseSection == 3 && !foundHair &&
variationIndex == charHairStyleId && colorIndex == charHairColorId) {
hairTexturePath = charSectionsDbc->getString(r, csTex1);
hairTexturePath = charSectionsDbc->getString(r, csF.texture1);
if (!hairTexturePath.empty()) {
foundHair = true;
LOG_INFO(" DBC hair texture: ", hairTexturePath,
@ -3703,8 +3703,8 @@ void Application::spawnPlayerCharacter() {
// Texture1 = face lower, Texture2 = face upper
else if (baseSection == 1 && !foundFaceLower &&
variationIndex == charFaceId && colorIndex == charSkinId) {
std::string tex1 = charSectionsDbc->getString(r, csTex1);
std::string tex2 = charSectionsDbc->getString(r, csTex1 + 1);
std::string tex1 = charSectionsDbc->getString(r, csF.texture1);
std::string tex2 = charSectionsDbc->getString(r, csF.texture2);
if (!tex1.empty()) {
faceLowerTexturePath = tex1;
LOG_INFO(" DBC face lower: ", faceLowerTexturePath);
@ -3717,7 +3717,7 @@ void Application::spawnPlayerCharacter() {
}
// Section 4 = underwear
else if (baseSection == 4 && !foundUnderwear && colorIndex == charSkinId) {
for (uint32_t f = csTex1; f <= csTex1 + 2; f++) {
for (uint32_t f = csF.texture1; f <= csF.texture1 + 2; f++) {
std::string tex = charSectionsDbc->getString(r, f);
if (!tex.empty()) {
underwearPaths.push_back(tex);
@ -5353,22 +5353,17 @@ void Application::buildCharSectionsCache() {
if (!dbc) return;
const auto* csL = pipeline::getActiveDBCLayout()
? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
uint32_t raceF = csL ? (*csL)["RaceID"] : 1;
uint32_t sexF = csL ? (*csL)["SexID"] : 2;
uint32_t secF = csL ? (*csL)["BaseSection"] : 3;
uint32_t varF = csL ? (*csL)["VariationIndex"] : 8;
uint32_t colF = csL ? (*csL)["ColorIndex"] : 9;
uint32_t tex1F = csL ? (*csL)["Texture1"] : 4;
auto csF = pipeline::detectCharSectionsFields(dbc.get(), csL);
for (uint32_t r = 0; r < dbc->getRecordCount(); r++) {
uint32_t race = dbc->getUInt32(r, raceF);
uint32_t sex = dbc->getUInt32(r, sexF);
uint32_t section = dbc->getUInt32(r, secF);
uint32_t variation = dbc->getUInt32(r, varF);
uint32_t color = dbc->getUInt32(r, colF);
uint32_t race = dbc->getUInt32(r, csF.raceId);
uint32_t sex = dbc->getUInt32(r, csF.sexId);
uint32_t section = dbc->getUInt32(r, csF.baseSection);
uint32_t variation = dbc->getUInt32(r, csF.variationIndex);
uint32_t color = dbc->getUInt32(r, csF.colorIndex);
// We only cache sections 0 (skin), 1 (face), 3 (hair), 4 (underwear)
if (section != 0 && section != 1 && section != 3 && section != 4) continue;
for (int ti = 0; ti < 3; ti++) {
std::string tex = dbc->getString(r, tex1F + ti);
std::string tex = dbc->getString(r, csF.texture1 + ti);
if (tex.empty()) continue;
// Key: race(8)|sex(4)|section(4)|variation(8)|color(8)|texIndex(2) packed into 64 bits
uint64_t key = (static_cast<uint64_t>(race) << 26) |
@ -5653,6 +5648,8 @@ audio::VoiceType Application::detectVoiceTypeFromDisplayId(uint32_t displayId) c
case 6: raceName = "Tauren"; result = (sexId == 0) ? audio::VoiceType::TAUREN_MALE : audio::VoiceType::TAUREN_FEMALE; break;
case 7: raceName = "Gnome"; result = (sexId == 0) ? audio::VoiceType::GNOME_MALE : audio::VoiceType::GNOME_FEMALE; break;
case 8: raceName = "Troll"; result = (sexId == 0) ? audio::VoiceType::TROLL_MALE : audio::VoiceType::TROLL_FEMALE; break;
case 10: raceName = "BloodElf"; result = (sexId == 0) ? audio::VoiceType::BLOODELF_MALE : audio::VoiceType::BLOODELF_FEMALE; break;
case 11: raceName = "Draenei"; result = (sexId == 0) ? audio::VoiceType::DRAENEI_MALE : audio::VoiceType::DRAENEI_FEMALE; break;
default: result = audio::VoiceType::GENERIC; break;
}
@ -5952,6 +5949,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
if (csDbc) {
const auto* csL = pipeline::getActiveDBCLayout()
? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
auto csF = pipeline::detectCharSectionsFields(csDbc.get(), csL);
uint32_t npcRace = static_cast<uint32_t>(extraCopy.raceId);
uint32_t npcSex = static_cast<uint32_t>(extraCopy.sexId);
uint32_t npcSkin = static_cast<uint32_t>(extraCopy.skinId);
@ -5960,23 +5958,22 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
std::vector<std::string> npcUnderwear;
for (uint32_t r = 0; r < csDbc->getRecordCount(); r++) {
uint32_t rId = csDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
uint32_t sId = csDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
uint32_t rId = csDbc->getUInt32(r, csF.raceId);
uint32_t sId = csDbc->getUInt32(r, csF.sexId);
if (rId != npcRace || sId != npcSex) continue;
uint32_t section = csDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
uint32_t variation = csDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8);
uint32_t color = csDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9);
uint32_t tex1F = csL ? (*csL)["Texture1"] : 4;
uint32_t section = csDbc->getUInt32(r, csF.baseSection);
uint32_t variation = csDbc->getUInt32(r, csF.variationIndex);
uint32_t color = csDbc->getUInt32(r, csF.colorIndex);
if (section == 0 && def.basePath.empty() && color == npcSkin) {
def.basePath = csDbc->getString(r, tex1F);
def.basePath = csDbc->getString(r, csF.texture1);
} else if (section == 1 && npcFaceLower.empty() &&
variation == npcFace && color == npcSkin) {
npcFaceLower = csDbc->getString(r, tex1F);
npcFaceUpper = csDbc->getString(r, tex1F + 1);
npcFaceLower = csDbc->getString(r, csF.texture1);
npcFaceUpper = csDbc->getString(r, csF.texture2);
} else if (section == 4 && npcUnderwear.empty() && color == npcSkin) {
for (uint32_t f = tex1F; f <= tex1F + 2; f++) {
for (uint32_t f = csF.texture1; f <= csF.texture1 + 2; f++) {
std::string tex = csDbc->getString(r, f);
if (!tex.empty()) npcUnderwear.push_back(tex);
}
@ -6074,20 +6071,21 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
if (csDbc) {
const auto* csL = pipeline::getActiveDBCLayout()
? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
auto csF = pipeline::detectCharSectionsFields(csDbc.get(), csL);
uint32_t targetRace = static_cast<uint32_t>(extraCopy.raceId);
uint32_t targetSex = static_cast<uint32_t>(extraCopy.sexId);
for (uint32_t r = 0; r < csDbc->getRecordCount(); r++) {
uint32_t raceId = csDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
uint32_t sexId = csDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
uint32_t raceId = csDbc->getUInt32(r, csF.raceId);
uint32_t sexId = csDbc->getUInt32(r, csF.sexId);
if (raceId != targetRace || sexId != targetSex) continue;
uint32_t section = csDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
uint32_t section = csDbc->getUInt32(r, csF.baseSection);
if (section != 3) continue;
uint32_t variation = csDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8);
uint32_t colorIdx = csDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9);
uint32_t variation = csDbc->getUInt32(r, csF.variationIndex);
uint32_t colorIdx = csDbc->getUInt32(r, csF.colorIndex);
if (variation != static_cast<uint32_t>(extraCopy.hairStyleId)) continue;
if (colorIdx != static_cast<uint32_t>(extraCopy.hairColorId)) continue;
def.hairTexturePath = csDbc->getString(r, csL ? (*csL)["Texture1"] : 4);
def.hairTexturePath = csDbc->getString(r, csF.texture1);
break;
}
@ -7194,9 +7192,9 @@ void Application::spawnOnlinePlayer(uint64_t guid,
if (auto charSectionsDbc = assetManager->loadDBC("CharSections.dbc"); charSectionsDbc && charSectionsDbc->isLoaded()) {
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
auto csF = pipeline::detectCharSectionsFields(charSectionsDbc.get(), csL);
uint32_t targetRaceId = raceId;
uint32_t targetSexId = genderId;
const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 4;
bool foundSkin = false;
bool foundUnderwear = false;
@ -7204,31 +7202,31 @@ void Application::spawnOnlinePlayer(uint64_t guid,
bool foundFaceLower = false;
for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
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 rRace = charSectionsDbc->getUInt32(r, csF.raceId);
uint32_t rSex = charSectionsDbc->getUInt32(r, csF.sexId);
uint32_t baseSection = charSectionsDbc->getUInt32(r, csF.baseSection);
uint32_t variationIndex = charSectionsDbc->getUInt32(r, csF.variationIndex);
uint32_t colorIndex = charSectionsDbc->getUInt32(r, csF.colorIndex);
if (rRace != targetRaceId || rSex != targetSexId) continue;
if (baseSection == 0 && !foundSkin && colorIndex == skinId) {
std::string tex1 = charSectionsDbc->getString(r, csTex1);
std::string tex1 = charSectionsDbc->getString(r, csF.texture1);
if (!tex1.empty()) { bodySkinPath = tex1; foundSkin = true; }
} else if (baseSection == 3 && !foundHair &&
variationIndex == hairStyleId && colorIndex == hairColorId) {
hairTexturePath = charSectionsDbc->getString(r, csTex1);
hairTexturePath = charSectionsDbc->getString(r, csF.texture1);
if (!hairTexturePath.empty()) foundHair = true;
} else if (baseSection == 4 && !foundUnderwear && colorIndex == skinId) {
for (uint32_t f = csTex1; f <= csTex1 + 2; f++) {
for (uint32_t f = csF.texture1; f <= csF.texture1 + 2; f++) {
std::string tex = charSectionsDbc->getString(r, f);
if (!tex.empty()) underwearPaths.push_back(tex);
}
foundUnderwear = true;
} else if (baseSection == 1 && !foundFaceLower &&
variationIndex == faceId && colorIndex == skinId) {
std::string tex1 = charSectionsDbc->getString(r, csTex1);
std::string tex2 = charSectionsDbc->getString(r, csTex1 + 1);
std::string tex1 = charSectionsDbc->getString(r, csF.texture1);
std::string tex2 = charSectionsDbc->getString(r, csF.texture2);
if (!tex1.empty()) faceLowerPath = tex1;
if (!tex2.empty()) faceUpperPath = tex2;
foundFaceLower = true;
@ -8183,32 +8181,32 @@ void Application::processCreatureSpawnQueue(bool unlimited) {
if (csDbc) {
const auto* csL = pipeline::getActiveDBCLayout()
? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
auto csF = pipeline::detectCharSectionsFields(csDbc.get(), csL);
uint32_t nRace = static_cast<uint32_t>(he.raceId);
uint32_t nSex = static_cast<uint32_t>(he.sexId);
uint32_t nSkin = static_cast<uint32_t>(he.skinId);
uint32_t nFace = static_cast<uint32_t>(he.faceId);
for (uint32_t r = 0; r < csDbc->getRecordCount(); r++) {
uint32_t rId = csDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
uint32_t sId = csDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
uint32_t rId = csDbc->getUInt32(r, csF.raceId);
uint32_t sId = csDbc->getUInt32(r, csF.sexId);
if (rId != nRace || sId != nSex) continue;
uint32_t section = csDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
uint32_t variation = csDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8);
uint32_t color = csDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9);
uint32_t tex1F = csL ? (*csL)["Texture1"] : 4;
uint32_t section = csDbc->getUInt32(r, csF.baseSection);
uint32_t variation = csDbc->getUInt32(r, csF.variationIndex);
uint32_t color = csDbc->getUInt32(r, csF.colorIndex);
if (section == 0 && color == nSkin) {
std::string t = csDbc->getString(r, tex1F);
std::string t = csDbc->getString(r, csF.texture1);
if (!t.empty()) displaySkinPaths.push_back(t);
} else if (section == 1 && variation == nFace && color == nSkin) {
std::string t1 = csDbc->getString(r, tex1F);
std::string t2 = csDbc->getString(r, tex1F + 1);
std::string t1 = csDbc->getString(r, csF.texture1);
std::string t2 = csDbc->getString(r, csF.texture2);
if (!t1.empty()) displaySkinPaths.push_back(t1);
if (!t2.empty()) displaySkinPaths.push_back(t2);
} else if (section == 3 && variation == static_cast<uint32_t>(he.hairStyleId)
&& color == static_cast<uint32_t>(he.hairColorId)) {
std::string t = csDbc->getString(r, tex1F);
std::string t = csDbc->getString(r, csF.texture1);
if (!t.empty()) displaySkinPaths.push_back(t);
} else if (section == 4 && color == nSkin) {
for (uint32_t f = tex1F; f <= tex1F + 2; f++) {
for (uint32_t f = csF.texture1; f <= csF.texture1 + 2; f++) {
std::string t = csDbc->getString(r, f);
if (!t.empty()) displaySkinPaths.push_back(t);
}

View file

@ -1,7 +1,9 @@
#include "pipeline/dbc_layout.hpp"
#include "pipeline/dbc_loader.hpp"
#include "core/logger.hpp"
#include <fstream>
#include <sstream>
#include <algorithm>
namespace wowee {
namespace pipeline {
@ -94,5 +96,69 @@ const DBCFieldMap* DBCLayout::getLayout(const std::string& dbcName) const {
return (it != layouts_.end()) ? &it->second : nullptr;
}
CharSectionsFields detectCharSectionsFields(const DBCFile* dbc, const DBCFieldMap* csL) {
// Cache: avoid re-probing the same DBC on every call.
static const DBCFile* s_cachedDbc = nullptr;
static CharSectionsFields s_cachedResult;
if (dbc && dbc == s_cachedDbc) return s_cachedResult;
CharSectionsFields f;
if (!dbc || dbc->getRecordCount() == 0) return f;
// Start from the JSON layout (or defaults matching Classic-style: variation-first)
f.raceId = csL ? (*csL)["RaceID"] : 1;
f.sexId = csL ? (*csL)["SexID"] : 2;
f.baseSection = csL ? (*csL)["BaseSection"] : 3;
f.variationIndex = csL ? (*csL)["VariationIndex"] : 4;
f.colorIndex = csL ? (*csL)["ColorIndex"] : 5;
f.texture1 = csL ? (*csL)["Texture1"] : 6;
f.texture2 = csL ? (*csL)["Texture2"] : 7;
f.texture3 = csL ? (*csL)["Texture3"] : 8;
f.flags = csL ? (*csL)["Flags"] : 9;
// Auto-detect: probe the field that the JSON layout says is VariationIndex.
// In Classic-style layout, VariationIndex (field 4) holds small integers 0-15.
// In stock WotLK layout, field 4 is actually Texture1 (a string block offset, typically > 100).
// Sample up to 20 records and check if all field-4 values are small integers.
uint32_t probeField = f.variationIndex;
if (probeField >= dbc->getFieldCount()) {
s_cachedDbc = dbc;
s_cachedResult = f;
return f; // safety
}
uint32_t sampleCount = std::min(dbc->getRecordCount(), 20u);
uint32_t largeCount = 0;
uint32_t smallCount = 0;
for (uint32_t r = 0; r < sampleCount; r++) {
uint32_t val = dbc->getUInt32(r, probeField);
if (val > 50) {
++largeCount;
} else {
++smallCount;
}
}
// If most sampled values are large, the JSON layout's VariationIndex field
// actually contains string offsets => this is stock WotLK (texture-first).
// Swap to texture-first layout: Tex1=4, Tex2=5, Tex3=6, Flags=7, Var=8, Color=9.
if (largeCount > smallCount) {
uint32_t base = probeField; // the field index the JSON calls VariationIndex (typically 4)
f.texture1 = base;
f.texture2 = base + 1;
f.texture3 = base + 2;
f.flags = base + 3;
f.variationIndex = base + 4;
f.colorIndex = base + 5;
LOG_INFO("CharSections.dbc: detected stock WotLK layout (textures-first at field ", base, ")");
} else {
LOG_INFO("CharSections.dbc: detected Classic-style layout (variation-first at field ", probeField, ")");
}
s_cachedDbc = dbc;
s_cachedResult = f;
return f;
}
} // namespace pipeline
} // namespace wowee

View file

@ -332,25 +332,21 @@ bool CharacterPreview::loadCharacter(game::Race race, game::Gender gender,
bool foundUnderwear = false;
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
auto csF = pipeline::detectCharSectionsFields(charSectionsDbc.get(), csL);
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, 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);
uint32_t raceId = charSectionsDbc->getUInt32(r, csF.raceId);
uint32_t sexId = charSectionsDbc->getUInt32(r, csF.sexId);
uint32_t baseSection = charSectionsDbc->getUInt32(r, csF.baseSection);
uint32_t variationIndex = charSectionsDbc->getUInt32(r, csF.variationIndex);
uint32_t colorIndex = charSectionsDbc->getUInt32(r, csF.colorIndex);
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"] : 6);
std::string tex1 = charSectionsDbc->getString(r, csF.texture1);
if (!tex1.empty()) {
bodySkinPath_ = tex1;
foundSkin = true;
@ -360,8 +356,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"] : 6);
std::string tex2 = charSectionsDbc->getString(r, csL ? (*csL)["Texture2"] : 7);
std::string tex1 = charSectionsDbc->getString(r, csF.texture1);
std::string tex2 = charSectionsDbc->getString(r, csF.texture2);
if (!tex1.empty()) faceLowerPath = tex1;
if (!tex2.empty()) faceUpperPath = tex2;
foundFace = true;
@ -370,7 +366,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"] : 6);
std::string tex1 = charSectionsDbc->getString(r, csF.texture1);
if (!tex1.empty()) {
hairScalpPath = tex1;
foundHair = true;
@ -379,8 +375,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"] : 6;
for (uint32_t f = texBase; f <= texBase + 2; f++) {
for (uint32_t f = csF.texture1; f <= csF.texture1 + 2; f++) {
std::string tex = charSectionsDbc->getString(r, f);
if (!tex.empty()) {
underwearPaths.push_back(tex);

View file

@ -249,16 +249,17 @@ void CharacterCreateScreen::updateAppearanceRanges() {
uint32_t targetSexId = (genderIndex == 1) ? 1u : 0u;
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
auto csF = pipeline::detectCharSectionsFields(dbc.get(), csL);
int skinMax = -1;
int hairStyleMax = -1;
for (uint32_t r = 0; r < dbc->getRecordCount(); r++) {
uint32_t raceId = dbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
uint32_t sexId = dbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
uint32_t raceId = dbc->getUInt32(r, csF.raceId);
uint32_t sexId = dbc->getUInt32(r, csF.sexId);
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"] : 4);
uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5);
uint32_t baseSection = dbc->getUInt32(r, csF.baseSection);
uint32_t variationIndex = dbc->getUInt32(r, csF.variationIndex);
uint32_t colorIndex = dbc->getUInt32(r, csF.colorIndex);
if (baseSection == 0 && variationIndex == 0) {
skinMax = std::max(skinMax, static_cast<int>(colorIndex));
@ -279,13 +280,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, csL ? (*csL)["RaceID"] : 1);
uint32_t sexId = dbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
uint32_t raceId = dbc->getUInt32(r, csF.raceId);
uint32_t sexId = dbc->getUInt32(r, csF.sexId);
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"] : 4);
uint32_t colorIndex = dbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5);
uint32_t baseSection = dbc->getUInt32(r, csF.baseSection);
uint32_t variationIndex = dbc->getUInt32(r, csF.variationIndex);
uint32_t colorIndex = dbc->getUInt32(r, csF.colorIndex);
if (baseSection == 1 && colorIndex == static_cast<uint32_t>(skin)) {
faceMax = std::max(faceMax, static_cast<int>(variationIndex));