diff --git a/apps/openmw/mwclass/esm4npc.cpp b/apps/openmw/mwclass/esm4npc.cpp index c5857c489a..78cbd89b50 100644 --- a/apps/openmw/mwclass/esm4npc.cpp +++ b/apps/openmw/mwclass/esm4npc.cpp @@ -34,11 +34,10 @@ namespace MWClass static const ESM4::Npc* chooseTemplate(const std::vector& recs, uint16_t flag) { - // If the record is neither TES4 nor TES5 (though maybe FO4 is compatible with tes5.templateFlags), then - // the function can return nullptr that will lead to "ESM4 NPC traits not found" exception and the NPC - // will not be added to the scene. But in any way it shouldn't cause a crash. + // In case of FO3 the function may return nullptr that will lead to "ESM4 NPC traits not found" + // exception and the NPC will not be added to the scene. But in any way it shouldn't cause a crash. for (const auto* rec : recs) - if (rec->mIsTES4 || !(rec->mBaseConfig.tes5.templateFlags & flag)) + if (rec->mIsTES4 || rec->mIsFONV || !(rec->mBaseConfig.tes5.templateFlags & flag)) return rec; return nullptr; } @@ -59,9 +58,16 @@ namespace MWClass const ESM4NpcCustomData& asESM4NpcCustomData() const override { return *this; } }; - ESM4NpcCustomData& ESM4Npc::getCustomData(const MWWorld::Ptr& ptr) + ESM4NpcCustomData& ESM4Npc::getCustomData(const MWWorld::ConstPtr& ptr) { - if (auto* data = ptr.getRefData().getCustomData()) + // Note: the argument is ConstPtr because this function is used in `getModel` and `getName` + // which are virtual and work with ConstPtr. `getModel` and `getName` use custom data + // because they require a lot of work including levelled records resolving and it would be + // stupid to not to cache the results. Maybe we should stop using ConstPtr at all + // to avoid such workarounds. + MWWorld::RefData& refData = const_cast(ptr.getRefData()); + + if (auto* data = refData.getCustomData()) return data->asESM4NpcCustomData(); auto data = std::make_unique(); @@ -114,7 +120,7 @@ namespace MWClass } ESM4NpcCustomData& res = *data; - ptr.getRefData().setCustomData(std::move(data)); + refData.setCustomData(std::move(data)); return res; } @@ -145,23 +151,18 @@ namespace MWClass std::string ESM4Npc::getModel(const MWWorld::ConstPtr& ptr) const { - if (!ptr.getRefData().getCustomData()) - return ""; - const ESM4NpcCustomData& data = ptr.getRefData().getCustomData()->asESM4NpcCustomData(); - const VFS::Manager* vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + const ESM4NpcCustomData& data = getCustomData(ptr); + std::string_view model; if (data.mTraits->mIsTES4) - return Misc::ResourceHelpers::correctMeshPath(data.mTraits->mModel, vfs); - if (data.mIsFemale) - return Misc::ResourceHelpers::correctMeshPath(data.mRace->mModelFemale, vfs); + model = data.mTraits->mModel; else - return Misc::ResourceHelpers::correctMeshPath(data.mRace->mModelMale, vfs); + model = data.mIsFemale ? data.mRace->mModelFemale : data.mRace->mModelMale; + const VFS::Manager* vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); + return Misc::ResourceHelpers::correctMeshPath(model, vfs); } std::string_view ESM4Npc::getName(const MWWorld::ConstPtr& ptr) const { - if (!ptr.getRefData().getCustomData()) - return ""; - const ESM4NpcCustomData& data = ptr.getRefData().getCustomData()->asESM4NpcCustomData(); - return data.mBaseData->mFullName; + return getCustomData(ptr).mBaseData->mFullName; } } diff --git a/apps/openmw/mwclass/esm4npc.hpp b/apps/openmw/mwclass/esm4npc.hpp index efa5a48ba6..7830f20f32 100644 --- a/apps/openmw/mwclass/esm4npc.hpp +++ b/apps/openmw/mwclass/esm4npc.hpp @@ -64,7 +64,7 @@ namespace MWClass static const std::vector& getEquippedClothing(const MWWorld::Ptr& ptr); private: - static ESM4NpcCustomData& getCustomData(const MWWorld::Ptr& ptr); + static ESM4NpcCustomData& getCustomData(const MWWorld::ConstPtr& ptr); }; } diff --git a/apps/openmw/mwrender/esm4npcanimation.cpp b/apps/openmw/mwrender/esm4npcanimation.cpp index a7e311b388..1f06e68bc2 100644 --- a/apps/openmw/mwrender/esm4npcanimation.cpp +++ b/apps/openmw/mwrender/esm4npcanimation.cpp @@ -31,13 +31,19 @@ namespace MWRender if (!mObjectRoot.get()) return; const ESM4::Npc* traits = MWClass::ESM4Npc::getTraitsRecord(mPtr); - // There is no flag "mIsTES5", so we can not distinguish from other cases. - // But calling wrong `updateParts*` function shouldn't crash the game and will - // only lead to the NPC not being rendered. 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) @@ -76,6 +82,8 @@ namespace MWRender 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)) @@ -84,6 +92,25 @@ namespace MWRender 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(); @@ -92,23 +119,6 @@ namespace MWRender const ESM4::Race* race = MWClass::ESM4Npc::getRace(mPtr); bool isFemale = MWClass::ESM4Npc::isFemale(mPtr); - std::set usedHeadPartTypes; - auto addHeadParts = [&](const std::vector& partIds) { - 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); - } - }; - std::vector armorAddons; auto findArmorAddons = [&](const ESM4::Armor* armor) { @@ -132,9 +142,19 @@ namespace MWRender for (const ESM4::Armor* armor : MWClass::ESM4Npc::getEquippedArmor(mPtr)) findArmorAddons(armor); if (!traits->mWornArmor.isZeroOrUnset()) - findArmorAddons(store->get().find(traits->mWornArmor)); + { + 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()) - findArmorAddons(store->get().find(race->mSkin)); + { + 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(), @@ -158,9 +178,10 @@ namespace MWRender } } + std::set usedHeadPartTypes; if (usedParts & ESM4::Armor::TES5_Hair) usedHeadPartTypes.insert(ESM4::HeadPart::Type_Hair); - addHeadParts(traits->mHeadParts); - addHeadParts(isFemale ? race->mHeadPartIdsFemale : race->mHeadPartIdsMale); + insertHeadParts(traits->mHeadParts, usedHeadPartTypes); + insertHeadParts(isFemale ? race->mHeadPartIdsFemale : race->mHeadPartIdsMale, usedHeadPartTypes); } } diff --git a/apps/openmw/mwrender/esm4npcanimation.hpp b/apps/openmw/mwrender/esm4npcanimation.hpp index 1d93dace45..7bb3fe1103 100644 --- a/apps/openmw/mwrender/esm4npcanimation.hpp +++ b/apps/openmw/mwrender/esm4npcanimation.hpp @@ -14,6 +14,9 @@ namespace MWRender private: void insertPart(std::string_view model); + // Works for FO3/FONV/TES5 + void insertHeadParts(const std::vector& partIds, std::set& usedHeadPartTypes); + void updateParts(); void updatePartsTES4(); void updatePartsTES5();