Refine NPC hair and facial geoset selection

- select humanoid scalp geoset by race/sex/style and filter overlapping scalp variants\n- constrain group 1 connectors to selected hair connector with safe fallback\n- add facial geoset handling for groups 100/200/300 from CharFacialHairStyles\n- allow selected facial 100 and fallback 100/101 when needed\n- map facial 200/300 directly from DBC indices (supports valid 200/300 variants)\n- keep existing NPC arm accessory suppression and clothing normalization
This commit is contained in:
Kelsi 2026-02-20 22:28:07 -08:00
parent 4e66f123eb
commit ce6887071c

View file

@ -4472,11 +4472,47 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
if (hasGroup3 || hasGroup4 || hasGroup8 || hasGroup12 || hasGroup13 || hasGroup15) {
bool hasRenderableCape = false;
bool hasEquippedTabard = false;
bool hasHumanoidExtra = false;
uint8_t extraRaceId = 0;
uint8_t extraSexId = 0;
uint16_t selectedHairScalp = 1;
uint16_t selectedFacial100 = 101;
uint16_t selectedFacial200 = 200;
uint16_t selectedFacial300 = 300;
bool wantsFacialHair = false;
std::unordered_set<uint16_t> hairScalpGeosetsForRaceSex;
if (itDisplayData != displayDataMap_.end() &&
itDisplayData->second.extraDisplayId != 0) {
auto itExtra = humanoidExtraMap_.find(itDisplayData->second.extraDisplayId);
if (itExtra != humanoidExtraMap_.end()) {
hasHumanoidExtra = true;
extraRaceId = itExtra->second.raceId;
extraSexId = itExtra->second.sexId;
hasEquippedTabard = (itExtra->second.equipDisplayId[9] != 0);
uint32_t hairKey = (static_cast<uint32_t>(extraRaceId) << 16) |
(static_cast<uint32_t>(extraSexId) << 8) |
static_cast<uint32_t>(itExtra->second.hairStyleId);
auto itHairGeo = hairGeosetMap_.find(hairKey);
if (itHairGeo != hairGeosetMap_.end() && itHairGeo->second > 0) {
selectedHairScalp = itHairGeo->second;
}
uint32_t facialKey = (static_cast<uint32_t>(extraRaceId) << 16) |
(static_cast<uint32_t>(extraSexId) << 8) |
static_cast<uint32_t>(itExtra->second.facialHairId);
wantsFacialHair = (itExtra->second.facialHairId != 0);
auto itFacial = facialHairGeosetMap_.find(facialKey);
if (itFacial != facialHairGeosetMap_.end()) {
selectedFacial100 = static_cast<uint16_t>(100 + itFacial->second.geoset100);
selectedFacial200 = static_cast<uint16_t>(200 + itFacial->second.geoset200);
selectedFacial300 = static_cast<uint16_t>(300 + itFacial->second.geoset300);
}
for (const auto& [k, v] : hairGeosetMap_) {
uint8_t race = static_cast<uint8_t>((k >> 16) & 0xFF);
uint8_t sex = static_cast<uint8_t>((k >> 8) & 0xFF);
if (race == extraRaceId && sex == extraSexId && v > 0 && v < 100) {
hairScalpGeosetsForRaceSex.insert(v);
}
}
auto itemDisplayDbc = assetManager->loadDBC("ItemDisplayInfo.dbc");
const auto* idiL = pipeline::getActiveDBCLayout()
? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
@ -4556,6 +4592,46 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
// Some humanoid models carry cloak cloth in group 16. Strip this too
// when no cape is equipped to avoid "everyone has a cape".
if (!hasRenderableCape && group == 16) continue;
// Group 0 can contain multiple scalp/hair meshes. Keep only the selected
// race/sex/style scalp to avoid overlapping broken hair.
if (hasHumanoidExtra && sid < 100 && hairScalpGeosetsForRaceSex.count(sid) > 0 && sid != selectedHairScalp) {
continue;
}
// Group 1 contains connector variants that mirror scalp style.
if (hasHumanoidExtra && group == 1) {
const uint16_t selectedConnector = static_cast<uint16_t>(100 + std::max<uint16_t>(selectedHairScalp, 1));
const bool allowSelectedFacial100 = wantsFacialHair &&
sid == selectedFacial100 && allGeosets.count(selectedFacial100) > 0;
const bool allowFacialFallback = wantsFacialHair && (sid == 100 || sid == 101);
if (allowSelectedFacial100) {
normalizedGeosets.insert(sid);
continue;
}
if (allowFacialFallback) {
normalizedGeosets.insert(sid);
continue;
}
if (sid != selectedConnector) {
// Keep fallback connector only when selected one does not exist on this model.
if (sid != 101 || allGeosets.count(selectedConnector) > 0) {
continue;
}
}
}
// Group 2 carries facial hair variants.
if (hasHumanoidExtra && group == 2) {
const bool allowSelectedFacial200 = wantsFacialHair &&
sid == selectedFacial200 && allGeosets.count(selectedFacial200) > 0;
const bool allowFacial200Fallback = wantsFacialHair && (sid == 200 || sid == 201);
if (!allowSelectedFacial200 && !allowFacial200Fallback) {
continue;
}
// If selected variant exists, do not keep fallback variants.
if (allowFacial200Fallback && allGeosets.count(selectedFacial200) > 0 &&
sid != selectedFacial200) {
continue;
}
}
normalizedGeosets.insert(sid);
}
@ -4582,6 +4658,13 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
if (tabardSid != 0) normalizedGeosets.insert(tabardSid);
}
// Some mustache/goatee variants are authored in facial group 3xx.
// Re-add only the selected facial 3xx geoset (not generic 3xx arm meshes).
if (hasHumanoidExtra && wantsFacialHair) {
uint16_t facial300Sid = pickFromGroup(selectedFacial300, 3);
if (facial300Sid != 0) normalizedGeosets.insert(facial300Sid);
}
// Prefer trousers geoset, not robe/kilt overlays.
if (hasGroup13) {
uint16_t pantsSid = pickFromGroup(1301, 13);