From 0396a42bebdc83fb31f9022ed77522e53762b4e3 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 27 Mar 2026 17:30:35 -0700 Subject: [PATCH] feat: render equipment on other players (helmets, weapons, belts, wrists) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Other players previously appeared partially naked — only chest, legs, feet, hands, cape, and tabard rendered. Now renders full equipment: - Helmet M2 model: loads from ItemDisplayInfo.dbc with race/gender suffix, attaches at head bone (point 0/11), hides hair geoset under helm - Weapons: mainhand (attachment 1) and offhand (attachment 2) M2 models loaded from ItemDisplayInfo, with Weapon/Shield path fallback - Wrist/bracer geoset (group 8): applies when no chest sleeve overrides - Belt/waist geoset (group 18): reads GeosetGroup1 from ItemDisplayInfo - Shoulder M2 attachments deferred (separate bone attachment system) Also applied same wrist/waist geosets to NPC and character preview paths. Minimap: batch 9 individual vkUpdateDescriptorSets into single call. --- src/core/application.cpp | 193 ++++++++++++++++++++++++++++ src/rendering/character_preview.cpp | 16 +++ src/rendering/minimap.cpp | 24 ++-- 3 files changed, 223 insertions(+), 10 deletions(-) diff --git a/src/core/application.cpp b/src/core/application.cpp index 00571d5e..4421d0a3 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -6483,6 +6483,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x uint16_t geosetPants = pickGeoset(1301, 13); // Bare legs (group 13) uint16_t geosetCape = 0; // Group 15 disabled unless cape is equipped uint16_t geosetTabard = pickGeoset(1201, 12); // Group 12 (tabard), default variant 1201 + uint16_t geosetBelt = 0; // Group 18 disabled unless belt is equipped rendering::VkTexture* npcCapeTextureId = nullptr; // Load equipment geosets from ItemDisplayInfo.dbc @@ -6530,6 +6531,19 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x if (gg > 0) geosetGloves = pickGeoset(static_cast(401 + gg), 4); } + // Wrists (slot 7) → group 8 (sleeves, only if chest didn't set it) + { + uint32_t gg = readGeosetGroup(7, "wrist"); + if (gg > 0 && geosetSleeves == pickGeoset(801, 8)) + geosetSleeves = pickGeoset(static_cast(801 + gg), 8); + } + + // Belt (slot 4) → group 18 (buckle) + { + uint32_t gg = readGeosetGroup(4, "belt"); + if (gg > 0) geosetBelt = static_cast(1801 + gg); + } + // Tabard (slot 9) → group 12 (tabard/robe mesh) { uint32_t gg = readGeosetGroup(9, "tabard"); @@ -6612,6 +6626,9 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x if (geosetTabard != 0) { activeGeosets.insert(geosetTabard); } + if (geosetBelt != 0) { + activeGeosets.insert(geosetBelt); + } activeGeosets.insert(pickGeoset(702, 7)); // Ears: default activeGeosets.insert(pickGeoset(902, 9)); // Kneepads: default activeGeosets.insert(pickGeoset(2002, 20)); // Bare feet mesh @@ -7436,17 +7453,116 @@ void Application::setOnlinePlayerEquipment(uint64_t guid, if (gg1 > 0) geosetGloves = static_cast(401 + gg1); } + // Wrists/Bracers (invType 9) → sleeve group 8 (only if chest/shirt didn't set it) + { + uint32_t did = findDisplayIdByInvType({9}); + if (did != 0 && geosetSleeves == 801) { + uint32_t gg1 = getGeosetGroup(did, geosetGroup1Field); + if (gg1 > 0) geosetSleeves = static_cast(801 + gg1); + } + } + + // Waist/Belt (invType 6) → buckle group 18 + uint16_t geosetBelt = 0; + { + uint32_t did = findDisplayIdByInvType({6}); + uint32_t gg1 = getGeosetGroup(did, geosetGroup1Field); + if (gg1 > 0) geosetBelt = static_cast(1801 + gg1); + } + geosets.insert(geosetGloves); geosets.insert(geosetBoots); geosets.insert(geosetSleeves); geosets.insert(geosetPants); + if (geosetBelt != 0) geosets.insert(geosetBelt); // Back/Cloak (invType 16) geosets.insert(hasInvType({16}) ? 1502 : 1501); // Tabard (invType 19) if (hasInvType({19})) geosets.insert(1201); + // Hide hair under helmets: replace style-specific scalp with bald scalp + // HEAD slot is index 0 in the 19-element equipment array + if (displayInfoIds[0] != 0 && hairStyleId > 0) { + uint16_t hairGeoset = static_cast(hairStyleId + 1); + geosets.erase(static_cast(100 + hairGeoset)); // Remove style group 1 + geosets.insert(101); // Default group 1 connector + } + charRenderer->setActiveGeosets(st.instanceId, geosets); + // --- Helmet model attachment --- + // HEAD slot is index 0 in the 19-element equipment array. + // Helmet M2s are race/gender-specific (e.g. Helm_Plate_B_01_HuM.m2 for Human Male). + if (displayInfoIds[0] != 0) { + // Detach any previously attached helmet before attaching a new one + charRenderer->detachWeapon(st.instanceId, 0); + charRenderer->detachWeapon(st.instanceId, 11); + + int32_t helmIdx = displayInfoDbc->findRecordById(displayInfoIds[0]); + if (helmIdx >= 0) { + const uint32_t leftModelField = idiL ? (*idiL)["LeftModel"] : 1u; + std::string helmModelName = displayInfoDbc->getString(static_cast(helmIdx), leftModelField); + if (!helmModelName.empty()) { + // Strip .mdx/.m2 extension + size_t dotPos = helmModelName.rfind('.'); + if (dotPos != std::string::npos) helmModelName = helmModelName.substr(0, dotPos); + + // Race/gender suffix for helmet variants + static const std::unordered_map racePrefix = { + {1, "Hu"}, {2, "Or"}, {3, "Dw"}, {4, "Ni"}, {5, "Sc"}, + {6, "Ta"}, {7, "Gn"}, {8, "Tr"}, {10, "Be"}, {11, "Dr"} + }; + std::string genderSuffix = (st.genderId == 0) ? "M" : "F"; + std::string raceSuffix; + auto itRace = racePrefix.find(st.raceId); + if (itRace != racePrefix.end()) { + raceSuffix = "_" + itRace->second + genderSuffix; + } + + // Try race/gender-specific variant first, then base name + std::string helmPath; + pipeline::M2Model helmModel; + if (!raceSuffix.empty()) { + helmPath = "Item\\ObjectComponents\\Head\\" + helmModelName + raceSuffix + ".m2"; + if (!loadWeaponM2(helmPath, helmModel)) helmModel = {}; + } + if (!helmModel.isValid()) { + helmPath = "Item\\ObjectComponents\\Head\\" + helmModelName + ".m2"; + loadWeaponM2(helmPath, helmModel); + } + + if (helmModel.isValid()) { + uint32_t helmModelId = nextWeaponModelId_++; + // Get texture from ItemDisplayInfo (LeftModelTexture) + const uint32_t leftTexField = idiL ? (*idiL)["LeftModelTexture"] : 3u; + std::string helmTexName = displayInfoDbc->getString(static_cast(helmIdx), leftTexField); + std::string helmTexPath; + if (!helmTexName.empty()) { + if (!raceSuffix.empty()) { + std::string suffixedTex = "Item\\ObjectComponents\\Head\\" + helmTexName + raceSuffix + ".blp"; + if (assetManager->fileExists(suffixedTex)) helmTexPath = suffixedTex; + } + if (helmTexPath.empty()) { + helmTexPath = "Item\\ObjectComponents\\Head\\" + helmTexName + ".blp"; + } + } + // Attachment point 0 (head bone), fallback to 11 (explicit head attachment) + bool attached = charRenderer->attachWeapon(st.instanceId, 0, helmModel, helmModelId, helmTexPath); + if (!attached) { + attached = charRenderer->attachWeapon(st.instanceId, 11, helmModel, helmModelId, helmTexPath); + } + if (attached) { + LOG_DEBUG("Attached player helmet: ", helmPath, " tex: ", helmTexPath); + } + } + } + } + } else { + // No helmet equipped — detach any existing helmet model + charRenderer->detachWeapon(st.instanceId, 0); + charRenderer->detachWeapon(st.instanceId, 11); + } + // --- Cape texture (group 15 / texture type 2) --- // The geoset above enables the cape mesh, but without a texture it renders blank. if (hasInvType({16})) { @@ -7585,6 +7701,83 @@ void Application::setOnlinePlayerEquipment(uint64_t guid, if (newTex) { charRenderer->setTextureSlotOverride(st.instanceId, static_cast(slots.skin), newTex); } + + // --- Weapon model attachment --- + // Slot indices in the 19-element EquipSlot array: + // 15 = MAIN_HAND → attachment 1 (right hand) + // 16 = OFF_HAND → attachment 2 (left hand) + struct OnlineWeaponSlot { + int slotIndex; + uint32_t attachmentId; + }; + static constexpr OnlineWeaponSlot weaponSlots[] = { + { 15, 1 }, // MAIN_HAND → right hand + { 16, 2 }, // OFF_HAND → left hand + }; + + const uint32_t modelFieldL = idiL ? (*idiL)["LeftModel"] : 1u; + const uint32_t modelFieldR = idiL ? (*idiL)["RightModel"] : 2u; + const uint32_t texFieldL = idiL ? (*idiL)["LeftModelTexture"] : 3u; + const uint32_t texFieldR = idiL ? (*idiL)["RightModelTexture"] : 4u; + + for (const auto& ws : weaponSlots) { + uint32_t weapDisplayId = displayInfoIds[ws.slotIndex]; + if (weapDisplayId == 0) { + charRenderer->detachWeapon(st.instanceId, ws.attachmentId); + continue; + } + + int32_t recIdx = displayInfoDbc->findRecordById(weapDisplayId); + if (recIdx < 0) { + charRenderer->detachWeapon(st.instanceId, ws.attachmentId); + continue; + } + + // Prefer LeftModel (full weapon), fall back to RightModel (hilt variants) + std::string modelName = displayInfoDbc->getString(static_cast(recIdx), modelFieldL); + std::string textureName = displayInfoDbc->getString(static_cast(recIdx), texFieldL); + if (modelName.empty()) { + modelName = displayInfoDbc->getString(static_cast(recIdx), modelFieldR); + textureName = displayInfoDbc->getString(static_cast(recIdx), texFieldR); + } + if (modelName.empty()) { + charRenderer->detachWeapon(st.instanceId, ws.attachmentId); + continue; + } + + // Convert .mdx → .m2 + std::string modelFile = modelName; + { + size_t dotPos = modelFile.rfind('.'); + if (dotPos != std::string::npos) modelFile = modelFile.substr(0, dotPos); + modelFile += ".m2"; + } + + // Try Weapon directory first, then Shield + std::string m2Path = "Item\\ObjectComponents\\Weapon\\" + modelFile; + pipeline::M2Model weaponModel; + if (!loadWeaponM2(m2Path, weaponModel)) { + m2Path = "Item\\ObjectComponents\\Shield\\" + modelFile; + if (!loadWeaponM2(m2Path, weaponModel)) { + charRenderer->detachWeapon(st.instanceId, ws.attachmentId); + continue; + } + } + + // Build texture path + std::string texturePath; + if (!textureName.empty()) { + texturePath = "Item\\ObjectComponents\\Weapon\\" + textureName + ".blp"; + if (!assetManager->fileExists(texturePath)) { + texturePath = "Item\\ObjectComponents\\Shield\\" + textureName + ".blp"; + if (!assetManager->fileExists(texturePath)) texturePath.clear(); + } + } + + uint32_t weaponModelId = nextWeaponModelId_++; + charRenderer->attachWeapon(st.instanceId, ws.attachmentId, + weaponModel, weaponModelId, texturePath); + } } void Application::despawnOnlinePlayer(uint64_t guid) { diff --git a/src/rendering/character_preview.cpp b/src/rendering/character_preview.cpp index 3b7e9d72..86b8eea2 100644 --- a/src/rendering/character_preview.cpp +++ b/src/rendering/character_preview.cpp @@ -623,11 +623,27 @@ bool CharacterPreview::applyEquipment(const std::vector& eq uint32_t gg = getGeosetGroup(did, 0); if (gg > 0) geosetGloves = static_cast(401 + gg); } + // Wrists/Bracers → group 8 (sleeves, only if chest/shirt didn't set it) + { + uint32_t did = findDisplayId({9}); + if (did != 0 && geosetSleeves == 801) { + uint32_t gg = getGeosetGroup(did, 0); + if (gg > 0) geosetSleeves = static_cast(801 + gg); + } + } + // Belt → group 18 (buckle) + uint16_t geosetBelt = 0; + { + uint32_t did = findDisplayId({6}); + uint32_t gg = getGeosetGroup(did, 0); + if (gg > 0) geosetBelt = static_cast(1801 + gg); + } geosets.insert(geosetGloves); geosets.insert(geosetBoots); geosets.insert(geosetSleeves); geosets.insert(geosetPants); + if (geosetBelt != 0) geosets.insert(geosetBelt); geosets.insert(hasInvType({16}) ? 1502 : 1501); // Cloak mesh toggle (visual may still be limited) if (hasInvType({19})) geosets.insert(1201); // Tabard mesh toggle diff --git a/src/rendering/minimap.cpp b/src/rendering/minimap.cpp index e6940d3e..6e9fbb05 100644 --- a/src/rendering/minimap.cpp +++ b/src/rendering/minimap.cpp @@ -11,6 +11,7 @@ #include "core/coordinates.hpp" #include "core/logger.hpp" #include +#include #include #include @@ -380,7 +381,10 @@ VkTexture* Minimap::getOrLoadTileTexture(int tileX, int tileY) { // -------------------------------------------------------- void Minimap::updateTileDescriptors(uint32_t frameIdx, int centerTileX, int centerTileY) { + constexpr int kTileCount = 9; // 3x3 grid VkDevice device = vkCtx->getDevice(); + std::array imgInfos{}; + std::array writes{}; int slot = 0; for (int dr = -1; dr <= 1; dr++) { @@ -392,20 +396,20 @@ void Minimap::updateTileDescriptors(uint32_t frameIdx, int centerTileX, int cent if (!tileTex || !tileTex->isValid()) tileTex = noDataTexture.get(); - VkDescriptorImageInfo imgInfo = tileTex->descriptorInfo(); + imgInfos[slot] = tileTex->descriptorInfo(); - VkWriteDescriptorSet write{}; - write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - write.dstSet = tileDescSets[frameIdx][slot]; - write.dstBinding = 0; - write.descriptorCount = 1; - write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - write.pImageInfo = &imgInfo; - - vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); + writes[slot] = {}; + writes[slot].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[slot].dstSet = tileDescSets[frameIdx][slot]; + writes[slot].dstBinding = 0; + writes[slot].descriptorCount = 1; + writes[slot].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[slot].pImageInfo = &imgInfos[slot]; slot++; } } + + vkUpdateDescriptorSets(device, kTileCount, writes.data(), 0, nullptr); } // --------------------------------------------------------