From d4bea91e375f6a7afee867f7deadc2c81871db01 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Feb 2026 15:43:09 -0800 Subject: [PATCH] Fix naked players and NPC gear textures Default PLAYER_VISIBLE_ITEM layout to known WotLK 3.3.5a values (base=284, stride=2) so equipment reads work immediately without waiting for heuristic detection. Add equipment texture compositing for humanoid NPCs over baked body textures using ItemDisplayInfo.dbc region lookups (texture-only, no geoset changes to avoid invisibility). --- include/game/game_handler.hpp | 5 +++- src/core/application.cpp | 52 ++++++++++++++++++++++++++++++++++- src/game/game_handler.cpp | 29 +++++++++---------- 3 files changed, 70 insertions(+), 16 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 3b19e5b6..9d18763d 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1132,8 +1132,11 @@ private: // Visible equipment for other players: detect the update-field layout (base + stride) // using the local player's own equipped items, then decode other players by index. - int visibleItemEntryBase_ = -1; + // Default to known WotLK 3.3.5a layout: UNIT_END(148) + 0x0088 = 284, stride 2. + // The heuristic in maybeDetectVisibleItemLayout() can still override if needed. + int visibleItemEntryBase_ = 284; int visibleItemStride_ = 2; + bool visibleItemLayoutVerified_ = false; // true once heuristic confirms/overrides default std::unordered_map> otherPlayerVisibleItemEntries_; std::unordered_set otherPlayerVisibleDirty_; std::unordered_map otherPlayerMoveTimeMs_; diff --git a/src/core/application.cpp b/src/core/application.cpp index 7d34f473..058091d3 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -3116,7 +3116,57 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x // Type 6 (hair) needs its own texture from CharSections.dbc if (!extra.bakeName.empty()) { std::string bakePath = "Textures\\BakedNpcTextures\\" + extra.bakeName; - GLuint finalTex = charRenderer->loadTexture(bakePath); + + // Build equipment texture region layers from NPC equipment display IDs + // (texture-only compositing — no geoset changes to avoid invisibility bugs) + std::vector> npcRegionLayers; + auto npcItemDisplayDbc = assetManager->loadDBC("ItemDisplayInfo.dbc"); + if (npcItemDisplayDbc) { + static const char* npcComponentDirs[] = { + "ArmUpperTexture", "ArmLowerTexture", "HandTexture", + "TorsoUpperTexture", "TorsoLowerTexture", + "LegUpperTexture", "LegLowerTexture", "FootTexture", + }; + const bool npcIsFemale = (extra.sexId == 1); + + // Iterate all 11 NPC equipment slots; let DBC lookup filter which have textures + for (int eqSlot = 0; eqSlot < 11; eqSlot++) { + uint32_t did = extra.equipDisplayId[eqSlot]; + if (did == 0) continue; + int32_t recIdx = npcItemDisplayDbc->findRecordById(did); + if (recIdx < 0) continue; + + for (int region = 0; region < 8; region++) { + std::string texName = npcItemDisplayDbc->getString( + static_cast(recIdx), 14 + region); + if (texName.empty()) + texName = npcItemDisplayDbc->getString( + static_cast(recIdx), 15 + region); + if (texName.empty()) continue; + + std::string base = "Item\\TextureComponents\\" + + std::string(npcComponentDirs[region]) + "\\" + texName; + std::string genderPath = base + (npcIsFemale ? "_F.blp" : "_M.blp"); + std::string unisexPath = base + "_U.blp"; + std::string fullPath; + if (assetManager->fileExists(genderPath)) fullPath = genderPath; + else if (assetManager->fileExists(unisexPath)) fullPath = unisexPath; + else fullPath = base + ".blp"; + + npcRegionLayers.emplace_back(region, fullPath); + } + } + } + + // Composite equipment textures over baked NPC texture, or just load baked texture + GLuint finalTex = 0; + if (!npcRegionLayers.empty()) { + finalTex = charRenderer->compositeWithRegions(bakePath, {}, npcRegionLayers); + LOG_DEBUG("Composited NPC baked texture with ", npcRegionLayers.size(), + " equipment regions: ", bakePath); + } else { + finalTex = charRenderer->loadTexture(bakePath); + } if (finalTex != 0 && modelData) { for (size_t ti = 0; ti < modelData->textures.size(); ti++) { diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index be03b1b5..512bc0b6 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -5681,7 +5681,7 @@ void GameHandler::rebuildOnlineInventory() { } void GameHandler::maybeDetectVisibleItemLayout() { - if (visibleItemEntryBase_ >= 0) return; + if (visibleItemLayoutVerified_) return; if (lastPlayerFields_.empty()) return; std::array equipEntries{}; @@ -5746,21 +5746,22 @@ void GameHandler::maybeDetectVisibleItemLayout() { } } - if (bestMatches < 2 || bestBase < 0 || bestStride <= 0) return; - if (bestMismatches > 1) return; + if (bestMatches >= 2 && bestBase >= 0 && bestStride > 0 && bestMismatches <= 1) { + visibleItemEntryBase_ = bestBase; + visibleItemStride_ = bestStride; + visibleItemLayoutVerified_ = true; + LOG_INFO("Detected PLAYER_VISIBLE_ITEM entry layout: base=", visibleItemEntryBase_, + " stride=", visibleItemStride_, " (matches=", bestMatches, + " mismatches=", bestMismatches, " score=", bestScore, ")"); - visibleItemEntryBase_ = bestBase; - visibleItemStride_ = bestStride; - LOG_INFO("Detected PLAYER_VISIBLE_ITEM entry layout: base=", visibleItemEntryBase_, - " stride=", visibleItemStride_, " (matches=", bestMatches, - " mismatches=", bestMismatches, " score=", bestScore, ")"); - - // Backfill existing player entities already in view. - for (const auto& [guid, ent] : entityManager.getEntities()) { - if (!ent || ent->getType() != ObjectType::PLAYER) continue; - if (guid == playerGuid) continue; - updateOtherPlayerVisibleItems(guid, ent->getFields()); + // Backfill existing player entities already in view. + for (const auto& [guid, ent] : entityManager.getEntities()) { + if (!ent || ent->getType() != ObjectType::PLAYER) continue; + if (guid == playerGuid) continue; + updateOtherPlayerVisibleItems(guid, ent->getFields()); + } } + // If heuristic didn't find a match, keep using the default WotLK layout (base=284, stride=2). } void GameHandler::updateOtherPlayerVisibleItems(uint64_t guid, const std::map& fields) {