From 503f9ed650083cbb7708d683e3542f0da1033d52 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 23 Mar 2026 11:00:49 -0700 Subject: [PATCH] fix: auto-detect CharSections.dbc layout and add Blood Elf/Draenei NPC voices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- include/audio/npc_voice_manager.hpp | 4 + include/pipeline/dbc_layout.hpp | 35 ++++++++ src/audio/npc_voice_manager.cpp | 56 +++++++++++++ src/core/application.cpp | 120 ++++++++++++++-------------- src/pipeline/dbc_layout.cpp | 66 +++++++++++++++ src/rendering/character_preview.cpp | 27 +++---- src/ui/character_create_screen.cpp | 21 ++--- 7 files changed, 242 insertions(+), 87 deletions(-) diff --git a/include/audio/npc_voice_manager.hpp b/include/audio/npc_voice_manager.hpp index 92ab8f32..1bf722fd 100644 --- a/include/audio/npc_voice_manager.hpp +++ b/include/audio/npc_voice_manager.hpp @@ -38,6 +38,10 @@ enum class VoiceType { GNOME_FEMALE, GOBLIN_MALE, GOBLIN_FEMALE, + BLOODELF_MALE, + BLOODELF_FEMALE, + DRAENEI_MALE, + DRAENEI_FEMALE, GENERIC, // Fallback }; diff --git a/include/pipeline/dbc_layout.hpp b/include/pipeline/dbc_layout.hpp index 154aef08..0bbb2b29 100644 --- a/include/pipeline/dbc_layout.hpp +++ b/include/pipeline/dbc_layout.hpp @@ -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 diff --git a/src/audio/npc_voice_manager.cpp b/src/audio/npc_voice_manager.cpp index 1027d165..6f6c3b67 100644 --- a/src/audio/npc_voice_manager.cpp +++ b/src/audio/npc_voice_manager.cpp @@ -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>& 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) { diff --git a/src/core/application.cpp b/src/core/application.cpp index 4486427a..0599ba42 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -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(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(extraCopy.raceId); uint32_t npcSex = static_cast(extraCopy.sexId); uint32_t npcSkin = static_cast(extraCopy.skinId); @@ -5960,23 +5958,22 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x std::vector 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(extraCopy.raceId); uint32_t targetSex = static_cast(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(extraCopy.hairStyleId)) continue; if (colorIdx != static_cast(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(he.raceId); uint32_t nSex = static_cast(he.sexId); uint32_t nSkin = static_cast(he.skinId); uint32_t nFace = static_cast(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(he.hairStyleId) && color == static_cast(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); } diff --git a/src/pipeline/dbc_layout.cpp b/src/pipeline/dbc_layout.cpp index 08730536..7d3878fe 100644 --- a/src/pipeline/dbc_layout.cpp +++ b/src/pipeline/dbc_layout.cpp @@ -1,7 +1,9 @@ #include "pipeline/dbc_layout.hpp" +#include "pipeline/dbc_loader.hpp" #include "core/logger.hpp" #include #include +#include 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 diff --git a/src/rendering/character_preview.cpp b/src/rendering/character_preview.cpp index 306509ed..3c53fc62 100644 --- a/src/rendering/character_preview.cpp +++ b/src/rendering/character_preview.cpp @@ -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(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(face) && colorIndex == static_cast(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(hairStyle) && colorIndex == static_cast(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(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); diff --git a/src/ui/character_create_screen.cpp b/src/ui/character_create_screen.cpp index fa81756f..9238bf7b 100644 --- a/src/ui/character_create_screen.cpp +++ b/src/ui/character_create_screen.cpp @@ -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(colorIndex)); @@ -279,13 +280,13 @@ void CharacterCreateScreen::updateAppearanceRanges() { int faceMax = -1; std::vector 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(skin)) { faceMax = std::max(faceMax, static_cast(variationIndex));