fix(rendering): filter player hair geosets via CharHairGeosets.dbc

buildDefaultPlayerGeosets() was inserting all submeshIds 0-99 into
activeGeosets, showing every hair variation simultaneously. Now uses
the hairGeosetMap_ (from CharHairGeosets.dbc) to select only the
correct hair scalp geoset for the player's race/sex/style, matching
the existing NPC geoset filtering logic in EntitySpawner.
This commit is contained in:
Kelsi 2026-04-03 22:43:37 -07:00
parent 5468a93f2e
commit f79395788a
3 changed files with 62 additions and 11 deletions

View file

@ -240,15 +240,63 @@ void AppearanceComposer::compositePlayerSkin(uint32_t modelSlotId, const PlayerT
}
}
std::unordered_set<uint16_t> AppearanceComposer::buildDefaultPlayerGeosets(uint8_t hairStyleId, uint8_t facialId) {
std::unordered_set<uint16_t> AppearanceComposer::buildDefaultPlayerGeosets(uint8_t raceId, uint8_t sexId,
uint8_t hairStyleId, uint8_t facialId) {
std::unordered_set<uint16_t> activeGeosets;
// Body parts (group 0: IDs 0-99, some models use up to 27)
for (uint16_t i = 0; i <= 99; i++) activeGeosets.insert(i);
// Hair style geoset: group 1 = 100 + variation + 1
activeGeosets.insert(static_cast<uint16_t>(100 + hairStyleId + 1));
// Facial hair geoset: group 2 = 200 + variation + 1
activeGeosets.insert(static_cast<uint16_t>(200 + facialId + 1));
// Look up the correct hair scalp geoset from CharHairGeosets.dbc
uint16_t selectedHairScalp = 1; // default
std::unordered_set<uint16_t> allHairScalpGeosets;
if (entitySpawner_) {
const auto& hairMap = entitySpawner_->getHairGeosetMap();
uint32_t hairKey = (static_cast<uint32_t>(raceId) << 16) |
(static_cast<uint32_t>(sexId) << 8) |
static_cast<uint32_t>(hairStyleId);
auto it = hairMap.find(hairKey);
if (it != hairMap.end() && it->second > 0)
selectedHairScalp = it->second;
// Collect all hair scalp geosets for this race/sex to exclude the others
for (const auto& [k, v] : hairMap) {
if (static_cast<uint8_t>((k >> 16) & 0xFF) == raceId &&
static_cast<uint8_t>((k >> 8) & 0xFF) == sexId &&
v > 0 && v < 100)
allHairScalpGeosets.insert(v);
}
}
// Group 0: add body base submeshes (0) but exclude other hair scalp variants
activeGeosets.insert(0); // body base
activeGeosets.insert(selectedHairScalp);
// Some models have additional non-hair body submeshes (e.g. earrings, jaw);
// these are typically < 100 and NOT in the hair geoset set
for (uint16_t i = 1; i < 100; i++) {
if (allHairScalpGeosets.count(i) == 0)
activeGeosets.insert(i); // not a hair geoset, safe to include
}
// Hair connector: group 1 = 100 + geoset
activeGeosets.insert(static_cast<uint16_t>(100 + std::max<uint16_t>(selectedHairScalp, 1)));
// Facial hair geosets from CharacterFacialHairStyles.dbc
if (entitySpawner_) {
const auto& facialMap = entitySpawner_->getFacialHairGeosetMap();
uint32_t facialKey = (static_cast<uint32_t>(raceId) << 16) |
(static_cast<uint32_t>(sexId) << 8) |
static_cast<uint32_t>(facialId);
auto it = facialMap.find(facialKey);
if (it != facialMap.end()) {
activeGeosets.insert(static_cast<uint16_t>(200 + std::max<uint16_t>(it->second.geoset200, 1)));
activeGeosets.insert(static_cast<uint16_t>(300 + std::max<uint16_t>(it->second.geoset300, 1)));
} else {
activeGeosets.insert(201);
activeGeosets.insert(301);
}
} else {
activeGeosets.insert(201);
activeGeosets.insert(301);
}
activeGeosets.insert(kGeosetBareForearms);
activeGeosets.insert(kGeosetBareShins);
activeGeosets.insert(kGeosetDefaultEars);
@ -257,8 +305,6 @@ std::unordered_set<uint16_t> AppearanceComposer::buildDefaultPlayerGeosets(uint8
activeGeosets.insert(kGeosetBarePants);
activeGeosets.insert(kGeosetWithCape);
activeGeosets.insert(kGeosetBareFeet);
// 1703 = DK eye glow mesh — skip for normal characters
// Normal eyes are part of the face texture on the body mesh
return activeGeosets;
}

View file

@ -3707,14 +3707,18 @@ void Application::spawnPlayerCharacter() {
// Build default geosets for the active character via AppearanceComposer
uint8_t hairStyleId = 0;
uint8_t facialId = 0;
uint8_t raceId = 0;
uint8_t sexId = 0;
if (gameHandler) {
if (const game::Character* ch = gameHandler->getActiveCharacter()) {
hairStyleId = static_cast<uint8_t>((ch->appearanceBytes >> 16) & 0xFF);
facialId = ch->facialFeatures;
raceId = static_cast<uint8_t>(ch->race);
sexId = static_cast<uint8_t>(ch->gender);
}
}
auto activeGeosets = appearanceComposer_
? appearanceComposer_->buildDefaultPlayerGeosets(hairStyleId, facialId)
? appearanceComposer_->buildDefaultPlayerGeosets(raceId, sexId, hairStyleId, facialId)
: std::unordered_set<uint16_t>{};
charRenderer->setActiveGeosets(instanceId, activeGeosets);