diff --git a/include/core/appearance_composer.hpp b/include/core/appearance_composer.hpp index 138fd800..fa722bfe 100644 --- a/include/core/appearance_composer.hpp +++ b/include/core/appearance_composer.hpp @@ -63,7 +63,8 @@ public: void compositePlayerSkin(uint32_t modelSlotId, const PlayerTextureInfo& texInfo); // Build default active geosets for player character - std::unordered_set buildDefaultPlayerGeosets(uint8_t hairStyleId, uint8_t facialId); + std::unordered_set buildDefaultPlayerGeosets(uint8_t raceId, uint8_t sexId, + uint8_t hairStyleId, uint8_t facialId); // Equipment weapon loading (reads inventory, attaches weapon M2 models) void loadEquippedWeapons(); diff --git a/src/core/appearance_composer.cpp b/src/core/appearance_composer.cpp index cedd2c43..9f14bd32 100644 --- a/src/core/appearance_composer.cpp +++ b/src/core/appearance_composer.cpp @@ -240,15 +240,63 @@ void AppearanceComposer::compositePlayerSkin(uint32_t modelSlotId, const PlayerT } } -std::unordered_set AppearanceComposer::buildDefaultPlayerGeosets(uint8_t hairStyleId, uint8_t facialId) { +std::unordered_set AppearanceComposer::buildDefaultPlayerGeosets(uint8_t raceId, uint8_t sexId, + uint8_t hairStyleId, uint8_t facialId) { std::unordered_set 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(100 + hairStyleId + 1)); - // Facial hair geoset: group 2 = 200 + variation + 1 - activeGeosets.insert(static_cast(200 + facialId + 1)); + // Look up the correct hair scalp geoset from CharHairGeosets.dbc + uint16_t selectedHairScalp = 1; // default + std::unordered_set allHairScalpGeosets; + if (entitySpawner_) { + const auto& hairMap = entitySpawner_->getHairGeosetMap(); + uint32_t hairKey = (static_cast(raceId) << 16) | + (static_cast(sexId) << 8) | + static_cast(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((k >> 16) & 0xFF) == raceId && + static_cast((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(100 + std::max(selectedHairScalp, 1))); + + // Facial hair geosets from CharacterFacialHairStyles.dbc + if (entitySpawner_) { + const auto& facialMap = entitySpawner_->getFacialHairGeosetMap(); + uint32_t facialKey = (static_cast(raceId) << 16) | + (static_cast(sexId) << 8) | + static_cast(facialId); + auto it = facialMap.find(facialKey); + if (it != facialMap.end()) { + activeGeosets.insert(static_cast(200 + std::max(it->second.geoset200, 1))); + activeGeosets.insert(static_cast(300 + std::max(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 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; } diff --git a/src/core/application.cpp b/src/core/application.cpp index 1a16e5ce..d4befd39 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -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((ch->appearanceBytes >> 16) & 0xFF); facialId = ch->facialFeatures; + raceId = static_cast(ch->race); + sexId = static_cast(ch->gender); } } auto activeGeosets = appearanceComposer_ - ? appearanceComposer_->buildDefaultPlayerGeosets(hairStyleId, facialId) + ? appearanceComposer_->buildDefaultPlayerGeosets(raceId, sexId, hairStyleId, facialId) : std::unordered_set{}; charRenderer->setActiveGeosets(instanceId, activeGeosets);