diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index b54d3bac92..16aea9736b 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -19,7 +19,7 @@ set(GAME_HEADER source_group(game FILES ${GAME} ${GAME_HEADER}) add_openmw_dir (mwrender - actors objects renderingmanager animation rotatecontroller sky skyutil npcanimation vismask + actors objects renderingmanager animation rotatecontroller sky skyutil npcanimation esm4npcanimation vismask creatureanimation effectmanager util renderinginterface pathgrid rendermode weaponanimation screenshotmanager bulletdebugdraw globalmap characterpreview camera localmap water terrainstorage ripplesimulation renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover @@ -91,7 +91,7 @@ add_openmw_dir (mwphysics add_openmw_dir (mwclass classes activator creature npc weapon armor potion apparatus book clothing container door ingredient creaturelevlist itemlevlist light lockpick misc probe repair static actor bodypart - esm4base light4 + esm4base esm4npc light4 ) add_openmw_dir (mwmechanics diff --git a/apps/openmw/mwclass/classes.cpp b/apps/openmw/mwclass/classes.cpp index 392cc45b6e..1668799eba 100644 --- a/apps/openmw/mwclass/classes.cpp +++ b/apps/openmw/mwclass/classes.cpp @@ -33,7 +33,6 @@ #include "ingredient.hpp" #include "itemlevlist.hpp" #include "light.hpp" -#include "light4.hpp" #include "lockpick.hpp" #include "misc.hpp" #include "npc.hpp" @@ -44,6 +43,8 @@ #include "weapon.hpp" #include "esm4base.hpp" +#include "esm4npc.hpp" +#include "light4.hpp" namespace MWClass { @@ -72,23 +73,23 @@ namespace MWClass BodyPart::registerSelf(); ESM4Named::registerSelf(); - ESM4Named::registerSelf(); ESM4Named::registerSelf(); ESM4Named::registerSelf(); ESM4Named::registerSelf(); ESM4Named::registerSelf(); + ESM4Named::registerSelf(); ESM4Named::registerSelf(); ESM4Named::registerSelf(); ESM4Named::registerSelf(); ESM4Named::registerSelf(); ESM4Named::registerSelf(); + ESM4Light::registerSelf(); ESM4Named::registerSelf(); + ESM4Npc::registerSelf(); + ESM4Named::registerSelf(); ESM4Static::registerSelf(); ESM4Named::registerSelf(); ESM4Tree::registerSelf(); ESM4Named::registerSelf(); - ESM4Light::registerSelf(); - ESM4Actor::registerSelf(); - ESM4Actor::registerSelf(); } } diff --git a/apps/openmw/mwclass/esm4base.hpp b/apps/openmw/mwclass/esm4base.hpp index c59e8e1dc2..5c05d81692 100644 --- a/apps/openmw/mwclass/esm4base.hpp +++ b/apps/openmw/mwclass/esm4base.hpp @@ -1,6 +1,7 @@ #ifndef GAME_MWCLASS_ESM4BASE_H #define GAME_MWCLASS_ESM4BASE_H +#include #include #include #include @@ -9,6 +10,7 @@ #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/registeredclass.hpp" #include "classmodel.hpp" @@ -23,13 +25,40 @@ namespace MWClass void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics); MWGui::ToolTipInfo getToolTipInfo(std::string_view name, int count); + + // We don't handle ESM4 player stats yet, so for resolving levelled object we use an arbitrary number. + constexpr int sDefaultLevel = 5; + + template + const TargetRecord* resolveLevelled(const ESM::RefId& id, int level = sDefaultLevel) + { + if (id.empty()) + return nullptr; + const MWWorld::ESMStore* esmStore = MWBase::Environment::get().getESMStore(); + const auto& targetStore = esmStore->get(); + const TargetRecord* res = targetStore.search(id); + if (res) + return res; + const LevelledRecord* lvlRec = esmStore->get().search(id); + if (!lvlRec) + return nullptr; + for (const ESM4::LVLO& obj : lvlRec->mLvlObject) + { + ESM::RefId candidateId = ESM::FormId::fromUint32(obj.item); + if (candidateId == id) + continue; + const TargetRecord* candidate = resolveLevelled(candidateId, level); + if (candidate && (!res || obj.level <= level)) + res = candidate; + } + return res; + } } - // Base for all ESM4 Classes + // Base for many ESM4 Classes template class ESM4Base : public MWWorld::Class { - MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override { const MWWorld::LiveCellRef* ref = ptr.get(); @@ -104,14 +133,11 @@ namespace MWClass class ESM4Named : public MWWorld::RegisteredClass, ESM4Base> { public: - friend MWWorld::RegisteredClass>; - ESM4Named() : MWWorld::RegisteredClass>(Record::sRecordId) { } - public: bool hasToolTip(const MWWorld::ConstPtr& ptr) const override { return true; } MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override @@ -124,36 +150,6 @@ namespace MWClass return ptr.get()->mBase->mFullName; } }; - - template - class ESM4Actor : public MWWorld::RegisteredClass, ESM4Base> - { - public: - friend MWWorld::RegisteredClass>; - - ESM4Actor() - : MWWorld::RegisteredClass>(Record::sRecordId) - { - } - - void insertObjectPhysics( - const MWWorld::Ptr&, const std::string&, const osg::Quat&, MWPhysics::PhysicsSystem&) const override - { - } - - bool hasToolTip(const MWWorld::ConstPtr& ptr) const override { return true; } - - MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override - { - return ESM4Impl::getToolTipInfo(ptr.get()->mBase->mEditorId, count); - } - - std::string getModel(const MWWorld::ConstPtr& ptr) const override - { - // TODO: Implement actor rendering. This function will typically return the skeleton. - return {}; - } - }; } #endif // GAME_MWCLASS_ESM4BASE_H diff --git a/apps/openmw/mwclass/esm4npc.cpp b/apps/openmw/mwclass/esm4npc.cpp new file mode 100644 index 0000000000..78cbd89b50 --- /dev/null +++ b/apps/openmw/mwclass/esm4npc.cpp @@ -0,0 +1,168 @@ +#include "esm4npc.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "../mwworld/customdata.hpp" +#include "../mwworld/esmstore.hpp" + +#include "esm4base.hpp" + +namespace MWClass +{ + template + static std::vector withBaseTemplates( + const TargetRecord* rec, int level = MWClass::ESM4Impl::sDefaultLevel) + { + std::vector res{ rec }; + while (true) + { + const TargetRecord* newRec + = MWClass::ESM4Impl::resolveLevelled(rec->mBaseTemplate, level); + if (!newRec || newRec == rec) + return res; + res.push_back(rec = newRec); + } + } + + static const ESM4::Npc* chooseTemplate(const std::vector& recs, uint16_t flag) + { + // 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->mIsFONV || !(rec->mBaseConfig.tes5.templateFlags & flag)) + return rec; + return nullptr; + } + + class ESM4NpcCustomData : public MWWorld::TypedCustomData + { + public: + const ESM4::Npc* mTraits; + const ESM4::Npc* mBaseData; + const ESM4::Race* mRace; + bool mIsFemale; + + // TODO: Use InventoryStore instead (currently doesn't support ESM4 objects) + std::vector mEquippedArmor; + std::vector mEquippedClothing; + + ESM4NpcCustomData& asESM4NpcCustomData() override { return *this; } + const ESM4NpcCustomData& asESM4NpcCustomData() const override { return *this; } + }; + + ESM4NpcCustomData& ESM4Npc::getCustomData(const MWWorld::ConstPtr& ptr) + { + // 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(); + + const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore(); + auto npcRecs = withBaseTemplates(ptr.get()->mBase); + + data->mTraits = chooseTemplate(npcRecs, ESM4::Npc::TES5_UseTraits); + data->mBaseData = chooseTemplate(npcRecs, ESM4::Npc::TES5_UseBaseData); + + if (!data->mTraits) + throw std::runtime_error("ESM4 NPC traits not found"); + if (!data->mBaseData) + throw std::runtime_error("ESM4 NPC base data not found"); + + data->mRace = store->get().find(data->mTraits->mRace); + if (data->mTraits->mIsTES4) + data->mIsFemale = data->mTraits->mBaseConfig.tes4.flags & ESM4::Npc::TES4_Female; + else if (data->mTraits->mIsFONV) + data->mIsFemale = data->mTraits->mBaseConfig.fo3.flags & ESM4::Npc::FO3_Female; + else + data->mIsFemale = data->mTraits->mBaseConfig.tes5.flags & ESM4::Npc::TES5_Female; + + if (auto inv = chooseTemplate(npcRecs, ESM4::Npc::TES5_UseInventory)) + { + for (const ESM4::InventoryItem& item : inv->mInventory) + { + if (auto* armor + = ESM4Impl::resolveLevelled(ESM::FormId::fromUint32(item.item))) + data->mEquippedArmor.push_back(armor); + else if (data->mTraits->mIsTES4) + { + const auto* clothing = ESM4Impl::resolveLevelled( + ESM::FormId::fromUint32(item.item)); + if (clothing) + data->mEquippedClothing.push_back(clothing); + } + } + if (!inv->mDefaultOutfit.isZeroOrUnset()) + { + if (const ESM4::Outfit* outfit = store->get().search(inv->mDefaultOutfit)) + { + for (ESM::FormId itemId : outfit->mInventory) + if (auto* armor = ESM4Impl::resolveLevelled(itemId)) + data->mEquippedArmor.push_back(armor); + } + else + Log(Debug::Error) << "Outfit not found: " << ESM::RefId(inv->mDefaultOutfit); + } + } + + ESM4NpcCustomData& res = *data; + refData.setCustomData(std::move(data)); + return res; + } + + const std::vector& ESM4Npc::getEquippedArmor(const MWWorld::Ptr& ptr) + { + return getCustomData(ptr).mEquippedArmor; + } + + const std::vector& ESM4Npc::getEquippedClothing(const MWWorld::Ptr& ptr) + { + return getCustomData(ptr).mEquippedClothing; + } + + const ESM4::Npc* ESM4Npc::getTraitsRecord(const MWWorld::Ptr& ptr) + { + return getCustomData(ptr).mTraits; + } + + const ESM4::Race* ESM4Npc::getRace(const MWWorld::Ptr& ptr) + { + return getCustomData(ptr).mRace; + } + + bool ESM4Npc::isFemale(const MWWorld::Ptr& ptr) + { + return getCustomData(ptr).mIsFemale; + } + + std::string ESM4Npc::getModel(const MWWorld::ConstPtr& ptr) const + { + const ESM4NpcCustomData& data = getCustomData(ptr); + std::string_view model; + if (data.mTraits->mIsTES4) + model = data.mTraits->mModel; + else + 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 + { + return getCustomData(ptr).mBaseData->mFullName; + } +} diff --git a/apps/openmw/mwclass/esm4npc.hpp b/apps/openmw/mwclass/esm4npc.hpp new file mode 100644 index 0000000000..7830f20f32 --- /dev/null +++ b/apps/openmw/mwclass/esm4npc.hpp @@ -0,0 +1,71 @@ +#ifndef GAME_MWCLASS_ESM4ACTOR_H +#define GAME_MWCLASS_ESM4ACTOR_H + +#include +#include + +#include "../mwgui/tooltips.hpp" + +#include "../mwrender/objects.hpp" +#include "../mwrender/renderinginterface.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/class.hpp" +#include "../mwworld/registeredclass.hpp" + +#include "esm4base.hpp" + +namespace MWClass +{ + class ESM4Npc final : public MWWorld::RegisteredClass + { + public: + ESM4Npc() + : MWWorld::RegisteredClass(ESM4::Npc::sRecordId) + { + } + + MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override + { + const MWWorld::LiveCellRef* ref = ptr.get(); + return MWWorld::Ptr(cell.insert(ref), &cell); + } + + void insertObjectRendering(const MWWorld::Ptr& ptr, const std::string& model, + MWRender::RenderingInterface& renderingInterface) const override + { + renderingInterface.getObjects().insertNPC(ptr); + } + + void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const override + { + insertObjectPhysics(ptr, model, rotation, physics); + } + + void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, + MWPhysics::PhysicsSystem& physics) const override + { + // ESM4Impl::insertObjectPhysics(ptr, getModel(ptr), rotation, physics); + } + + bool hasToolTip(const MWWorld::ConstPtr& ptr) const override { return true; } + MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override + { + return ESM4Impl::getToolTipInfo(getName(ptr), count); + } + + std::string getModel(const MWWorld::ConstPtr& ptr) const override; + std::string_view getName(const MWWorld::ConstPtr& ptr) const override; + + static const ESM4::Npc* getTraitsRecord(const MWWorld::Ptr& ptr); + static const ESM4::Race* getRace(const MWWorld::Ptr& ptr); + static bool isFemale(const MWWorld::Ptr& ptr); + static const std::vector& getEquippedArmor(const MWWorld::Ptr& ptr); + static const std::vector& getEquippedClothing(const MWWorld::Ptr& ptr); + + private: + static ESM4NpcCustomData& getCustomData(const MWWorld::ConstPtr& ptr); + }; +} + +#endif // GAME_MWCLASS_ESM4ACTOR_H diff --git a/apps/openmw/mwrender/esm4npcanimation.cpp b/apps/openmw/mwrender/esm4npcanimation.cpp new file mode 100644 index 0000000000..1f06e68bc2 --- /dev/null +++ b/apps/openmw/mwrender/esm4npcanimation.cpp @@ -0,0 +1,187 @@ +#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) + { + 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); + } +} diff --git a/apps/openmw/mwrender/esm4npcanimation.hpp b/apps/openmw/mwrender/esm4npcanimation.hpp new file mode 100644 index 0000000000..7bb3fe1103 --- /dev/null +++ b/apps/openmw/mwrender/esm4npcanimation.hpp @@ -0,0 +1,26 @@ +#ifndef GAME_RENDER_ESM4NPCANIMATION_H +#define GAME_RENDER_ESM4NPCANIMATION_H + +#include "animation.hpp" + +namespace MWRender +{ + class ESM4NpcAnimation : public Animation + { + public: + ESM4NpcAnimation( + const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem); + + 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(); + }; +} + +#endif // GAME_RENDER_ESM4NPCANIMATION_H diff --git a/apps/openmw/mwrender/objects.cpp b/apps/openmw/mwrender/objects.cpp index b8b7d62309..89e192f6c8 100644 --- a/apps/openmw/mwrender/objects.cpp +++ b/apps/openmw/mwrender/objects.cpp @@ -13,6 +13,7 @@ #include "animation.hpp" #include "creatureanimation.hpp" +#include "esm4npcanimation.hpp" #include "npcanimation.hpp" #include "vismask.hpp" @@ -116,13 +117,22 @@ namespace MWRender insertBegin(ptr); ptr.getRefData().getBaseNode()->setNodeMask(Mask_Actor); - osg::ref_ptr anim( - new NpcAnimation(ptr, osg::ref_ptr(ptr.getRefData().getBaseNode()), mResourceSystem)); - - if (mObjects.emplace(ptr.mRef, anim).second) + if (ptr.getType() == ESM::REC_NPC_4) { - ptr.getClass().getInventoryStore(ptr).setInvListener(anim.get()); - ptr.getClass().getInventoryStore(ptr).setContListener(anim.get()); + osg::ref_ptr anim( + new ESM4NpcAnimation(ptr, osg::ref_ptr(ptr.getRefData().getBaseNode()), mResourceSystem)); + mObjects.emplace(ptr.mRef, anim); + } + else + { + osg::ref_ptr anim( + new NpcAnimation(ptr, osg::ref_ptr(ptr.getRefData().getBaseNode()), mResourceSystem)); + + if (mObjects.emplace(ptr.mRef, anim).second) + { + ptr.getClass().getInventoryStore(ptr).setInvListener(anim.get()); + ptr.getClass().getInventoryStore(ptr).setContListener(anim.get()); + } } } diff --git a/apps/openmw/mwworld/customdata.cpp b/apps/openmw/mwworld/customdata.cpp index 395d230a1a..db340302ce 100644 --- a/apps/openmw/mwworld/customdata.cpp +++ b/apps/openmw/mwworld/customdata.cpp @@ -77,4 +77,17 @@ namespace MWWorld throw std::logic_error(error.str()); } + MWClass::ESM4NpcCustomData& CustomData::asESM4NpcCustomData() + { + std::stringstream error; + error << "bad cast " << typeid(this).name() << " to ESM4NpcCustomData"; + throw std::logic_error(error.str()); + } + + const MWClass::ESM4NpcCustomData& CustomData::asESM4NpcCustomData() const + { + std::stringstream error; + error << "bad cast " << typeid(this).name() << " to ESM4NpcCustomData"; + throw std::logic_error(error.str()); + } } diff --git a/apps/openmw/mwworld/customdata.hpp b/apps/openmw/mwworld/customdata.hpp index 9d9283f085..8051876309 100644 --- a/apps/openmw/mwworld/customdata.hpp +++ b/apps/openmw/mwworld/customdata.hpp @@ -6,6 +6,7 @@ namespace MWClass { class CreatureCustomData; + class ESM4NpcCustomData; class NpcCustomData; class ContainerCustomData; class DoorCustomData; @@ -38,6 +39,9 @@ namespace MWWorld virtual MWClass::CreatureLevListCustomData& asCreatureLevListCustomData(); virtual const MWClass::CreatureLevListCustomData& asCreatureLevListCustomData() const; + + virtual MWClass::ESM4NpcCustomData& asESM4NpcCustomData(); + virtual const MWClass::ESM4NpcCustomData& asESM4NpcCustomData() const; }; template diff --git a/components/esm4/loadarma.cpp b/components/esm4/loadarma.cpp index 3390e59828..2bb6240ee8 100644 --- a/components/esm4/loadarma.cpp +++ b/components/esm4/loadarma.cpp @@ -95,6 +95,22 @@ void ESM4::ArmorAddon::load(ESM4::Reader& reader) break; case ESM4::SUB_DNAM: + if (subHdr.dataSize == 12) + { + std::uint16_t unknownInt16; + std::uint8_t unknownInt8; + reader.get(mMalePriority); + reader.get(mFemalePriority); + reader.get(mWeightSliderMale); + reader.get(mWeightSliderFemale); + reader.get(unknownInt16); + reader.get(mDetectionSoundValue); + reader.get(unknownInt8); + reader.get(mWeaponAdjust); + } + else + reader.skipSubRecordData(); + break; case ESM4::SUB_MO2T: // FIXME: should group with MOD2 case ESM4::SUB_MO2S: // FIXME: should group with MOD2 case ESM4::SUB_MO2C: // FIXME: should group with MOD2 diff --git a/components/esm4/loadarma.hpp b/components/esm4/loadarma.hpp index 24fa8b1093..6733184aa8 100644 --- a/components/esm4/loadarma.hpp +++ b/components/esm4/loadarma.hpp @@ -59,6 +59,16 @@ namespace ESM4 BodyTemplate mBodyTemplate; // TES5 + std::uint8_t mMalePriority = 0; + std::uint8_t mFemalePriority = 0; + + // Flag 0x2 in mWeightSlider means that there are 2 world models for different weights: _0.nif and _1.nif + std::uint8_t mWeightSliderMale = 0; + std::uint8_t mWeightSliderFemale = 0; + + std::uint8_t mDetectionSoundValue = 0; + float mWeaponAdjust = 0; + void load(ESM4::Reader& reader); // void save(ESM4::Writer& writer) const; diff --git a/components/esm4/loadhdpt.cpp b/components/esm4/loadhdpt.cpp index 7592db5486..250a687042 100644 --- a/components/esm4/loadhdpt.cpp +++ b/components/esm4/loadhdpt.cpp @@ -88,6 +88,8 @@ void ESM4::HeadPart::load(ESM4::Reader& reader) reader.getFormId(mBaseTexture); break; case ESM4::SUB_PNAM: + reader.get(mType); + break; case ESM4::SUB_MODT: // Model data case ESM4::SUB_MODC: case ESM4::SUB_MODS: diff --git a/components/esm4/loadhdpt.hpp b/components/esm4/loadhdpt.hpp index 6f08d72961..aca3b0ca7b 100644 --- a/components/esm4/loadhdpt.hpp +++ b/components/esm4/loadhdpt.hpp @@ -49,6 +49,26 @@ namespace ESM4 std::string mModel; std::uint8_t mData; + std::uint32_t mType; + + enum Type : std::uint32_t + { + Type_Misc = 0, + Type_Face = 1, + Type_Eyes = 2, + Type_Hair = 3, + Type_FacialHair = 4, + Type_Scar = 5, + Type_Eyebrows = 6, + // FO4+ + Type_Meatcaps = 7, + Type_Teeth = 8, + Type_HeadRear = 9, + // Starfield + // 10 and 11 are unknown + Type_LeftEye = 12, + Type_Eyelashes = 13, + }; ESM::FormId mAdditionalPart; diff --git a/components/misc/resourcehelpers.cpp b/components/misc/resourcehelpers.cpp index cddcbe463b..ce552df4f7 100644 --- a/components/misc/resourcehelpers.cpp +++ b/components/misc/resourcehelpers.cpp @@ -159,9 +159,11 @@ std::string Misc::ResourceHelpers::correctActorModelPath(const std::string& resP return mdlname; } -std::string Misc::ResourceHelpers::correctMeshPath(const std::string& resPath, const VFS::Manager* vfs) +std::string Misc::ResourceHelpers::correctMeshPath(std::string_view resPath, const VFS::Manager* vfs) { - return "meshes\\" + resPath; + std::string res = "meshes\\"; + res.append(resPath); + return res; } std::string Misc::ResourceHelpers::correctSoundPath(const std::string& resPath) diff --git a/components/misc/resourcehelpers.hpp b/components/misc/resourcehelpers.hpp index 37932ea155..478569ed14 100644 --- a/components/misc/resourcehelpers.hpp +++ b/components/misc/resourcehelpers.hpp @@ -33,7 +33,7 @@ namespace Misc std::string correctActorModelPath(const std::string& resPath, const VFS::Manager* vfs); // Adds "meshes\\". - std::string correctMeshPath(const std::string& resPath, const VFS::Manager* vfs); + std::string correctMeshPath(std::string_view resPath, const VFS::Manager* vfs); // Adds "sound\\". std::string correctSoundPath(const std::string& resPath);