mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-27 05:23:51 +00:00
Fix NPC clothing geoset selection and cape visibility rules
- normalize humanoid NPC clothing geosets at spawn time to avoid conflicting overlays\n- force pants-first selection for group 13 to prevent robe/kilt meshes on trouser NPCs\n- hide cloak groups for NPCs unless a renderable cape texture can actually be resolved\n- avoid falling back to missing equipment texture paths during region compositing\n- stop model-scope type-2 cape texture writes that could leak cape/white textures across shared model IDs\n- add group texture override usage in character renderer draw path\n- update character preview equipment application to reuse preview applyEquipment path\n- keep hand-only keybone fallback for attachments to prevent helmet/anchor misbinds
This commit is contained in:
parent
7e1a463061
commit
40ce3ec97d
4 changed files with 474 additions and 243 deletions
|
|
@ -3606,7 +3606,10 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
case 3: // chest
|
||||
return region <= 4;
|
||||
case 4: // belt
|
||||
return region == 4;
|
||||
// TODO(#npc-belt-region): belt torso-lower overlay can
|
||||
// cut out male abdomen on some humanoid NPCs.
|
||||
// Keep disabled until region compositing is fixed.
|
||||
return false;
|
||||
case 5: // legs
|
||||
return region == 5 || region == 6;
|
||||
case 6: // feet
|
||||
|
|
@ -3639,10 +3642,12 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
std::string(npcComponentDirs[region]) + "\\" + texName;
|
||||
std::string genderPath = base + (npcIsFemale ? "_F.blp" : "_M.blp");
|
||||
std::string unisexPath = base + "_U.blp";
|
||||
std::string basePath = base + ".blp";
|
||||
std::string fullPath;
|
||||
if (assetManager->fileExists(genderPath)) fullPath = genderPath;
|
||||
else if (assetManager->fileExists(unisexPath)) fullPath = unisexPath;
|
||||
else fullPath = base + ".blp";
|
||||
else if (assetManager->fileExists(basePath)) fullPath = basePath;
|
||||
else continue;
|
||||
|
||||
npcRegionLayers.emplace_back(region, fullPath);
|
||||
}
|
||||
|
|
@ -3722,12 +3727,13 @@ 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
|
||||
const bool allowNpcRegionComposite = true;
|
||||
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()) {
|
||||
if (allowNpcRegionComposite && !npcRegionLayers.empty()) {
|
||||
finalTex = charRenderer->compositeWithRegions(bakePath, {}, npcRegionLayers);
|
||||
LOG_DEBUG("Composited NPC baked texture with ", npcRegionLayers.size(),
|
||||
" equipment regions: ", bakePath);
|
||||
|
|
@ -3802,7 +3808,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
for (const auto& uw : npcUnderwear) skinLayers.push_back(uw);
|
||||
|
||||
GLuint npcSkinTex = 0;
|
||||
if (!npcRegionLayers.empty()) {
|
||||
if (allowNpcRegionComposite && !npcRegionLayers.empty()) {
|
||||
npcSkinTex = charRenderer->compositeWithRegions(npcSkinPath,
|
||||
std::vector<std::string>(skinLayers.begin() + 1, skinLayers.end()),
|
||||
npcRegionLayers);
|
||||
|
|
@ -3863,27 +3869,9 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
}
|
||||
}
|
||||
|
||||
// Apply cape texture only to object-skin slots (type 2) so body/face
|
||||
// textures never bleed onto cloaks.
|
||||
if (!npcCapeTexturePath.empty() && modelData) {
|
||||
GLuint capeTex = charRenderer->loadTexture(npcCapeTexturePath);
|
||||
if (capeTex != 0) {
|
||||
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
||||
if (modelData->textures[ti].type == 2) {
|
||||
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), capeTex);
|
||||
LOG_DEBUG("Applied NPC cape texture to slot ", ti, ": ", npcCapeTexturePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (modelData) {
|
||||
// Hide cloak mesh when no cape texture exists for this NPC.
|
||||
GLuint hiddenTex = charRenderer->getTransparentTexture();
|
||||
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
||||
if (modelData->textures[ti].type == 2) {
|
||||
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), hiddenTex);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Do not apply cape textures at model scope here. Type-2 texture slots are
|
||||
// shared per model and this can leak cape textures/white fallbacks onto
|
||||
// unrelated humanoid NPCs that use the same modelId.
|
||||
} else {
|
||||
LOG_WARNING(" extraDisplayId ", dispData.extraDisplayId, " not found in humanoidExtraMap");
|
||||
}
|
||||
|
|
@ -3997,16 +3985,45 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
return;
|
||||
}
|
||||
|
||||
// Use a safe humanoid geoset mask to avoid rendering conflicting geosets
|
||||
// (e.g. robe skirt + pants simultaneously) when model defaults expose all groups.
|
||||
if (itDisplayData != displayDataMap_.end() &&
|
||||
// Optional humanoid NPC geoset mask. Disabled by default because forcing geosets
|
||||
// causes long-standing visual artifacts on some models (missing waist, phantom
|
||||
// bracers, flickering apron overlays). Prefer model defaults.
|
||||
static constexpr bool kEnableNpcSafeGeosetMask = false;
|
||||
if (kEnableNpcSafeGeosetMask &&
|
||||
itDisplayData != displayDataMap_.end() &&
|
||||
itDisplayData->second.extraDisplayId != 0) {
|
||||
auto itExtra = humanoidExtraMap_.find(itDisplayData->second.extraDisplayId);
|
||||
if (itExtra != humanoidExtraMap_.end()) {
|
||||
const auto& extra = itExtra->second;
|
||||
std::unordered_set<uint16_t> safeGeosets;
|
||||
for (uint16_t i = 0; i <= 99; i++) safeGeosets.insert(i);
|
||||
|
||||
std::unordered_set<uint16_t> modelGeosets;
|
||||
std::unordered_map<uint16_t, uint16_t> firstGeosetByGroup;
|
||||
if (const auto* md = charRenderer->getModelData(modelId)) {
|
||||
for (const auto& b : md->batches) {
|
||||
const uint16_t sid = b.submeshId;
|
||||
modelGeosets.insert(sid);
|
||||
const uint16_t group = static_cast<uint16_t>(sid / 100);
|
||||
auto it = firstGeosetByGroup.find(group);
|
||||
if (it == firstGeosetByGroup.end() || sid < it->second) {
|
||||
firstGeosetByGroup[group] = sid;
|
||||
}
|
||||
}
|
||||
}
|
||||
auto addSafeGeoset = [&](uint16_t preferredId) {
|
||||
if (preferredId < 100 || modelGeosets.empty()) {
|
||||
safeGeosets.insert(preferredId);
|
||||
return;
|
||||
}
|
||||
if (modelGeosets.count(preferredId) > 0) {
|
||||
safeGeosets.insert(preferredId);
|
||||
return;
|
||||
}
|
||||
const uint16_t group = static_cast<uint16_t>(preferredId / 100);
|
||||
auto it = firstGeosetByGroup.find(group);
|
||||
if (it != firstGeosetByGroup.end()) {
|
||||
safeGeosets.insert(it->second);
|
||||
}
|
||||
};
|
||||
uint16_t hairGeoset = 1;
|
||||
uint32_t hairKey = (static_cast<uint32_t>(extra.raceId) << 16) |
|
||||
(static_cast<uint32_t>(extra.sexId) << 8) |
|
||||
|
|
@ -4015,8 +4032,24 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
if (itHairGeo != hairGeosetMap_.end() && itHairGeo->second > 0) {
|
||||
hairGeoset = itHairGeo->second;
|
||||
}
|
||||
safeGeosets.insert(hairGeoset > 0 ? hairGeoset : 1);
|
||||
safeGeosets.insert(static_cast<uint16_t>(100 + std::max<uint16_t>(hairGeoset, 1)));
|
||||
const uint16_t selectedHairScalp = (hairGeoset > 0 ? hairGeoset : 1);
|
||||
std::unordered_set<uint16_t> hairScalpGeosetsForRaceSex;
|
||||
for (const auto& [k, v] : hairGeosetMap_) {
|
||||
uint8_t race = static_cast<uint8_t>((k >> 16) & 0xFF);
|
||||
uint8_t sex = static_cast<uint8_t>((k >> 8) & 0xFF);
|
||||
if (race == extra.raceId && sex == extra.sexId && v > 0 && v < 100) {
|
||||
hairScalpGeosetsForRaceSex.insert(v);
|
||||
}
|
||||
}
|
||||
// Group 0 contains both base body parts and race/sex hair scalp variants.
|
||||
// Keep all non-hair body submeshes, but only the selected hair scalp.
|
||||
for (uint16_t sid : modelGeosets) {
|
||||
if (sid >= 100) continue;
|
||||
if (hairScalpGeosetsForRaceSex.count(sid) > 0 && sid != selectedHairScalp) continue;
|
||||
safeGeosets.insert(sid);
|
||||
}
|
||||
safeGeosets.insert(selectedHairScalp);
|
||||
addSafeGeoset(static_cast<uint16_t>(100 + std::max<uint16_t>(hairGeoset, 1)));
|
||||
|
||||
uint32_t facialKey = (static_cast<uint32_t>(extra.raceId) << 16) |
|
||||
(static_cast<uint32_t>(extra.sexId) << 8) |
|
||||
|
|
@ -4024,23 +4057,25 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
auto itFacial = facialHairGeosetMap_.find(facialKey);
|
||||
if (itFacial != facialHairGeosetMap_.end()) {
|
||||
const auto& fhg = itFacial->second;
|
||||
safeGeosets.insert(static_cast<uint16_t>(200 + std::max<uint16_t>(fhg.geoset200, 1)));
|
||||
safeGeosets.insert(static_cast<uint16_t>(300 + std::max<uint16_t>(fhg.geoset300, 1)));
|
||||
addSafeGeoset(static_cast<uint16_t>(200 + std::max<uint16_t>(fhg.geoset200, 1)));
|
||||
addSafeGeoset(static_cast<uint16_t>(300 + std::max<uint16_t>(fhg.geoset300, 1)));
|
||||
} else {
|
||||
safeGeosets.insert(201);
|
||||
safeGeosets.insert(301);
|
||||
addSafeGeoset(201);
|
||||
addSafeGeoset(301);
|
||||
}
|
||||
|
||||
// Force pants (1301) and avoid robe skirt variants unless we re-enable full slot-accurate geosets.
|
||||
safeGeosets.insert(401);
|
||||
safeGeosets.insert(502);
|
||||
safeGeosets.insert(701);
|
||||
safeGeosets.insert(801);
|
||||
safeGeosets.insert(902);
|
||||
safeGeosets.insert(1201);
|
||||
safeGeosets.insert(1301);
|
||||
safeGeosets.insert(1502);
|
||||
safeGeosets.insert(2002);
|
||||
addSafeGeoset(301);
|
||||
addSafeGeoset(401);
|
||||
addSafeGeoset(402);
|
||||
addSafeGeoset(501);
|
||||
addSafeGeoset(701);
|
||||
addSafeGeoset(801);
|
||||
addSafeGeoset(901);
|
||||
addSafeGeoset(1201);
|
||||
addSafeGeoset(1301);
|
||||
addSafeGeoset(2002);
|
||||
|
||||
charRenderer->setActiveGeosets(instanceId, safeGeosets);
|
||||
}
|
||||
}
|
||||
|
|
@ -4099,12 +4134,34 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
|
||||
// Default equipment geosets (bare/no armor)
|
||||
// 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)
|
||||
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)
|
||||
std::unordered_set<uint16_t> modelGeosets;
|
||||
std::unordered_map<uint16_t, uint16_t> firstByGroup;
|
||||
if (const auto* md = charRenderer->getModelData(modelId)) {
|
||||
for (const auto& b : md->batches) {
|
||||
const uint16_t sid = b.submeshId;
|
||||
modelGeosets.insert(sid);
|
||||
const uint16_t group = static_cast<uint16_t>(sid / 100);
|
||||
auto it = firstByGroup.find(group);
|
||||
if (it == firstByGroup.end() || sid < it->second) {
|
||||
firstByGroup[group] = sid;
|
||||
}
|
||||
}
|
||||
}
|
||||
auto pickGeoset = [&](uint16_t preferred, uint16_t group) -> uint16_t {
|
||||
if (preferred != 0 && modelGeosets.count(preferred) > 0) return preferred;
|
||||
auto it = firstByGroup.find(group);
|
||||
if (it != firstByGroup.end()) return it->second;
|
||||
return preferred;
|
||||
};
|
||||
|
||||
uint16_t geosetGloves = pickGeoset(301, 3); // Bare gloves/forearms (group 3)
|
||||
uint16_t geosetBoots = pickGeoset(401, 4); // Bare boots/shins (group 4)
|
||||
uint16_t geosetTorso = pickGeoset(501, 5); // Base torso/waist (group 5)
|
||||
uint16_t geosetSleeves = pickGeoset(801, 8); // Bare wrists (group 8, controlled by chest)
|
||||
uint16_t geosetPants = pickGeoset(1301, 13); // Bare legs (group 13)
|
||||
uint16_t geosetCape = 0; // Group 15 disabled unless cape is equipped
|
||||
uint16_t geosetTabard = 0; // TODO: NPC tabard geosets currently flicker/apron; keep hidden for now
|
||||
GLuint npcCapeTextureId = 0;
|
||||
|
||||
// Load equipment geosets from ItemDisplayInfo.dbc
|
||||
// DBC columns: 7=GeosetGroup[0], 8=GeosetGroup[1], 9=GeosetGroup[2]
|
||||
|
|
@ -4113,17 +4170,17 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
if (itemDisplayDbc) {
|
||||
// Equipment slots: 0=helm, 1=shoulder, 2=shirt, 3=chest, 4=belt, 5=legs, 6=feet, 7=wrist, 8=hands, 9=tabard, 10=cape
|
||||
const uint32_t fGG1 = idiL ? (*idiL)["GeosetGroup1"] : 7;
|
||||
const uint32_t fGG3 = idiL ? (*idiL)["GeosetGroup3"] : 9;
|
||||
|
||||
// Chest (slot 3) → group 8 (sleeves/wristbands)
|
||||
// Chest (slot 3) → group 5 (torso) + 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) 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);
|
||||
if (gg > 0) geosetTorso = pickGeoset(static_cast<uint16_t>(501 + gg), 5);
|
||||
if (gg > 0) geosetSleeves = pickGeoset(static_cast<uint16_t>(801 + gg), 8);
|
||||
// Do not derive robe/kilt from chest by default here.
|
||||
// Some NPC datasets set chest geosets that cause persistent
|
||||
// apron/robe overlays; prefer explicit legs slot for trousers.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4132,41 +4189,91 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[5]);
|
||||
if (idx >= 0) {
|
||||
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
|
||||
if (gg > 0) geosetPants = static_cast<uint16_t>(1301 + gg);
|
||||
if (gg > 0) geosetPants = pickGeoset(static_cast<uint16_t>(1301 + gg), 13);
|
||||
}
|
||||
}
|
||||
|
||||
// Feet (slot 6) → group 5 (boots/shins)
|
||||
// Feet (slot 6) → group 4 (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>(501 + gg);
|
||||
if (gg > 0) geosetBoots = pickGeoset(static_cast<uint16_t>(401 + gg), 4);
|
||||
}
|
||||
}
|
||||
|
||||
// Hands (slot 8) → group 4 (gloves/forearms)
|
||||
// Hands (slot 8) → group 3 (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>(401 + gg);
|
||||
if (gg > 0) geosetGloves = pickGeoset(static_cast<uint16_t>(301 + gg), 3);
|
||||
}
|
||||
}
|
||||
|
||||
// Tabard (slot 9) → group 12
|
||||
if (extra.equipDisplayId[9] != 0) {
|
||||
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[9]);
|
||||
if (idx >= 0) {
|
||||
geosetTabard = 1202;
|
||||
}
|
||||
}
|
||||
// Tabard (slot 9) intentionally disabled for now (see geosetTabard TODO above).
|
||||
|
||||
// Cape (slot 10) → group 15
|
||||
if (extra.equipDisplayId[10] != 0) {
|
||||
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[10]);
|
||||
if (idx >= 0) {
|
||||
geosetCape = 1502;
|
||||
const bool npcIsFemale = (extra.sexId == 1);
|
||||
const uint32_t leftTexField = idiL ? (*idiL)["LeftModelTexture"] : 3u;
|
||||
std::vector<std::string> capeNames;
|
||||
auto addName = [&](const std::string& n) {
|
||||
if (!n.empty() && std::find(capeNames.begin(), capeNames.end(), n) == capeNames.end()) {
|
||||
capeNames.push_back(n);
|
||||
}
|
||||
};
|
||||
std::string leftName = itemDisplayDbc->getString(static_cast<uint32_t>(idx), leftTexField);
|
||||
addName(leftName);
|
||||
|
||||
auto hasBlpExt = [](const std::string& p) {
|
||||
if (p.size() < 4) return false;
|
||||
std::string ext = p.substr(p.size() - 4);
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return ext == ".blp";
|
||||
};
|
||||
std::vector<std::string> capeCandidates;
|
||||
auto addCapeCandidate = [&](const std::string& p) {
|
||||
if (p.empty()) return;
|
||||
if (std::find(capeCandidates.begin(), capeCandidates.end(), p) == capeCandidates.end()) {
|
||||
capeCandidates.push_back(p);
|
||||
}
|
||||
};
|
||||
for (const auto& nameRaw : capeNames) {
|
||||
std::string name = nameRaw;
|
||||
std::replace(name.begin(), name.end(), '/', '\\');
|
||||
const bool hasDir = (name.find('\\') != std::string::npos);
|
||||
const bool hasExt = hasBlpExt(name);
|
||||
if (hasDir) {
|
||||
addCapeCandidate(name);
|
||||
if (!hasExt) addCapeCandidate(name + ".blp");
|
||||
} else {
|
||||
std::string baseObj = "Item\\ObjectComponents\\Cape\\" + name;
|
||||
std::string baseTex = "Item\\TextureComponents\\Cape\\" + name;
|
||||
addCapeCandidate(baseObj);
|
||||
addCapeCandidate(baseTex);
|
||||
if (!hasExt) {
|
||||
addCapeCandidate(baseObj + ".blp");
|
||||
addCapeCandidate(baseTex + ".blp");
|
||||
}
|
||||
addCapeCandidate(baseObj + (npcIsFemale ? "_F.blp" : "_M.blp"));
|
||||
addCapeCandidate(baseObj + "_U.blp");
|
||||
addCapeCandidate(baseTex + (npcIsFemale ? "_F.blp" : "_M.blp"));
|
||||
addCapeCandidate(baseTex + "_U.blp");
|
||||
}
|
||||
}
|
||||
const GLuint whiteTex = charRenderer->loadTexture("");
|
||||
for (const auto& candidate : capeCandidates) {
|
||||
GLuint tex = charRenderer->loadTexture(candidate);
|
||||
if (tex != 0 && tex != whiteTex) {
|
||||
npcCapeTextureId = tex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4174,13 +4281,28 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
// Apply equipment geosets
|
||||
activeGeosets.insert(geosetGloves);
|
||||
activeGeosets.insert(geosetBoots);
|
||||
activeGeosets.insert(geosetTorso);
|
||||
activeGeosets.insert(geosetSleeves);
|
||||
activeGeosets.insert(geosetPants);
|
||||
activeGeosets.insert(geosetCape);
|
||||
activeGeosets.insert(geosetTabard);
|
||||
activeGeosets.insert(702); // Ears: default
|
||||
activeGeosets.insert(902); // Kneepads: default
|
||||
activeGeosets.insert(2002); // Bare feet mesh
|
||||
if (geosetCape != 0) {
|
||||
activeGeosets.insert(geosetCape);
|
||||
}
|
||||
if (geosetTabard != 0) {
|
||||
activeGeosets.insert(geosetTabard);
|
||||
}
|
||||
activeGeosets.insert(pickGeoset(702, 7)); // Ears: default
|
||||
activeGeosets.insert(pickGeoset(902, 9)); // Kneepads: default
|
||||
activeGeosets.insert(pickGeoset(2002, 20)); // Bare feet mesh
|
||||
// Keep all model-present torso variants active to avoid missing male
|
||||
// abdomen/waist sections when a single 5xx pick is wrong.
|
||||
for (uint16_t sid : modelGeosets) {
|
||||
if ((sid / 100) == 5) activeGeosets.insert(sid);
|
||||
}
|
||||
// Keep all model-present pelvis variants active to avoid missing waist/belt
|
||||
// sections on some humanoid males when a single 9xx variant is wrong.
|
||||
for (uint16_t sid : modelGeosets) {
|
||||
if ((sid / 100) == 9) activeGeosets.insert(sid);
|
||||
}
|
||||
|
||||
// Hide hair under helmets: replace style-specific scalp with bald scalp
|
||||
if (extra.equipDisplayId[0] != 0 && hairGeoset > 1) {
|
||||
|
|
@ -4208,12 +4330,25 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
}
|
||||
LOG_INFO("NPC geosets for instance ", instanceId, ": [", geosetList, "]");
|
||||
charRenderer->setActiveGeosets(instanceId, activeGeosets);
|
||||
if (geosetCape != 0 && npcCapeTextureId != 0) {
|
||||
charRenderer->setGroupTextureOverride(instanceId, 15, npcCapeTextureId);
|
||||
if (const auto* md = charRenderer->getModelData(modelId)) {
|
||||
for (size_t ti = 0; ti < md->textures.size(); ti++) {
|
||||
if (md->textures[ti].type == 2) {
|
||||
charRenderer->setTextureSlotOverride(instanceId, static_cast<uint16_t>(ti), npcCapeTextureId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG_DEBUG("Set humanoid geosets: hair=", (int)hairGeoset,
|
||||
" sleeves=", geosetSleeves, " pants=", geosetPants,
|
||||
" boots=", geosetBoots, " gloves=", geosetGloves);
|
||||
|
||||
// TODO(#helmet-attach): NPC helmet attachment anchors are currently unreliable
|
||||
// on some humanoid models (floating/incorrect bone bind). Keep hidden for now.
|
||||
static constexpr bool kEnableNpcHelmetAttachmentsMainPath = false;
|
||||
// Load and attach helmet model if equipped
|
||||
if (extra.equipDisplayId[0] != 0 && itemDisplayDbc) {
|
||||
if (kEnableNpcHelmetAttachmentsMainPath && extra.equipDisplayId[0] != 0 && itemDisplayDbc) {
|
||||
int32_t helmIdx = itemDisplayDbc->findRecordById(extra.equipDisplayId[0]);
|
||||
if (helmIdx >= 0) {
|
||||
// Get helmet model name from ItemDisplayInfo.dbc (LeftModel)
|
||||
|
|
@ -4278,8 +4413,13 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
helmTexPath = "Item\\ObjectComponents\\Head\\" + helmTexName + ".blp";
|
||||
}
|
||||
}
|
||||
charRenderer->attachWeapon(instanceId, 11, helmModel, helmModelId, helmTexPath);
|
||||
LOG_DEBUG("Attached helmet model: ", helmPath, " tex: ", helmTexPath);
|
||||
bool attached = charRenderer->attachWeapon(instanceId, 0, helmModel, helmModelId, helmTexPath);
|
||||
if (!attached) {
|
||||
attached = charRenderer->attachWeapon(instanceId, 11, helmModel, helmModelId, helmTexPath);
|
||||
}
|
||||
if (attached) {
|
||||
LOG_DEBUG("Attached helmet model: ", helmPath, " tex: ", helmTexPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4288,6 +4428,141 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
}
|
||||
}
|
||||
|
||||
// With full humanoid overrides disabled, some character-style NPC models still render
|
||||
// conflicting clothing geosets at once (global capes, robe skirts over trousers).
|
||||
// Normalize only clothing groups while leaving all other model batches untouched.
|
||||
if (const auto* md = charRenderer->getModelData(modelId)) {
|
||||
std::unordered_set<uint16_t> allGeosets;
|
||||
std::unordered_map<uint16_t, uint16_t> firstByGroup;
|
||||
bool hasGroup13 = false; // trousers/robe skirt variants
|
||||
bool hasGroup15 = false; // cloak variants
|
||||
for (const auto& b : md->batches) {
|
||||
const uint16_t sid = b.submeshId;
|
||||
const uint16_t group = static_cast<uint16_t>(sid / 100);
|
||||
allGeosets.insert(sid);
|
||||
auto itFirst = firstByGroup.find(group);
|
||||
if (itFirst == firstByGroup.end() || sid < itFirst->second) {
|
||||
firstByGroup[group] = sid;
|
||||
}
|
||||
if (group == 13) hasGroup13 = true;
|
||||
if (group == 15) hasGroup15 = true;
|
||||
}
|
||||
|
||||
// Only apply to humanoid-like clothing models.
|
||||
if (hasGroup13 || hasGroup15) {
|
||||
bool hasRenderableCape = false;
|
||||
if (itDisplayData != displayDataMap_.end() &&
|
||||
itDisplayData->second.extraDisplayId != 0) {
|
||||
auto itExtra = humanoidExtraMap_.find(itDisplayData->second.extraDisplayId);
|
||||
if (itExtra != humanoidExtraMap_.end()) {
|
||||
uint32_t capeDisplayId = itExtra->second.equipDisplayId[10];
|
||||
if (capeDisplayId != 0) {
|
||||
auto itemDisplayDbc = assetManager->loadDBC("ItemDisplayInfo.dbc");
|
||||
const auto* idiL = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
|
||||
if (itemDisplayDbc) {
|
||||
int32_t recIdx = itemDisplayDbc->findRecordById(capeDisplayId);
|
||||
if (recIdx >= 0) {
|
||||
const uint32_t leftTexField = idiL ? (*idiL)["LeftModelTexture"] : 3u;
|
||||
const uint32_t rightTexField = idiL ? (*idiL)["RightModelTexture"] : 4u;
|
||||
std::vector<std::string> capeNames;
|
||||
auto addName = [&](const std::string& n) {
|
||||
if (!n.empty() &&
|
||||
std::find(capeNames.begin(), capeNames.end(), n) == capeNames.end()) {
|
||||
capeNames.push_back(n);
|
||||
}
|
||||
};
|
||||
addName(itemDisplayDbc->getString(static_cast<uint32_t>(recIdx), leftTexField));
|
||||
addName(itemDisplayDbc->getString(static_cast<uint32_t>(recIdx), rightTexField));
|
||||
|
||||
auto hasBlpExt = [](const std::string& p) {
|
||||
if (p.size() < 4) return false;
|
||||
std::string ext = p.substr(p.size() - 4);
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return ext == ".blp";
|
||||
};
|
||||
|
||||
const bool npcIsFemale = (itExtra->second.sexId == 1);
|
||||
std::vector<std::string> candidates;
|
||||
auto addCandidate = [&](const std::string& p) {
|
||||
if (p.empty()) return;
|
||||
if (std::find(candidates.begin(), candidates.end(), p) == candidates.end()) {
|
||||
candidates.push_back(p);
|
||||
}
|
||||
};
|
||||
|
||||
for (const auto& raw : capeNames) {
|
||||
std::string name = raw;
|
||||
std::replace(name.begin(), name.end(), '/', '\\');
|
||||
const bool hasDir = (name.find('\\') != std::string::npos);
|
||||
const bool hasExt = hasBlpExt(name);
|
||||
if (hasDir) {
|
||||
addCandidate(name);
|
||||
if (!hasExt) addCandidate(name + ".blp");
|
||||
} else {
|
||||
std::string baseObj = "Item\\ObjectComponents\\Cape\\" + name;
|
||||
std::string baseTex = "Item\\TextureComponents\\Cape\\" + name;
|
||||
addCandidate(baseObj);
|
||||
addCandidate(baseTex);
|
||||
if (!hasExt) {
|
||||
addCandidate(baseObj + ".blp");
|
||||
addCandidate(baseTex + ".blp");
|
||||
}
|
||||
addCandidate(baseObj + (npcIsFemale ? "_F.blp" : "_M.blp"));
|
||||
addCandidate(baseObj + "_U.blp");
|
||||
addCandidate(baseTex + (npcIsFemale ? "_F.blp" : "_M.blp"));
|
||||
addCandidate(baseTex + "_U.blp");
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& p : candidates) {
|
||||
if (assetManager->fileExists(p)) {
|
||||
hasRenderableCape = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_set<uint16_t> normalizedGeosets;
|
||||
for (uint16_t sid : allGeosets) {
|
||||
const uint16_t group = static_cast<uint16_t>(sid / 100);
|
||||
if (group == 13 || group == 15) continue;
|
||||
// Some humanoid models carry cloak cloth in group 16. Strip this too
|
||||
// when no cape is equipped to avoid "everyone has a cape".
|
||||
if (!hasRenderableCape && group == 16) continue;
|
||||
normalizedGeosets.insert(sid);
|
||||
}
|
||||
|
||||
auto pickFromGroup = [&](uint16_t preferredSid, uint16_t group) -> uint16_t {
|
||||
if (allGeosets.count(preferredSid) > 0) return preferredSid;
|
||||
auto it = firstByGroup.find(group);
|
||||
if (it != firstByGroup.end()) return it->second;
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Prefer trousers geoset, not robe/kilt overlays.
|
||||
if (hasGroup13) {
|
||||
uint16_t pantsSid = pickFromGroup(1301, 13);
|
||||
if (pantsSid != 0) normalizedGeosets.insert(pantsSid);
|
||||
}
|
||||
|
||||
// Prefer explicit cloak variant only when a cape is equipped.
|
||||
if (hasGroup15 && hasRenderableCape) {
|
||||
uint16_t capeSid = pickFromGroup(1502, 15);
|
||||
if (capeSid != 0) normalizedGeosets.insert(capeSid);
|
||||
}
|
||||
|
||||
if (!normalizedGeosets.empty()) {
|
||||
charRenderer->setActiveGeosets(instanceId, normalizedGeosets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Optional NPC helmet attachments (kept disabled for stability: this path
|
||||
// can increase spawn-time pressure and regress NPC visibility in crowded areas).
|
||||
static constexpr bool kEnableNpcHelmetAttachments = false;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#include "core/logger.hpp"
|
||||
#include <GL/glew.h>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <algorithm>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace wowee {
|
||||
|
|
@ -495,12 +496,15 @@ bool CharacterPreview::applyEquipment(const std::vector<game::EquipmentItem>& eq
|
|||
std::string genderPath = base + genderSuffix;
|
||||
std::string unisexPath = base + "_U.blp";
|
||||
std::string fullPath;
|
||||
std::string basePath = base + ".blp";
|
||||
if (assetManager_->fileExists(genderPath)) {
|
||||
fullPath = genderPath;
|
||||
} else if (assetManager_->fileExists(unisexPath)) {
|
||||
fullPath = unisexPath;
|
||||
} else if (assetManager_->fileExists(basePath)) {
|
||||
fullPath = basePath;
|
||||
} else {
|
||||
fullPath = base + ".blp";
|
||||
continue;
|
||||
}
|
||||
regionLayers.emplace_back(region, fullPath);
|
||||
}
|
||||
|
|
@ -513,6 +517,91 @@ bool CharacterPreview::applyEquipment(const std::vector<game::EquipmentItem>& eq
|
|||
}
|
||||
}
|
||||
|
||||
// Cloak texture (group 15) is separate from body compositing.
|
||||
if (hasInvType({16})) {
|
||||
uint32_t capeDisplayId = findDisplayId({16});
|
||||
if (capeDisplayId != 0) {
|
||||
int32_t capeRecIdx = displayInfoDbc->findRecordById(capeDisplayId);
|
||||
if (capeRecIdx >= 0) {
|
||||
std::vector<std::string> capeNames;
|
||||
auto addName = [&](const std::string& n) {
|
||||
if (!n.empty() && std::find(capeNames.begin(), capeNames.end(), n) == capeNames.end()) {
|
||||
capeNames.push_back(n);
|
||||
}
|
||||
};
|
||||
std::string leftName = displayInfoDbc->getString(static_cast<uint32_t>(capeRecIdx), 3);
|
||||
std::string rightName = displayInfoDbc->getString(static_cast<uint32_t>(capeRecIdx), 4);
|
||||
if (gender_ == game::Gender::FEMALE) {
|
||||
addName(rightName);
|
||||
addName(leftName);
|
||||
} else {
|
||||
addName(leftName);
|
||||
addName(rightName);
|
||||
}
|
||||
|
||||
auto hasBlpExt = [](const std::string& p) {
|
||||
if (p.size() < 4) return false;
|
||||
std::string ext = p.substr(p.size() - 4);
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return ext == ".blp";
|
||||
};
|
||||
std::vector<std::string> candidates;
|
||||
auto addCandidate = [&](const std::string& p) {
|
||||
if (!p.empty() && std::find(candidates.begin(), candidates.end(), p) == candidates.end()) {
|
||||
candidates.push_back(p);
|
||||
}
|
||||
};
|
||||
for (const auto& nameRaw : capeNames) {
|
||||
std::string name = nameRaw;
|
||||
std::replace(name.begin(), name.end(), '/', '\\');
|
||||
bool hasDir = (name.find('\\') != std::string::npos);
|
||||
bool hasExt = hasBlpExt(name);
|
||||
if (hasDir) {
|
||||
addCandidate(name);
|
||||
if (!hasExt) addCandidate(name + ".blp");
|
||||
} else {
|
||||
std::string baseObj = "Item\\ObjectComponents\\Cape\\" + name;
|
||||
std::string baseTex = "Item\\TextureComponents\\Cape\\" + name;
|
||||
addCandidate(baseObj);
|
||||
addCandidate(baseTex);
|
||||
if (!hasExt) {
|
||||
addCandidate(baseObj + ".blp");
|
||||
addCandidate(baseTex + ".blp");
|
||||
}
|
||||
addCandidate(baseObj + (gender_ == game::Gender::FEMALE ? "_F.blp" : "_M.blp"));
|
||||
addCandidate(baseObj + "_U.blp");
|
||||
addCandidate(baseTex + (gender_ == game::Gender::FEMALE ? "_F.blp" : "_M.blp"));
|
||||
addCandidate(baseTex + "_U.blp");
|
||||
}
|
||||
}
|
||||
const GLuint whiteTex = charRenderer_->loadTexture("");
|
||||
for (const auto& c : candidates) {
|
||||
GLuint capeTex = charRenderer_->loadTexture(c);
|
||||
if (capeTex != 0 && capeTex != whiteTex) {
|
||||
charRenderer_->setGroupTextureOverride(instanceId_, 15, capeTex);
|
||||
if (const auto* md = charRenderer_->getModelData(PREVIEW_MODEL_ID)) {
|
||||
for (size_t ti = 0; ti < md->textures.size(); ti++) {
|
||||
if (md->textures[ti].type == 2) {
|
||||
charRenderer_->setTextureSlotOverride(instanceId_, static_cast<uint16_t>(ti), capeTex);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (const auto* md = charRenderer_->getModelData(PREVIEW_MODEL_ID)) {
|
||||
for (size_t ti = 0; ti < md->textures.size(); ti++) {
|
||||
if (md->textures[ti].type == 2) {
|
||||
charRenderer_->clearTextureSlotOverride(instanceId_, static_cast<uint16_t>(ti));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1587,6 +1587,11 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
|||
|
||||
// Resolve texture for this batch (prefer hair textures for hair geosets).
|
||||
GLuint texId = resolveBatchTexture(instance, gpuModel, batch);
|
||||
const uint16_t batchGroup = static_cast<uint16_t>(batch.submeshId / 100);
|
||||
auto groupTexIt = instance.groupTextureOverrides.find(batchGroup);
|
||||
if (groupTexIt != instance.groupTextureOverrides.end() && groupTexIt->second != 0) {
|
||||
texId = groupTexIt->second;
|
||||
}
|
||||
|
||||
// Respect M2 material blend mode for creature/character submeshes.
|
||||
uint16_t blendMode = 0;
|
||||
|
|
@ -1611,7 +1616,7 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
|||
// Groups that share the body skin atlas: 0=body, 3=gloves, 4=boots, 5=chest,
|
||||
// 8=wristbands, 9=pelvis, 13=pants. Hair (group 1) and facial hair (group 2) do NOT.
|
||||
if (texId == whiteTexture) {
|
||||
uint16_t group = batch.submeshId / 100;
|
||||
uint16_t group = batchGroup;
|
||||
bool isSkinGroup = (group == 0 || group == 3 || group == 4 || group == 5 ||
|
||||
group == 8 || group == 9 || group == 13);
|
||||
if (isSkinGroup) {
|
||||
|
|
@ -2080,8 +2085,8 @@ bool CharacterRenderer::attachWeapon(uint32_t charInstanceId, uint32_t attachmen
|
|||
}
|
||||
}
|
||||
}
|
||||
// Fallback: scan bones for keyBoneId 26 (right hand) / 27 (left hand)
|
||||
if (!found) {
|
||||
// Fallback to key-bone lookup only for weapon hand attachment IDs.
|
||||
if (!found && (attachmentId == 1 || attachmentId == 2)) {
|
||||
int32_t targetKeyBone = (attachmentId == 1) ? 26 : 27;
|
||||
for (size_t i = 0; i < charModel.bones.size(); i++) {
|
||||
if (charModel.bones[i].keyBoneId == targetKeyBone) {
|
||||
|
|
@ -2096,7 +2101,7 @@ bool CharacterRenderer::attachWeapon(uint32_t charInstanceId, uint32_t attachmen
|
|||
if (found && boneIndex >= charModel.bones.size()) {
|
||||
found = false;
|
||||
}
|
||||
if (!found) {
|
||||
if (!found && (attachmentId == 1 || attachmentId == 2)) {
|
||||
int32_t targetKeyBone = (attachmentId == 1) ? 26 : 27;
|
||||
for (size_t i = 0; i < charModel.bones.size(); i++) {
|
||||
if (charModel.bones[i].keyBoneId == targetKeyBone) {
|
||||
|
|
@ -2247,18 +2252,22 @@ bool CharacterRenderer::getAttachmentTransform(uint32_t instanceId, uint32_t att
|
|||
|
||||
// Validate bone index; invalid indices bind attachments to origin (looks like weapons at feet).
|
||||
if (boneIndex >= model.bones.size()) {
|
||||
// Fallback: key bones (26/27) for hand attachments.
|
||||
int32_t targetKeyBone = (attachmentId == 1) ? 26 : 27;
|
||||
found = false;
|
||||
for (size_t i = 0; i < model.bones.size(); i++) {
|
||||
if (model.bones[i].keyBoneId == targetKeyBone) {
|
||||
boneIndex = static_cast<uint16_t>(i);
|
||||
offset = glm::vec3(0.0f);
|
||||
found = true;
|
||||
break;
|
||||
// Fallback: key bones (26/27) only for hand attachments.
|
||||
if (attachmentId == 1 || attachmentId == 2) {
|
||||
int32_t targetKeyBone = (attachmentId == 1) ? 26 : 27;
|
||||
found = false;
|
||||
for (size_t i = 0; i < model.bones.size(); i++) {
|
||||
if (model.bones[i].keyBoneId == targetKeyBone) {
|
||||
boneIndex = static_cast<uint16_t>(i);
|
||||
offset = glm::vec3(0.0f);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (!found) return false;
|
||||
}
|
||||
|
||||
// Get bone matrix
|
||||
|
|
|
|||
|
|
@ -196,162 +196,20 @@ void InventoryScreen::updatePreview(float deltaTime) {
|
|||
}
|
||||
|
||||
void InventoryScreen::updatePreviewEquipment(game::Inventory& inventory) {
|
||||
if (!charPreview_ || !charPreview_->isModelLoaded() || !assetManager_) return;
|
||||
if (!charPreview_ || !charPreview_->isModelLoaded()) return;
|
||||
|
||||
auto* charRenderer = charPreview_->getCharacterRenderer();
|
||||
uint32_t instanceId = charPreview_->getInstanceId();
|
||||
if (!charRenderer || instanceId == 0) return;
|
||||
|
||||
// --- Geosets (mirroring GameScreen::updateCharacterGeosets) ---
|
||||
auto displayInfoDbc = assetManager_->loadDBC("ItemDisplayInfo.dbc");
|
||||
|
||||
auto getGeosetGroup = [&](uint32_t displayInfoId, int groupField) -> uint32_t {
|
||||
if (!displayInfoDbc || displayInfoId == 0) return 0;
|
||||
int32_t recIdx = displayInfoDbc->findRecordById(displayInfoId);
|
||||
if (recIdx < 0) return 0;
|
||||
return displayInfoDbc->getUInt32(static_cast<uint32_t>(recIdx), 7 + groupField);
|
||||
};
|
||||
|
||||
auto findEquippedDisplayId = [&](std::initializer_list<uint8_t> types) -> uint32_t {
|
||||
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
||||
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
||||
if (!slot.empty()) {
|
||||
for (uint8_t t : types) {
|
||||
if (slot.item.inventoryType == t)
|
||||
return slot.item.displayInfoId;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
auto hasEquippedType = [&](std::initializer_list<uint8_t> types) -> bool {
|
||||
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
||||
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
||||
if (!slot.empty()) {
|
||||
for (uint8_t t : types) {
|
||||
if (slot.item.inventoryType == t) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
std::unordered_set<uint16_t> geosets;
|
||||
// Body parts (group 0: IDs 0-99, some models use up to 27)
|
||||
for (uint16_t i = 0; i <= 99; i++) geosets.insert(i);
|
||||
|
||||
// Hair geoset: group 1 = 100 + hairStyle + 1
|
||||
geosets.insert(static_cast<uint16_t>(100 + playerHairStyle_ + 1));
|
||||
// Facial hair geoset: group 2 = 200 + facialHair + 1
|
||||
geosets.insert(static_cast<uint16_t>(200 + playerFacialHair_ + 1));
|
||||
geosets.insert(701); // Ears
|
||||
|
||||
// Chest/Shirt
|
||||
{
|
||||
uint32_t did = findEquippedDisplayId({4, 5, 20});
|
||||
uint32_t gg = getGeosetGroup(did, 0);
|
||||
geosets.insert(static_cast<uint16_t>(gg > 0 ? 501 + gg : 501));
|
||||
uint32_t gg3 = getGeosetGroup(did, 2);
|
||||
if (gg3 > 0) {
|
||||
geosets.insert(static_cast<uint16_t>(1301 + gg3));
|
||||
}
|
||||
}
|
||||
|
||||
// Legs
|
||||
{
|
||||
uint32_t did = findEquippedDisplayId({7});
|
||||
uint32_t gg = getGeosetGroup(did, 0);
|
||||
if (geosets.count(1302) == 0 && geosets.count(1303) == 0) {
|
||||
geosets.insert(static_cast<uint16_t>(gg > 0 ? 1301 + gg : 1301));
|
||||
}
|
||||
}
|
||||
|
||||
// Feet
|
||||
{
|
||||
uint32_t did = findEquippedDisplayId({8});
|
||||
uint32_t gg = getGeosetGroup(did, 0);
|
||||
geosets.insert(static_cast<uint16_t>(gg > 0 ? 401 + gg : 401));
|
||||
}
|
||||
|
||||
// Gloves
|
||||
{
|
||||
uint32_t did = findEquippedDisplayId({10});
|
||||
uint32_t gg = getGeosetGroup(did, 0);
|
||||
geosets.insert(static_cast<uint16_t>(gg > 0 ? 301 + gg : 301));
|
||||
}
|
||||
|
||||
// Cloak
|
||||
geosets.insert(hasEquippedType({16}) ? 1502 : 1501);
|
||||
|
||||
// Tabard
|
||||
if (hasEquippedType({19})) {
|
||||
geosets.insert(1201);
|
||||
}
|
||||
|
||||
charRenderer->setActiveGeosets(instanceId, geosets);
|
||||
|
||||
// --- Textures (mirroring GameScreen::updateCharacterTextures) ---
|
||||
auto& app = core::Application::getInstance();
|
||||
const auto& bodySkinPath = app.getBodySkinPath();
|
||||
const auto& underwearPaths = app.getUnderwearPaths();
|
||||
|
||||
if (bodySkinPath.empty() || !displayInfoDbc) return;
|
||||
|
||||
static const char* componentDirs[] = {
|
||||
"ArmUpperTexture", "ArmLowerTexture", "HandTexture",
|
||||
"TorsoUpperTexture", "TorsoLowerTexture",
|
||||
"LegUpperTexture", "LegLowerTexture", "FootTexture",
|
||||
};
|
||||
|
||||
std::vector<std::pair<int, std::string>> regionLayers;
|
||||
std::vector<game::EquipmentItem> equipped;
|
||||
equipped.reserve(game::Inventory::NUM_EQUIP_SLOTS);
|
||||
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
||||
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
||||
if (slot.empty() || slot.item.displayInfoId == 0) continue;
|
||||
|
||||
int32_t recIdx = displayInfoDbc->findRecordById(slot.item.displayInfoId);
|
||||
if (recIdx < 0) continue;
|
||||
|
||||
for (int region = 0; region < 8; region++) {
|
||||
uint32_t fieldIdx = 14 + region;
|
||||
std::string texName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), fieldIdx);
|
||||
if (texName.empty()) continue;
|
||||
|
||||
std::string base = "Item\\TextureComponents\\" +
|
||||
std::string(componentDirs[region]) + "\\" + texName;
|
||||
std::string genderSuffix = (playerGender_ == game::Gender::FEMALE) ? "_F.blp" : "_M.blp";
|
||||
std::string genderPath = base + genderSuffix;
|
||||
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";
|
||||
}
|
||||
regionLayers.emplace_back(region, fullPath);
|
||||
}
|
||||
game::EquipmentItem ei;
|
||||
ei.displayModel = slot.item.displayInfoId;
|
||||
ei.inventoryType = slot.item.inventoryType;
|
||||
ei.enchantment = 0;
|
||||
equipped.push_back(ei);
|
||||
}
|
||||
|
||||
// Find the skin texture slot index in the preview model
|
||||
// The preview model uses model ID PREVIEW_MODEL_ID; find slot for type-1 (body skin)
|
||||
const auto* modelData = charRenderer->getModelData(charPreview_->getModelId());
|
||||
uint32_t skinSlot = 0;
|
||||
if (modelData) {
|
||||
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
||||
if (modelData->textures[ti].type == 1) {
|
||||
skinSlot = static_cast<uint32_t>(ti);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GLuint newTex = charRenderer->compositeWithRegions(bodySkinPath, underwearPaths, regionLayers);
|
||||
if (newTex != 0) {
|
||||
charRenderer->setModelTexture(charPreview_->getModelId(), skinSlot, newTex);
|
||||
}
|
||||
|
||||
charPreview_->applyEquipment(equipped);
|
||||
previewDirty_ = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue