#include "esm4npcanimation.hpp" #include #include #include #include #include #include #include #include #include #include #include "../mwclass/esm4npc.hpp" #include "../mwworld/customdata.hpp" #include "../mwworld/esmstore.hpp" namespace MWRender { ESM4NpcAnimation::ESM4NpcAnimation( const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem) : Animation(ptr, std::move(parentNode), resourceSystem) { setObjectRoot(mPtr.getClass().getModel(mPtr), true, true, false); updateParts(); } void ESM4NpcAnimation::updateParts() { if (!mObjectRoot.get()) return; const ESM4::Npc* traits = MWClass::ESM4Npc::getTraitsRecord(mPtr); if (traits->mIsTES4) updatePartsTES4(); else if (traits->mIsFONV) { // Not implemented yet } else { // There is no easy way to distinguish TES5 and FO3. // In case of FO3 the function shouldn't crash the game and will // only lead to the NPC not being rendered. updatePartsTES5(); } } void ESM4NpcAnimation::insertPart(std::string_view model) { if (model.empty()) return; mResourceSystem->getSceneManager()->getInstance( Misc::ResourceHelpers::correctMeshPath(model, mResourceSystem->getVFS()), mObjectRoot.get()); } template static std::string_view chooseTes4EquipmentModel(const Record* rec, bool isFemale) { if (isFemale && !rec->mModelFemale.empty()) return rec->mModelFemale; else if (!isFemale && !rec->mModelMale.empty()) return rec->mModelMale; else return rec->mModel; } void ESM4NpcAnimation::updatePartsTES4() { const ESM4::Npc* traits = MWClass::ESM4Npc::getTraitsRecord(mPtr); const ESM4::Race* race = MWClass::ESM4Npc::getRace(mPtr); bool isFemale = MWClass::ESM4Npc::isFemale(mPtr); // TODO: Body and head parts are placed incorrectly, need to attach to bones for (const ESM4::Race::BodyPart& bodyPart : (isFemale ? race->mBodyPartsFemale : race->mBodyPartsMale)) insertPart(bodyPart.mesh); for (const ESM4::Race::BodyPart& bodyPart : race->mHeadParts) insertPart(bodyPart.mesh); if (!traits->mHair.isZeroOrUnset()) { const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore(); if (const ESM4::Hair* hair = store->get().search(traits->mHair)) insertPart(hair->mModel); else Log(Debug::Error) << "Hair not found: " << ESM::RefId(traits->mHair); } for (const ESM4::Armor* armor : MWClass::ESM4Npc::getEquippedArmor(mPtr)) insertPart(chooseTes4EquipmentModel(armor, isFemale)); for (const ESM4::Clothing* clothing : MWClass::ESM4Npc::getEquippedClothing(mPtr)) insertPart(chooseTes4EquipmentModel(clothing, isFemale)); } void ESM4NpcAnimation::insertHeadParts( const std::vector& partIds, std::set& usedHeadPartTypes) { const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore(); for (ESM::FormId partId : partIds) { if (partId.isZeroOrUnset()) continue; const ESM4::HeadPart* part = store->get().search(partId); if (!part) { Log(Debug::Error) << "Head part not found: " << ESM::RefId(partId); continue; } if (usedHeadPartTypes.emplace(part->mType).second) insertPart(part->mModel); } } void ESM4NpcAnimation::updatePartsTES5() { const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore(); const ESM4::Npc* traits = MWClass::ESM4Npc::getTraitsRecord(mPtr); const ESM4::Race* race = MWClass::ESM4Npc::getRace(mPtr); bool isFemale = MWClass::ESM4Npc::isFemale(mPtr); std::vector armorAddons; auto findArmorAddons = [&](const ESM4::Armor* armor) { for (ESM::FormId armaId : armor->mAddOns) { if (armaId.isZeroOrUnset()) continue; const ESM4::ArmorAddon* arma = store->get().search(armaId); if (!arma) { Log(Debug::Error) << "ArmorAddon not found: " << ESM::RefId(armaId); continue; } bool compatibleRace = arma->mRacePrimary == traits->mRace; for (auto r : arma->mRaces) if (r == traits->mRace) compatibleRace = true; if (compatibleRace) armorAddons.push_back(arma); } }; for (const ESM4::Armor* armor : MWClass::ESM4Npc::getEquippedArmor(mPtr)) findArmorAddons(armor); if (!traits->mWornArmor.isZeroOrUnset()) { if (const ESM4::Armor* armor = store->get().search(traits->mWornArmor)) findArmorAddons(armor); else Log(Debug::Error) << "Worn armor not found: " << ESM::RefId(traits->mWornArmor); } if (!race->mSkin.isZeroOrUnset()) { if (const ESM4::Armor* armor = store->get().search(race->mSkin)) findArmorAddons(armor); else Log(Debug::Error) << "Skin not found: " << ESM::RefId(race->mSkin); } if (isFemale) std::sort(armorAddons.begin(), armorAddons.end(), [](auto x, auto y) { return x->mFemalePriority > y->mFemalePriority; }); else std::sort(armorAddons.begin(), armorAddons.end(), [](auto x, auto y) { return x->mMalePriority > y->mMalePriority; }); uint32_t usedParts = 0; for (const ESM4::ArmorAddon* arma : armorAddons) { const uint32_t covers = arma->mBodyTemplate.bodyPart; // if body is already covered, skip to avoid clipping if (covers & usedParts & ESM4::Armor::TES5_Body) continue; // if covers at least something that wasn't covered before - add model if (covers & ~usedParts) { usedParts |= covers; insertPart(isFemale ? arma->mModelFemale : arma->mModelMale); } } std::set usedHeadPartTypes; if (usedParts & ESM4::Armor::TES5_Hair) usedHeadPartTypes.insert(ESM4::HeadPart::Type_Hair); insertHeadParts(traits->mHeadParts, usedHeadPartTypes); insertHeadParts(isFemale ? race->mHeadPartIdsFemale : race->mHeadPartIdsMale, usedHeadPartTypes); } }