Fix character geoset mapping and texture corruption on equipment change

Corrected CharGeosets group assignments verified via vertex bounding boxes:
- Group 4 (401+) = gloves/forearms, Group 5 (501+) = boots/shins,
  Group 8 (801+) = sleeves (chest-controlled), Group 9 = kneepads,
  Group 13 (1301+) = pants/trousers, Group 20 (2002) = bare feet
- Changed bare shin default from 501 to 502 for better width match
  with thigh mesh (0.39 vs 0.32, thighs are 0.42)
- Added clearCompositeCache() to prevent stale composite textures
  from being reused across equipment changes
- Fixed character preview geoset defaults to match corrected mapping
This commit is contained in:
Kelsi 2026-02-15 20:53:01 -08:00
parent 8a468e9533
commit ed6b305158
10 changed files with 424 additions and 169 deletions

View file

@ -1191,6 +1191,14 @@ void Application::setupUICallbacks() {
uint32_t appearanceBytes,
uint8_t facialFeatures,
float x, float y, float z, float orientation) {
// Skip local player — already spawned as the main character
uint64_t localGuid = gameHandler ? gameHandler->getPlayerGuid() : 0;
uint64_t activeGuid = gameHandler ? gameHandler->getActiveCharacterGuid() : 0;
if ((localGuid != 0 && guid == localGuid) ||
(activeGuid != 0 && guid == activeGuid) ||
(spawnedPlayerGuid_ != 0 && guid == spawnedPlayerGuid_)) {
return;
}
if (playerInstances_.count(guid)) return;
if (pendingPlayerSpawnGuids_.count(guid)) return;
pendingPlayerSpawns_.push_back({guid, raceId, genderId, appearanceBytes, facialFeatures, x, y, z, orientation});
@ -1947,8 +1955,12 @@ void Application::spawnPlayerCharacter() {
if (useCharSections) {
// Save skin composite state for re-compositing on equipment changes
// Include face textures so compositeWithRegions can rebuild the full base
bodySkinPath_ = bodySkinPath;
underwearPaths_ = underwearPaths;
underwearPaths_.clear();
if (!faceLowerTexturePath.empty()) underwearPaths_.push_back(faceLowerTexturePath);
if (!faceUpperTexturePath.empty()) underwearPaths_.push_back(faceUpperTexturePath);
for (const auto& up : underwearPaths) underwearPaths_.push_back(up);
// Composite body skin + face + underwear overlays
{
@ -2080,8 +2092,8 @@ void Application::spawnPlayerCharacter() {
// Default geosets for the active character (match CharacterPreview logic).
// Previous hardcoded values (notably always inserting 101) caused wrong hair meshes in-world.
std::unordered_set<uint16_t> activeGeosets;
// Body parts (group 0)
for (uint16_t i = 0; i <= 18; i++) activeGeosets.insert(i);
// Body parts (group 0: IDs 0-99, some models use up to 27)
for (uint16_t i = 0; i <= 99; i++) activeGeosets.insert(i);
uint8_t hairStyleId = 0;
uint8_t facialId = 0;
@ -2095,13 +2107,14 @@ void Application::spawnPlayerCharacter() {
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));
activeGeosets.insert(302); // Gloves: bare hands
activeGeosets.insert(401); // Boots: bare feet
activeGeosets.insert(501); // Chest: bare
activeGeosets.insert(401); // Bare forearms (no gloves) — group 4
activeGeosets.insert(502); // Bare shins (no boots) — group 5 (wider mesh matches thighs)
activeGeosets.insert(702); // Ears: default
activeGeosets.insert(802); // Wristbands: default
activeGeosets.insert(1301); // Trousers: bare legs
activeGeosets.insert(1502); // Back body (cloak=none)
activeGeosets.insert(801); // Bare wrists (no chest armor sleeves) — group 8
activeGeosets.insert(902); // Kneepads: default — group 9
activeGeosets.insert(1301); // Bare legs (no pants) — group 13
activeGeosets.insert(1502); // No cloak — group 15
activeGeosets.insert(2002); // Bare feet — group 20
// 1703 = DK eye glow mesh — skip for normal characters
// Normal eyes are part of the face texture on the body mesh
charRenderer->setActiveGeosets(instanceId, activeGeosets);
@ -3105,15 +3118,10 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
" hands=", extra.equipDisplayId[8], " tabard=", extra.equipDisplayId[9],
" cape=", extra.equipDisplayId[10]);
// Use baked texture for body skin only (types 1, 2)
// Type 6 (hair) needs its own texture from CharSections.dbc
if (!extra.bakeName.empty()) {
std::string bakePath = "Textures\\BakedNpcTextures\\" + extra.bakeName;
// Build equipment texture region layers from NPC equipment display IDs
// (texture-only compositing — no geoset changes to avoid invisibility bugs)
std::vector<std::pair<int, std::string>> npcRegionLayers;
auto npcItemDisplayDbc = assetManager->loadDBC("ItemDisplayInfo.dbc");
// Build equipment texture region layers from NPC equipment display IDs
// (texture-only compositing — no geoset changes to avoid invisibility bugs)
std::vector<std::pair<int, std::string>> npcRegionLayers;
auto npcItemDisplayDbc = assetManager->loadDBC("ItemDisplayInfo.dbc");
if (npcItemDisplayDbc) {
static const char* npcComponentDirs[] = {
"ArmUpperTexture", "ArmLowerTexture", "HandTexture",
@ -3162,6 +3170,11 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
}
}
// Use baked texture for body skin (types 1, 2)
// Type 6 (hair) needs its own texture from CharSections.dbc
if (!extra.bakeName.empty()) {
std::string bakePath = "Textures\\BakedNpcTextures\\" + extra.bakeName;
// Composite equipment textures over baked NPC texture, or just load baked texture
GLuint finalTex = 0;
if (!npcRegionLayers.empty()) {
@ -3188,6 +3201,79 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
}
} else {
LOG_DEBUG(" Humanoid extra has empty bakeName, trying CharSections fallback");
// Build skin texture from CharSections.dbc (same as player character)
auto csFallbackDbc = assetManager->loadDBC("CharSections.dbc");
if (csFallbackDbc) {
const auto* csFL = pipeline::getActiveDBCLayout()
? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
uint32_t npcRace = static_cast<uint32_t>(extra.raceId);
uint32_t npcSex = static_cast<uint32_t>(extra.sexId);
uint32_t npcSkin = static_cast<uint32_t>(extra.skinId);
uint32_t npcFace = static_cast<uint32_t>(extra.faceId);
std::string npcSkinPath, npcFaceLower, npcFaceUpper;
std::vector<std::string> npcUnderwear;
for (uint32_t r = 0; r < csFallbackDbc->getRecordCount(); r++) {
uint32_t rId = csFallbackDbc->getUInt32(r, csFL ? (*csFL)["RaceID"] : 1);
uint32_t sId = csFallbackDbc->getUInt32(r, csFL ? (*csFL)["SexID"] : 2);
if (rId != npcRace || sId != npcSex) continue;
uint32_t section = csFallbackDbc->getUInt32(r, csFL ? (*csFL)["BaseSection"] : 3);
uint32_t variation = csFallbackDbc->getUInt32(r, csFL ? (*csFL)["VariationIndex"] : 8);
uint32_t color = csFallbackDbc->getUInt32(r, csFL ? (*csFL)["ColorIndex"] : 9);
uint32_t tex1F = csFL ? (*csFL)["Texture1"] : 4;
// Section 0 = skin: match colorIndex = skinId
if (section == 0 && npcSkinPath.empty() && color == npcSkin) {
npcSkinPath = csFallbackDbc->getString(r, tex1F);
}
// Section 1 = face: match variation=faceId, color=skinId
else if (section == 1 && npcFaceLower.empty() &&
variation == npcFace && color == npcSkin) {
npcFaceLower = csFallbackDbc->getString(r, tex1F);
npcFaceUpper = csFallbackDbc->getString(r, tex1F + 1);
}
// Section 4 = underwear: match color=skinId
else if (section == 4 && npcUnderwear.empty() && color == npcSkin) {
for (uint32_t f = tex1F; f <= tex1F + 2; f++) {
std::string tex = csFallbackDbc->getString(r, f);
if (!tex.empty()) npcUnderwear.push_back(tex);
}
}
}
if (!npcSkinPath.empty()) {
// Composite skin + face + underwear
std::vector<std::string> skinLayers;
skinLayers.push_back(npcSkinPath);
if (!npcFaceLower.empty()) skinLayers.push_back(npcFaceLower);
if (!npcFaceUpper.empty()) skinLayers.push_back(npcFaceUpper);
for (const auto& uw : npcUnderwear) skinLayers.push_back(uw);
GLuint npcSkinTex = 0;
if (!npcRegionLayers.empty()) {
npcSkinTex = charRenderer->compositeWithRegions(npcSkinPath,
std::vector<std::string>(skinLayers.begin() + 1, skinLayers.end()),
npcRegionLayers);
} else if (skinLayers.size() > 1) {
npcSkinTex = charRenderer->compositeTextures(skinLayers);
} else {
npcSkinTex = charRenderer->loadTexture(npcSkinPath);
}
if (npcSkinTex != 0 && modelData) {
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
uint32_t texType = modelData->textures[ti].type;
if (texType == 1 || texType == 2 || texType == 11 || texType == 12 || texType == 13) {
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), npcSkinTex);
hasHumanoidTexture = true;
}
}
LOG_DEBUG("Applied CharSections skin to NPC: ", npcSkinPath);
}
}
}
}
// Load hair texture from CharSections.dbc (section 3)
@ -3329,12 +3415,13 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
}
// Default equipment geosets (bare/no armor)
uint16_t geosetGloves = 302; // Bare hands
uint16_t geosetBoots = 401; // Bare feet
uint16_t geosetChest = 501; // Bare chest
uint16_t geosetPants = 1301; // Bare legs
uint16_t geosetCape = 1502; // No cape
uint16_t geosetTabard = 1201; // No tabard
// CharGeosets: group 4=gloves(forearm), 5=boots(shin), 8=sleeves, 9=kneepads, 13=pants
uint16_t geosetGloves = 401; // Bare forearms (group 4)
uint16_t geosetBoots = 502; // Bare shins (group 5, wider mesh)
uint16_t geosetSleeves = 801; // Bare wrists (group 8, controlled by chest)
uint16_t geosetPants = 1301; // Bare legs (group 13)
uint16_t geosetCape = 1502; // No cape (group 15)
uint16_t geosetTabard = 1201; // No tabard (group 12)
// Load equipment geosets from ItemDisplayInfo.dbc
// DBC columns: 7=GeosetGroup[0], 8=GeosetGroup[1], 9=GeosetGroup[2]
@ -3345,21 +3432,19 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
const uint32_t fGG1 = idiL ? (*idiL)["GeosetGroup1"] : 7;
const uint32_t fGG3 = idiL ? (*idiL)["GeosetGroup3"] : 9;
// Helm (slot 0) - noted for helmet model attachment below
// Chest (slot 3) - geoset group 5xx
// Chest (slot 3) → group 8 (sleeves/wristbands)
if (extra.equipDisplayId[3] != 0) {
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[3]);
if (idx >= 0) {
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
if (gg > 0) geosetChest = static_cast<uint16_t>(501 + gg);
if (gg > 0) geosetSleeves = static_cast<uint16_t>(801 + gg);
// Robes: GeosetGroup[2] > 0 shows kilt legs
uint32_t gg3 = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG3);
if (gg3 > 0) geosetPants = static_cast<uint16_t>(1301 + gg3);
}
}
// Legs (slot 5) - geoset group 13xx
// Legs (slot 5) → group 13 (trousers)
if (extra.equipDisplayId[5] != 0) {
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[5]);
if (idx >= 0) {
@ -3368,37 +3453,37 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
}
}
// Feet (slot 6) - geoset group 4xx
// Feet (slot 6) → group 5 (boots/shins)
if (extra.equipDisplayId[6] != 0) {
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[6]);
if (idx >= 0) {
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
if (gg > 0) geosetBoots = static_cast<uint16_t>(401 + gg);
if (gg > 0) geosetBoots = static_cast<uint16_t>(501 + gg);
}
}
// Hands (slot 8) - geoset group 3xx
// Hands (slot 8) → group 4 (gloves/forearms)
if (extra.equipDisplayId[8] != 0) {
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[8]);
if (idx >= 0) {
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
if (gg > 0) geosetGloves = static_cast<uint16_t>(301 + gg);
if (gg > 0) geosetGloves = static_cast<uint16_t>(401 + gg);
}
}
// Tabard (slot 9) - geoset group 12xx
// Tabard (slot 9) → group 12
if (extra.equipDisplayId[9] != 0) {
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[9]);
if (idx >= 0) {
geosetTabard = 1202; // Show tabard mesh
geosetTabard = 1202;
}
}
// Cape (slot 10) - geoset group 15xx
// Cape (slot 10) → group 15
if (extra.equipDisplayId[10] != 0) {
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[10]);
if (idx >= 0) {
geosetCape = 1502; // Show cloak mesh
geosetCape = 1502;
}
}
}
@ -3406,12 +3491,13 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
// Apply equipment geosets
activeGeosets.insert(geosetGloves);
activeGeosets.insert(geosetBoots);
activeGeosets.insert(geosetChest);
activeGeosets.insert(geosetSleeves);
activeGeosets.insert(geosetPants);
activeGeosets.insert(geosetCape);
activeGeosets.insert(geosetTabard);
activeGeosets.insert(702); // Ears: default
activeGeosets.insert(802); // Wristbands: default
activeGeosets.insert(902); // Kneepads: default
activeGeosets.insert(2002); // Bare feet mesh
// Hide hair under helmets: replace style-specific scalp with bald scalp
if (extra.equipDisplayId[0] != 0 && hairGeoset > 1) {
@ -3440,7 +3526,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
LOG_INFO("NPC geosets for instance ", instanceId, ": [", geosetList, "]");
charRenderer->setActiveGeosets(instanceId, activeGeosets);
LOG_DEBUG("Set humanoid geosets: hair=", (int)hairGeoset,
" chest=", geosetChest, " pants=", geosetPants,
" sleeves=", geosetSleeves, " pants=", geosetPants,
" boots=", geosetBoots, " gloves=", geosetGloves);
// Load and attach helmet model if equipped
@ -3617,6 +3703,16 @@ void Application::spawnOnlinePlayer(uint64_t guid,
if (!renderer || !renderer->getCharacterRenderer() || !assetManager || !assetManager->isInitialized()) return;
if (playerInstances_.count(guid)) return;
// Skip local player — already spawned as the main character
if (gameHandler) {
uint64_t localGuid = gameHandler->getPlayerGuid();
uint64_t activeGuid = gameHandler->getActiveCharacterGuid();
if ((localGuid != 0 && guid == localGuid) ||
(activeGuid != 0 && guid == activeGuid) ||
(spawnedPlayerGuid_ != 0 && guid == spawnedPlayerGuid_)) {
return;
}
}
auto* charRenderer = renderer->getCharacterRenderer();
// Base geometry model: cache by (race, gender)
@ -3828,16 +3924,18 @@ void Application::spawnOnlinePlayer(uint64_t guid,
// Geosets: body + hair/facial hair selections
std::unordered_set<uint16_t> activeGeosets;
for (uint16_t i = 0; i <= 18; i++) activeGeosets.insert(i);
// Body parts (group 0: IDs 0-99, some models use up to 27)
for (uint16_t i = 0; i <= 99; i++) activeGeosets.insert(i);
activeGeosets.insert(static_cast<uint16_t>(100 + hairStyleId + 1));
activeGeosets.insert(static_cast<uint16_t>(200 + facialFeatures + 1));
activeGeosets.insert(302);
activeGeosets.insert(401);
activeGeosets.insert(501);
activeGeosets.insert(702);
activeGeosets.insert(802);
activeGeosets.insert(1301);
activeGeosets.insert(1502);
activeGeosets.insert(401); // Bare forearms (no gloves) — group 4
activeGeosets.insert(502); // Bare shins (no boots) — group 5 (wider mesh)
activeGeosets.insert(702); // Ears
activeGeosets.insert(801); // Bare wrists (no sleeves) — group 8
activeGeosets.insert(902); // Kneepads — group 9
activeGeosets.insert(1301); // Bare legs — group 13
activeGeosets.insert(1502); // No cloak — group 15
activeGeosets.insert(2002); // Bare feet — group 20
charRenderer->setActiveGeosets(instanceId, activeGeosets);
charRenderer->playAnimation(instanceId, 0, true);
@ -3851,7 +3949,10 @@ void Application::spawnOnlinePlayer(uint64_t guid,
st.appearanceBytes = appearanceBytes;
st.facialFeatures = facialFeatures;
st.bodySkinPath = bodySkinPath;
st.underwearPaths = underwearPaths;
// Include face textures so compositeWithRegions can rebuild the full base
if (!faceLowerPath.empty()) st.underwearPaths.push_back(faceLowerPath);
if (!faceUpperPath.empty()) st.underwearPaths.push_back(faceUpperPath);
for (const auto& up : underwearPaths) st.underwearPaths.push_back(up);
onlinePlayerAppearance_[guid] = std::move(st);
}
@ -3860,6 +3961,13 @@ void Application::setOnlinePlayerEquipment(uint64_t guid,
const std::array<uint8_t, 19>& inventoryTypes) {
if (!renderer || !renderer->getCharacterRenderer() || !assetManager || !assetManager->isInitialized()) return;
// Skip local player — equipment handled by GameScreen::updateCharacterGeosets/Textures
// via consumeOnlineEquipmentDirty(), which fires on the same server update.
if (gameHandler) {
uint64_t localGuid = gameHandler->getPlayerGuid();
if (localGuid != 0 && guid == localGuid) return;
}
// If the player isn't spawned yet, store equipment until spawn.
if (!playerInstances_.count(guid) || !onlinePlayerAppearance_.count(guid)) {
pendingOnlinePlayerEquipment_[guid] = {displayInfoIds, inventoryTypes};
@ -3911,12 +4019,17 @@ void Application::setOnlinePlayerEquipment(uint64_t guid,
// --- Geosets ---
std::unordered_set<uint16_t> geosets;
for (uint16_t i = 0; i <= 18; i++) geosets.insert(i);
// Body parts (group 0: IDs 0-99, some models use up to 27)
for (uint16_t i = 0; i <= 99; i++) geosets.insert(i);
uint8_t hairStyleId = static_cast<uint8_t>((st.appearanceBytes >> 16) & 0xFF);
geosets.insert(static_cast<uint16_t>(100 + hairStyleId + 1));
geosets.insert(static_cast<uint16_t>(200 + st.facialFeatures + 1));
geosets.insert(701);
geosets.insert(401); // Body joint patches (knees)
geosets.insert(402); // Body joint patches (elbows)
geosets.insert(701); // Ears
geosets.insert(902); // Kneepads
geosets.insert(2002); // Bare feet mesh
const uint32_t geosetGroup1Field = idiL ? (*idiL)["GeosetGroup1"] : 7;
const uint32_t geosetGroup3Field = idiL ? (*idiL)["GeosetGroup3"] : 9;
@ -3940,11 +4053,11 @@ void Application::setOnlinePlayerEquipment(uint64_t guid,
}
}
// Feet (invType 8)
// Feet (invType 8): 401/402 are body patches (always on), 403+ are boot meshes
{
uint32_t did = findDisplayIdByInvType({8});
uint32_t gg1 = getGeosetGroup(did, geosetGroup1Field);
geosets.insert(static_cast<uint16_t>(gg1 > 0 ? 401 + gg1 : 401));
if (gg1 > 0) geosets.insert(static_cast<uint16_t>(402 + gg1));
}
// Hands (invType 10)