From fce8ccdc45fbf80eaa7b0d409f83929918ac1d95 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 7 Apr 2026 03:20:13 -0700 Subject: [PATCH] fix(rendering): restore NPC back panel and apply cape textures (#57) The geoset normalization stripped all group 15 (cloak) submeshes but only re-added them when a cape was equipped. NPCs without capes lost the "no cape" back panel (geoset 1501), exposing the single-sided torso mesh. Always add either the cape or no-cape geoset. Also load and apply cape texture overrides for NPCs that do have capes equipped via CreatureDisplayInfoExtra, matching the player path. --- src/core/entity_spawner.cpp | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/core/entity_spawner.cpp b/src/core/entity_spawner.cpp index 3baa1ae6..8c03a122 100644 --- a/src/core/entity_spawner.cpp +++ b/src/core/entity_spawner.cpp @@ -1959,6 +1959,7 @@ void EntitySpawner::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float // Only apply to humanoid-like clothing models. if (hasGroup3 || hasGroup4 || hasGroup8 || hasGroup12 || hasGroup13 || hasGroup15) { bool hasRenderableCape = false; + std::string capeTexturePath; // first found cape texture for override bool hasEquippedTabard = false; bool hasHumanoidExtra = false; uint8_t extraRaceId = 0; @@ -2066,6 +2067,7 @@ void EntitySpawner::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float for (const auto& p : candidates) { if (assetManager_->fileExists(p)) { hasRenderableCape = true; + capeTexturePath = p; break; } } @@ -2203,15 +2205,38 @@ void EntitySpawner::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float if (pantsSid != 0) normalizedGeosets.insert(pantsSid); } - // Prefer explicit cloak variant only when a cape is equipped. - if (hasGroup15 && hasRenderableCape) { - uint16_t capeSid = pickFromGroup(kGeosetWithCape, 15); - if (capeSid != 0) normalizedGeosets.insert(capeSid); + // Group 15: cloak mesh. Use "with cape" when equipped, otherwise + // use "no cape" back panel to cover the single-sided torso. + if (hasGroup15) { + if (hasRenderableCape) { + uint16_t capeSid = pickFromGroup(kGeosetWithCape, 15); + if (capeSid != 0) normalizedGeosets.insert(capeSid); + } else { + uint16_t noCape = pickFromGroup(kGeosetNoCape, 15); + if (noCape != 0) normalizedGeosets.insert(noCape); + } } if (!normalizedGeosets.empty()) { charRenderer->setActiveGeosets(instanceId, normalizedGeosets); } + + // Apply cape texture override so the cloak mesh shows the actual cape + // instead of the default body texture. + if (hasRenderableCape && !capeTexturePath.empty()) { + rendering::VkTexture* capeTex = charRenderer->loadTexture(capeTexturePath); + const rendering::VkTexture* whiteTex = charRenderer->loadTexture(""); + if (capeTex && capeTex != whiteTex) { + charRenderer->setGroupTextureOverride(instanceId, 15, capeTex); + if (const auto* md2 = charRenderer->getModelData(modelId)) { + for (size_t ti = 0; ti < md2->textures.size(); ti++) { + if (md2->textures[ti].type == 2) { + charRenderer->setTextureSlotOverride(instanceId, static_cast(ti), capeTex); + } + } + } + } + } } }