mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Fix NPC virtual weapons and stabilize facial hair geosets
- resolve creature virtual item values as Item.dbc entry -> DisplayID before direct ItemDisplayInfo fallback\n- use correct WotLK virtual item base index set (starting at 56) and keep retry attach loop for late update fields\n- keep NPC main-hand weapon attachment path and clear erroneous offhand attachments\n- refactor NPC virtual weapon attach into helper with per-creature retry tracking\n- tighten humanoid facial hair geoset selection to avoid loading all beard/sideburn meshes\n- adjust facial 3xx preference to favor chin-hair channel when available
This commit is contained in:
parent
ce6887071c
commit
d76537a8aa
2 changed files with 194 additions and 30 deletions
|
|
@ -91,6 +91,7 @@ private:
|
|||
void buildFactionHostilityMap(uint8_t playerRace);
|
||||
void spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation);
|
||||
void despawnOnlineCreature(uint64_t guid);
|
||||
bool tryAttachCreatureVirtualWeapons(uint64_t guid, uint32_t instanceId);
|
||||
void spawnOnlinePlayer(uint64_t guid,
|
||||
uint8_t raceId,
|
||||
uint8_t genderId,
|
||||
|
|
@ -177,6 +178,8 @@ private:
|
|||
std::unordered_map<uint64_t, uint32_t> creatureInstances_; // guid → render instanceId
|
||||
std::unordered_map<uint64_t, uint32_t> creatureModelIds_; // guid → loaded modelId
|
||||
std::unordered_map<uint64_t, glm::vec3> creatureRenderPosCache_; // guid -> last synced render position
|
||||
std::unordered_set<uint64_t> creatureWeaponsAttached_; // guid set when NPC virtual weapons attached
|
||||
std::unordered_map<uint64_t, uint8_t> creatureWeaponAttachAttempts_; // guid -> attach attempts
|
||||
std::unordered_set<uint64_t> deadCreatureGuids_; // GUIDs that should spawn in corpse/death pose
|
||||
std::unordered_map<uint32_t, uint32_t> displayIdModelCache_; // displayId → modelId (model caching)
|
||||
uint32_t nextCreatureModelId_ = 5000; // Model IDs for online creatures
|
||||
|
|
|
|||
|
|
@ -923,6 +923,10 @@ void Application::update(float deltaTime) {
|
|||
// creature models remain at stale spawn positions.
|
||||
if (renderer && gameHandler && renderer->getCharacterRenderer()) {
|
||||
auto* charRenderer = renderer->getCharacterRenderer();
|
||||
static float npcWeaponRetryTimer = 0.0f;
|
||||
npcWeaponRetryTimer += deltaTime;
|
||||
const bool npcWeaponRetryTick = (npcWeaponRetryTimer >= 1.0f);
|
||||
if (npcWeaponRetryTick) npcWeaponRetryTimer = 0.0f;
|
||||
glm::vec3 playerPos(0.0f);
|
||||
glm::vec3 playerRenderPos(0.0f);
|
||||
bool havePlayerPos = false;
|
||||
|
|
@ -942,6 +946,20 @@ void Application::update(float deltaTime) {
|
|||
auto entity = gameHandler->getEntityManager().getEntity(guid);
|
||||
if (!entity || entity->getType() != game::ObjectType::UNIT) continue;
|
||||
|
||||
if (npcWeaponRetryTick && !creatureWeaponsAttached_.count(guid)) {
|
||||
uint8_t attempts = 0;
|
||||
auto itAttempts = creatureWeaponAttachAttempts_.find(guid);
|
||||
if (itAttempts != creatureWeaponAttachAttempts_.end()) attempts = itAttempts->second;
|
||||
if (attempts < 30) {
|
||||
if (tryAttachCreatureVirtualWeapons(guid, instanceId)) {
|
||||
creatureWeaponsAttached_.insert(guid);
|
||||
creatureWeaponAttachAttempts_.erase(guid);
|
||||
} else {
|
||||
creatureWeaponAttachAttempts_[guid] = static_cast<uint8_t>(attempts + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
|
||||
if (havePlayerPos) {
|
||||
glm::vec3 d = canonical - playerPos;
|
||||
|
|
@ -2620,6 +2638,151 @@ void Application::loadEquippedWeapons() {
|
|||
}
|
||||
}
|
||||
|
||||
bool Application::tryAttachCreatureVirtualWeapons(uint64_t guid, uint32_t instanceId) {
|
||||
if (!renderer || !renderer->getCharacterRenderer() || !assetManager || !gameHandler) return false;
|
||||
auto* charRenderer = renderer->getCharacterRenderer();
|
||||
if (!charRenderer) return false;
|
||||
|
||||
auto entity = gameHandler->getEntityManager().getEntity(guid);
|
||||
if (!entity || entity->getType() != game::ObjectType::UNIT) return false;
|
||||
|
||||
auto itemDisplayDbc = assetManager->loadDBC("ItemDisplayInfo.dbc");
|
||||
if (!itemDisplayDbc) return false;
|
||||
auto itemDbc = assetManager->loadDBC("Item.dbc");
|
||||
const auto* idiL = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
|
||||
const auto* itemL = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("Item") : nullptr;
|
||||
|
||||
auto resolveDisplayInfoId = [&](uint32_t rawId) -> uint32_t {
|
||||
if (rawId == 0) return 0;
|
||||
// Prefer interpreting virtual slot value as Item.dbc entry first (AzerothCore style).
|
||||
if (itemDbc) {
|
||||
int32_t itemRec = itemDbc->findRecordById(rawId); // treat as item entry
|
||||
if (itemRec >= 0) {
|
||||
const uint32_t dispFieldPrimary = itemL ? (*itemL)["DisplayID"] : 5u;
|
||||
const uint32_t dispFieldFallback = 4u;
|
||||
uint32_t displayIdA = itemDbc->getUInt32(static_cast<uint32_t>(itemRec), dispFieldPrimary);
|
||||
if (displayIdA != 0 && itemDisplayDbc->findRecordById(displayIdA) >= 0) {
|
||||
return displayIdA;
|
||||
}
|
||||
uint32_t displayIdB = itemDbc->getUInt32(static_cast<uint32_t>(itemRec), dispFieldFallback);
|
||||
if (displayIdB != 0 && itemDisplayDbc->findRecordById(displayIdB) >= 0) {
|
||||
return displayIdB;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback: some cores can send ItemDisplayInfo IDs directly.
|
||||
if (itemDisplayDbc->findRecordById(rawId) >= 0) return rawId;
|
||||
return 0;
|
||||
};
|
||||
|
||||
auto attachNpcWeaponDisplay = [&](uint32_t itemDisplayId, uint32_t attachmentId) -> bool {
|
||||
uint32_t resolvedDisplayId = resolveDisplayInfoId(itemDisplayId);
|
||||
if (resolvedDisplayId == 0) return false;
|
||||
int32_t recIdx = itemDisplayDbc->findRecordById(resolvedDisplayId);
|
||||
if (recIdx < 0) return false;
|
||||
|
||||
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;
|
||||
// Prefer LeftModel (stock player equipment path uses LeftModel and avoids
|
||||
// the "hilt-only" variants seen when forcing RightModel).
|
||||
std::string modelName = itemDisplayDbc->getString(static_cast<uint32_t>(recIdx), modelFieldL);
|
||||
std::string textureName = itemDisplayDbc->getString(static_cast<uint32_t>(recIdx), texFieldL);
|
||||
if (modelName.empty()) {
|
||||
modelName = itemDisplayDbc->getString(static_cast<uint32_t>(recIdx), modelFieldR);
|
||||
textureName = itemDisplayDbc->getString(static_cast<uint32_t>(recIdx), texFieldR);
|
||||
}
|
||||
if (modelName.empty()) return false;
|
||||
|
||||
std::string modelFile = modelName;
|
||||
size_t dotPos = modelFile.rfind('.');
|
||||
if (dotPos != std::string::npos) modelFile = modelFile.substr(0, dotPos);
|
||||
modelFile += ".m2";
|
||||
|
||||
// Main-hand NPC weapon path: only use actual weapon models.
|
||||
// This avoids shields/placeholder hilts being attached incorrectly.
|
||||
std::string m2Path = "Item\\ObjectComponents\\Weapon\\" + modelFile;
|
||||
auto m2Data = assetManager->readFile(m2Path);
|
||||
if (m2Data.empty()) return false;
|
||||
|
||||
auto weaponModel = pipeline::M2Loader::load(m2Data);
|
||||
std::string skinFile = modelFile;
|
||||
size_t skinDot = skinFile.rfind('.');
|
||||
if (skinDot != std::string::npos) skinFile = skinFile.substr(0, skinDot);
|
||||
skinFile += "00.skin";
|
||||
std::string skinDir = m2Path.substr(0, m2Path.rfind('\\') + 1);
|
||||
auto skinData = assetManager->readFile(skinDir + skinFile);
|
||||
if (!skinData.empty() && weaponModel.version >= 264) {
|
||||
pipeline::M2Loader::loadSkin(skinData, weaponModel);
|
||||
}
|
||||
if (!weaponModel.isValid()) return false;
|
||||
|
||||
std::string texturePath;
|
||||
if (!textureName.empty()) {
|
||||
texturePath = "Item\\ObjectComponents\\Weapon\\" + textureName + ".blp";
|
||||
if (!assetManager->fileExists(texturePath)) texturePath.clear();
|
||||
}
|
||||
|
||||
uint32_t weaponModelId = nextWeaponModelId_++;
|
||||
return charRenderer->attachWeapon(instanceId, attachmentId, weaponModel, weaponModelId, texturePath);
|
||||
};
|
||||
|
||||
auto hasResolvableWeaponModel = [&](uint32_t itemDisplayId) -> bool {
|
||||
uint32_t resolvedDisplayId = resolveDisplayInfoId(itemDisplayId);
|
||||
if (resolvedDisplayId == 0) return false;
|
||||
int32_t recIdx = itemDisplayDbc->findRecordById(resolvedDisplayId);
|
||||
if (recIdx < 0) return false;
|
||||
const uint32_t modelFieldL = idiL ? (*idiL)["LeftModel"] : 1u;
|
||||
const uint32_t modelFieldR = idiL ? (*idiL)["RightModel"] : 2u;
|
||||
std::string modelName = itemDisplayDbc->getString(static_cast<uint32_t>(recIdx), modelFieldL);
|
||||
if (modelName.empty()) {
|
||||
modelName = itemDisplayDbc->getString(static_cast<uint32_t>(recIdx), modelFieldR);
|
||||
}
|
||||
if (modelName.empty()) return false;
|
||||
std::string modelFile = modelName;
|
||||
size_t dotPos = modelFile.rfind('.');
|
||||
if (dotPos != std::string::npos) modelFile = modelFile.substr(0, dotPos);
|
||||
modelFile += ".m2";
|
||||
return assetManager->fileExists("Item\\ObjectComponents\\Weapon\\" + modelFile);
|
||||
};
|
||||
|
||||
bool attachedMain = false;
|
||||
bool hadWeaponCandidate = false;
|
||||
|
||||
const uint16_t candidateBases[] = {56, 57, 58, 70, 148, 149, 150, 151, 152};
|
||||
for (uint16_t base : candidateBases) {
|
||||
uint32_t v0 = entity->getField(static_cast<uint16_t>(base + 0));
|
||||
if (v0 != 0) hadWeaponCandidate = true;
|
||||
if (!attachedMain && v0 != 0) attachedMain = attachNpcWeaponDisplay(v0, 1);
|
||||
if (attachedMain) break;
|
||||
}
|
||||
|
||||
uint16_t unitEnd = game::fieldIndex(game::UF::UNIT_END);
|
||||
uint16_t scanLo = 60;
|
||||
uint16_t scanHi = (unitEnd != 0xFFFF) ? static_cast<uint16_t>(unitEnd + 96) : 320;
|
||||
std::map<uint16_t, uint32_t> candidateByIndex;
|
||||
for (const auto& [idx, val] : entity->getFields()) {
|
||||
if (idx < scanLo || idx > scanHi) continue;
|
||||
if (val == 0) continue;
|
||||
if (hasResolvableWeaponModel(val)) {
|
||||
candidateByIndex[idx] = val;
|
||||
hadWeaponCandidate = true;
|
||||
}
|
||||
}
|
||||
for (const auto& [idx, val] : candidateByIndex) {
|
||||
if (!attachedMain) attachedMain = attachNpcWeaponDisplay(val, 1);
|
||||
if (attachedMain) break;
|
||||
}
|
||||
|
||||
// Force off-hand clear in NPC path to avoid incorrect shields/placeholder hilts.
|
||||
charRenderer->detachWeapon(instanceId, 2);
|
||||
// Success if main-hand attached when there was at least one candidate.
|
||||
return hadWeaponCandidate && attachedMain;
|
||||
}
|
||||
|
||||
void Application::buildFactionHostilityMap(uint8_t playerRace) {
|
||||
if (!assetManager || !assetManager->isInitialized() || !gameHandler) return;
|
||||
|
||||
|
|
@ -4476,9 +4639,8 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
uint8_t extraRaceId = 0;
|
||||
uint8_t extraSexId = 0;
|
||||
uint16_t selectedHairScalp = 1;
|
||||
uint16_t selectedFacial100 = 101;
|
||||
uint16_t selectedFacial200 = 200;
|
||||
uint16_t selectedFacial300 = 300;
|
||||
uint16_t selectedFacial300Alt = 300;
|
||||
bool wantsFacialHair = false;
|
||||
std::unordered_set<uint16_t> hairScalpGeosetsForRaceSex;
|
||||
if (itDisplayData != displayDataMap_.end() &&
|
||||
|
|
@ -4502,9 +4664,8 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
wantsFacialHair = (itExtra->second.facialHairId != 0);
|
||||
auto itFacial = facialHairGeosetMap_.find(facialKey);
|
||||
if (itFacial != facialHairGeosetMap_.end()) {
|
||||
selectedFacial100 = static_cast<uint16_t>(100 + itFacial->second.geoset100);
|
||||
selectedFacial200 = static_cast<uint16_t>(200 + itFacial->second.geoset200);
|
||||
selectedFacial300 = static_cast<uint16_t>(300 + itFacial->second.geoset300);
|
||||
selectedFacial300Alt = static_cast<uint16_t>(300 + itFacial->second.geoset200);
|
||||
}
|
||||
for (const auto& [k, v] : hairGeosetMap_) {
|
||||
uint8_t race = static_cast<uint8_t>((k >> 16) & 0xFF);
|
||||
|
|
@ -4600,17 +4761,6 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
// Group 1 contains connector variants that mirror scalp style.
|
||||
if (hasHumanoidExtra && group == 1) {
|
||||
const uint16_t selectedConnector = static_cast<uint16_t>(100 + std::max<uint16_t>(selectedHairScalp, 1));
|
||||
const bool allowSelectedFacial100 = wantsFacialHair &&
|
||||
sid == selectedFacial100 && allGeosets.count(selectedFacial100) > 0;
|
||||
const bool allowFacialFallback = wantsFacialHair && (sid == 100 || sid == 101);
|
||||
if (allowSelectedFacial100) {
|
||||
normalizedGeosets.insert(sid);
|
||||
continue;
|
||||
}
|
||||
if (allowFacialFallback) {
|
||||
normalizedGeosets.insert(sid);
|
||||
continue;
|
||||
}
|
||||
if (sid != selectedConnector) {
|
||||
// Keep fallback connector only when selected one does not exist on this model.
|
||||
if (sid != 101 || allGeosets.count(selectedConnector) > 0) {
|
||||
|
|
@ -4618,19 +4768,11 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
}
|
||||
}
|
||||
}
|
||||
// Group 2 carries facial hair variants.
|
||||
if (hasHumanoidExtra && group == 2) {
|
||||
const bool allowSelectedFacial200 = wantsFacialHair &&
|
||||
sid == selectedFacial200 && allGeosets.count(selectedFacial200) > 0;
|
||||
const bool allowFacial200Fallback = wantsFacialHair && (sid == 200 || sid == 201);
|
||||
if (!allowSelectedFacial200 && !allowFacial200Fallback) {
|
||||
continue;
|
||||
}
|
||||
// If selected variant exists, do not keep fallback variants.
|
||||
if (allowFacial200Fallback && allGeosets.count(selectedFacial200) > 0 &&
|
||||
sid != selectedFacial200) {
|
||||
continue;
|
||||
}
|
||||
// Group 2 carries facial-hair sub-pieces (mustache/chin/side details).
|
||||
// Keep all present group-2 pieces when facial hair is requested to avoid
|
||||
// dropping valid chin meshes due partial geoset mapping differences.
|
||||
if (hasHumanoidExtra && group == 2 && !wantsFacialHair) {
|
||||
continue;
|
||||
}
|
||||
normalizedGeosets.insert(sid);
|
||||
}
|
||||
|
|
@ -4659,10 +4801,16 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
}
|
||||
|
||||
// Some mustache/goatee variants are authored in facial group 3xx.
|
||||
// Re-add only the selected facial 3xx geoset (not generic 3xx arm meshes).
|
||||
// Re-add selected facial 3xx plus low-index facial fallbacks.
|
||||
if (hasHumanoidExtra && wantsFacialHair) {
|
||||
uint16_t facial300Sid = pickFromGroup(selectedFacial300, 3);
|
||||
// Prefer alt channel first (often chin-beard), then primary.
|
||||
uint16_t facial300Sid = pickFromGroup(selectedFacial300Alt, 3);
|
||||
if (facial300Sid == 0) facial300Sid = pickFromGroup(selectedFacial300, 3);
|
||||
if (facial300Sid != 0) normalizedGeosets.insert(facial300Sid);
|
||||
if (facial300Sid == 0) {
|
||||
if (allGeosets.count(300) > 0) normalizedGeosets.insert(300);
|
||||
else if (allGeosets.count(301) > 0) normalizedGeosets.insert(301);
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer trousers geoset, not robe/kilt overlays.
|
||||
|
|
@ -4761,6 +4909,10 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
}
|
||||
}
|
||||
|
||||
// Try attaching NPC held weapons; if update fields are not ready yet,
|
||||
// IN_GAME retry loop will attempt again shortly.
|
||||
bool weaponsAttachedNow = tryAttachCreatureVirtualWeapons(guid, instanceId);
|
||||
|
||||
// Spawn in the correct pose. If the server marked this creature dead before
|
||||
// the queued spawn was processed, start directly in death animation.
|
||||
if (deadCreatureGuids_.count(guid)) {
|
||||
|
|
@ -4774,6 +4926,13 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
creatureInstances_[guid] = instanceId;
|
||||
creatureModelIds_[guid] = modelId;
|
||||
creatureRenderPosCache_[guid] = renderPos;
|
||||
if (weaponsAttachedNow) {
|
||||
creatureWeaponsAttached_.insert(guid);
|
||||
creatureWeaponAttachAttempts_.erase(guid);
|
||||
} else {
|
||||
creatureWeaponsAttached_.erase(guid);
|
||||
creatureWeaponAttachAttempts_[guid] = 1;
|
||||
}
|
||||
LOG_DEBUG("Spawned creature: guid=0x", std::hex, guid, std::dec,
|
||||
" displayId=", displayId, " at (", x, ", ", y, ", ", z, ")");
|
||||
}
|
||||
|
|
@ -6003,6 +6162,8 @@ void Application::despawnOnlineCreature(uint64_t guid) {
|
|||
creatureInstances_.erase(it);
|
||||
creatureModelIds_.erase(guid);
|
||||
creatureRenderPosCache_.erase(guid);
|
||||
creatureWeaponsAttached_.erase(guid);
|
||||
creatureWeaponAttachAttempts_.erase(guid);
|
||||
|
||||
LOG_DEBUG("Despawned creature: guid=0x", std::hex, guid, std::dec);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue