diff --git a/CHANGELOG.md b/CHANGELOG.md index bd8bf9f48..e47b8ee2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -209,6 +209,7 @@ Feature #5132: Unique animations for different weapon types Feature #5146: Safe Dispose corpse Feature #5147: Show spell magicka cost in spell buying window + Feature #5193: Weapon sheathing Task #4686: Upgrade media decoder to a more current FFmpeg API Task #4695: Optimize Distant Terrain memory consumption Task #4789: Optimize cell transitions diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 2dbbfea35..4b9287f31 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -985,7 +985,11 @@ void CharacterController::handleTextKey(const std::string &groupname, const std: size_t off = groupname.size()+2; size_t len = evt.size() - off; - if(evt.compare(off, len, "equip attach") == 0) + if(groupname == "shield" && evt.compare(off, len, "equip attach") == 0) + mAnimation->showCarriedLeft(true); + else if(groupname == "shield" && evt.compare(off, len, "unequip detach") == 0) + mAnimation->showCarriedLeft(false); + else if(evt.compare(off, len, "equip attach") == 0) mAnimation->showWeapons(true); else if(evt.compare(off, len, "unequip detach") == 0) mAnimation->showWeapons(false); @@ -1193,7 +1197,7 @@ bool CharacterController::updateCarriedLeftVisible(const int weaptype) const // Shields/torches shouldn't be visible during any operation involving two hands // There seems to be no text keys for this purpose, except maybe for "[un]equip start/stop", // but they are also present in weapon drawing animation. - return !(getWeaponType(weaptype)->mFlags & ESM::WeaponType::TwoHanded); + return mAnimation->updateCarriedLeftVisible(weaptype); } bool CharacterController::updateWeaponState(CharacterState& idle) @@ -1275,8 +1279,19 @@ bool CharacterController::updateWeaponState(CharacterState& idle) { // Note: we do not disable unequipping animation automatically to avoid body desync weapgroup = getWeaponAnimation(mWeaponType); - mAnimation->play(weapgroup, priorityWeapon, - MWRender::Animation::BlendMask_All, false, + int unequipMask = MWRender::Animation::BlendMask_All; + bool useShieldAnims = mAnimation->useShieldAnimations(); + if (useShieldAnims && mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell && !(mWeaponType == ESM::Weapon::None && weaptype == ESM::Weapon::Spell)) + { + unequipMask = unequipMask |~MWRender::Animation::BlendMask_LeftArm; + mAnimation->play("shield", Priority_Block, + MWRender::Animation::BlendMask_LeftArm, true, + 1.0f, "unequip start", "unequip stop", 0.0f, 0); + } + else if (mWeaponType == ESM::Weapon::HandToHand) + mAnimation->showCarriedLeft(false); + + mAnimation->play(weapgroup, priorityWeapon, unequipMask, false, 1.0f, "unequip start", "unequip stop", 0.0f, 0); mUpperBodyState = UpperCharState_UnEquipingWeap; @@ -1301,7 +1316,10 @@ bool CharacterController::updateWeaponState(CharacterState& idle) if (weaptype != mWeaponType) { forcestateupdate = true; - mAnimation->showCarriedLeft(updateCarriedLeftVisible(weaptype)); + bool useShieldAnims = mAnimation->useShieldAnimations(); + if (!useShieldAnims) + mAnimation->showCarriedLeft(updateCarriedLeftVisible(weaptype)); + weapgroup = getWeaponAnimation(weaptype); // Note: controllers for ranged weapon should use time for beginning of animation to play shooting properly, @@ -1316,8 +1334,16 @@ bool CharacterController::updateWeaponState(CharacterState& idle) if (weaptype != ESM::Weapon::None) { mAnimation->showWeapons(false); - mAnimation->play(weapgroup, priorityWeapon, - MWRender::Animation::BlendMask_All, true, + int equipMask = MWRender::Animation::BlendMask_All; + if (useShieldAnims && weaptype != ESM::Weapon::Spell) + { + equipMask = equipMask |~MWRender::Animation::BlendMask_LeftArm; + mAnimation->play("shield", Priority_Block, + MWRender::Animation::BlendMask_LeftArm, true, + 1.0f, "equip start", "equip stop", 0.0f, 0); + } + + mAnimation->play(weapgroup, priorityWeapon, equipMask, true, 1.0f, "equip start", "equip stop", 0.0f, 0); mUpperBodyState = UpperCharState_EquipingWeap; diff --git a/apps/openmw/mwrender/actoranimation.cpp b/apps/openmw/mwrender/actoranimation.cpp index d05215b72..eac6c7a44 100644 --- a/apps/openmw/mwrender/actoranimation.cpp +++ b/apps/openmw/mwrender/actoranimation.cpp @@ -62,6 +62,7 @@ ActorAnimation::~ActorAnimation() } mScabbard.reset(); + mHolsteredShield.reset(); } PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::string& bonename, bool enchantedGlow, osg::Vec4f* glowColor) @@ -83,6 +84,163 @@ PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::st return PartHolderPtr(new PartHolder(instance)); } +std::string ActorAnimation::getShieldMesh(MWWorld::ConstPtr shield) const +{ + std::string mesh = shield.getClass().getModel(shield); + std::string holsteredName = mesh; + holsteredName = holsteredName.replace(holsteredName.size()-4, 4, "_sh.nif"); + if(mResourceSystem->getVFS()->exists(holsteredName)) + { + osg::ref_ptr shieldTemplate = mResourceSystem->getSceneManager()->getInstance(holsteredName); + SceneUtil::FindByNameVisitor findVisitor ("Bip01 Sheath"); + shieldTemplate->accept(findVisitor); + osg::ref_ptr sheathNode = findVisitor.mFoundNode; + if(!sheathNode) + return std::string(); + } + + return mesh; +} + +bool ActorAnimation::updateCarriedLeftVisible(const int weaptype) const +{ + static const bool shieldSheathing = Settings::Manager::getBool("shield sheathing", "Game"); + if (shieldSheathing) + { + const MWWorld::Class &cls = mPtr.getClass(); + MWMechanics::CreatureStats &stats = cls.getCreatureStats(mPtr); + if (cls.hasInventoryStore(mPtr) && weaptype != ESM::Weapon::Spell) + { + const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); + const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); + if (shield != inv.end() && shield->getTypeName() == typeid(ESM::Armor).name() && !getShieldMesh(*shield).empty()) + { + if(stats.getDrawState() != MWMechanics::DrawState_Weapon) + return false; + + if (weapon != inv.end()) + { + const std::string &type = weapon->getTypeName(); + if(type == typeid(ESM::Weapon).name()) + { + const MWWorld::LiveCellRef *ref = weapon->get(); + ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; + return !(MWMechanics::getWeaponType(weaponType)->mFlags & ESM::WeaponType::TwoHanded); + } + else if (type == typeid(ESM::Lockpick).name() || type == typeid(ESM::Probe).name()) + return true; + } + } + } + } + + return !(MWMechanics::getWeaponType(weaptype)->mFlags & ESM::WeaponType::TwoHanded); +} + +void ActorAnimation::updateHolsteredShield(bool showCarriedLeft) +{ + static const bool shieldSheathing = Settings::Manager::getBool("shield sheathing", "Game"); + if (!shieldSheathing) + return; + + if (!mPtr.getClass().hasInventoryStore(mPtr)) + return; + + mHolsteredShield.reset(); + + if (showCarriedLeft) + return; + + const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); + if (shield == inv.end() || shield->getTypeName() != typeid(ESM::Armor).name()) + return; + + // Can not show holdstered shields with two-handed weapons at all + const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if(weapon == inv.end()) + return; + + const std::string &type = weapon->getTypeName(); + if(type == typeid(ESM::Weapon).name()) + { + const MWWorld::LiveCellRef *ref = weapon->get(); + ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; + if (MWMechanics::getWeaponType(weaponType)->mFlags & ESM::WeaponType::TwoHanded) + return; + } + + std::string mesh = getShieldMesh(*shield); + if (mesh.empty()) + return; + + std::string boneName = "Bip01 AttachShield"; + osg::Vec4f glowColor = shield->getClass().getEnchantmentColor(*shield); + std::string holsteredName = mesh; + holsteredName = holsteredName.replace(holsteredName.size()-4, 4, "_sh.nif"); + bool isEnchanted = !shield->getClass().getEnchantment(*shield).empty(); + + // If we have no dedicated sheath model, use basic shield model as fallback. + if (!mResourceSystem->getVFS()->exists(holsteredName)) + mHolsteredShield = attachMesh(mesh, boneName, isEnchanted, &glowColor); + else + mHolsteredShield = attachMesh(holsteredName, boneName, isEnchanted, &glowColor); + + if (!mHolsteredShield) + return; + + SceneUtil::FindByNameVisitor findVisitor ("Bip01 Sheath"); + mHolsteredShield->getNode()->accept(findVisitor); + osg::Group* shieldNode = findVisitor.mFoundNode; + + // If mesh author declared an empty sheath node, use transformation from this node, but use the common shield mesh. + // This approach allows to tweak shield position without need to store the whole shield mesh in the _sh file. + if (shieldNode && !shieldNode->getNumChildren()) + { + osg::ref_ptr fallbackNode = mResourceSystem->getSceneManager()->getInstance(mesh, shieldNode); + if (isEnchanted) + SceneUtil::addEnchantedGlow(shieldNode, mResourceSystem, glowColor); + } + + if (mAlpha != 1.f) + mResourceSystem->getSceneManager()->recreateShaders(mHolsteredShield->getNode()); +} + +bool ActorAnimation::useShieldAnimations() const +{ + static const bool shieldSheathing = Settings::Manager::getBool("shield sheathing", "Game"); + if (!shieldSheathing) + return false; + + const MWWorld::Class &cls = mPtr.getClass(); + if (!cls.hasInventoryStore(mPtr)) + return false; + + if (getTextKeyTime("shield: equip attach") < 0 || getTextKeyTime("shield: unequip detach") < 0) + return false; + + const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr); + const MWWorld::ConstContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + const MWWorld::ConstContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); + if (weapon != inv.end() && shield != inv.end() && + shield->getTypeName() == typeid(ESM::Armor).name() && + !getShieldMesh(*shield).empty()) + { + const std::string &type = weapon->getTypeName(); + if(type == typeid(ESM::Weapon).name()) + { + const MWWorld::LiveCellRef *ref = weapon->get(); + ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; + return !(MWMechanics::getWeaponType(weaponType)->mFlags & ESM::WeaponType::TwoHanded); + } + else if (type == typeid(ESM::Lockpick).name() || type == typeid(ESM::Probe).name()) + return true; + } + + return false; +} + osg::Group* ActorAnimation::getBoneByName(const std::string& boneName) { if (!mObjectRoot) diff --git a/apps/openmw/mwrender/actoranimation.hpp b/apps/openmw/mwrender/actoranimation.hpp index 038dcde6d..14c687a5d 100644 --- a/apps/openmw/mwrender/actoranimation.hpp +++ b/apps/openmw/mwrender/actoranimation.hpp @@ -38,11 +38,15 @@ class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener virtual void itemAdded(const MWWorld::ConstPtr& item, int count); virtual void itemRemoved(const MWWorld::ConstPtr& item, int count); virtual bool isArrowAttached() const { return false; } + virtual bool useShieldAnimations() const; + bool updateCarriedLeftVisible(const int weaptype) const; protected: osg::Group* getBoneByName(const std::string& boneName); virtual void updateHolsteredWeapon(bool showHolsteredWeapons); + virtual void updateHolsteredShield(bool showCarriedLeft); virtual void updateQuiver(); + virtual std::string getShieldMesh(MWWorld::ConstPtr shield) const; virtual std::string getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon); virtual PartHolderPtr attachMesh(const std::string& model, const std::string& bonename, bool enchantedGlow, osg::Vec4f* glowColor); virtual PartHolderPtr attachMesh(const std::string& model, const std::string& bonename) @@ -52,6 +56,7 @@ class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener }; PartHolderPtr mScabbard; + PartHolderPtr mHolsteredShield; private: void addHiddenItemLight(const MWWorld::ConstPtr& item, const ESM::Light* esmLight); diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 763c7a917..b4d4ac664 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -153,6 +153,8 @@ public: void setTextKeyListener(TextKeyListener* listener); + virtual bool updateCarriedLeftVisible(const int weaptype) const { return false; }; + protected: class AnimationTime : public SceneUtil::ControllerSource { @@ -453,6 +455,7 @@ public: /// @note The matching is case-insensitive. const osg::Node* getNode(const std::string& name) const; + virtual bool useShieldAnimations() const { return false; } virtual void showWeapons(bool showWeapon) {} virtual void showCarriedLeft(bool show) {} virtual void setWeaponGroup(const std::string& group, bool relativeDuration) {} diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index 6bece05ec..baa695cda 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -90,6 +90,7 @@ void CreatureWeaponAnimation::updateParts() updateHolsteredWeapon(!mShowWeapons); updateQuiver(); + updateHolsteredShield(mShowCarriedLeft); if (mShowWeapons) updatePart(mWeapon, MWWorld::InventoryStore::Slot_CarriedRight); diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index d56ac9bd0..cde3b3041 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -24,6 +24,8 @@ #include // TextKeyMapHolder +#include + #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" @@ -511,6 +513,55 @@ void NpcAnimation::updateNpcBase() mWeaponAnimationTime->updateStartTime(); } +std::string NpcAnimation::getShieldMesh(MWWorld::ConstPtr shield) const +{ + std::string mesh = shield.getClass().getModel(shield); + const ESM::Armor *armor = shield.get()->mBase; + std::vector bodyparts = armor->mParts.mParts; + if (!bodyparts.empty()) + { + const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::Store &partStore = store.get(); + + // For NPCs try to get shield model from bodyparts first, with ground model as fallback + for (auto & part : bodyparts) + { + if (part.mPart != ESM::PRT_Shield) + continue; + + std::string bodypartName; + if (!mNpc->isMale() && !part.mFemale.empty()) + bodypartName = part.mFemale; + else if (!part.mMale.empty()) + bodypartName = part.mMale; + + if (!bodypartName.empty()) + { + const ESM::BodyPart *bodypart = 0; + bodypart = partStore.search(bodypartName); + if (bodypart->mData.mType != ESM::BodyPart::MT_Armor) + return ""; + else if (!bodypart->mModel.empty()) + mesh = "meshes\\" + bodypart->mModel; + } + } + } + + std::string holsteredName = mesh; + holsteredName = holsteredName.replace(holsteredName.size()-4, 4, "_sh.nif"); + if(mResourceSystem->getVFS()->exists(holsteredName)) + { + osg::ref_ptr shieldTemplate = mResourceSystem->getSceneManager()->getInstance(holsteredName); + SceneUtil::FindByNameVisitor findVisitor ("Bip01 Sheath"); + shieldTemplate->accept(findVisitor); + osg::ref_ptr sheathNode = findVisitor.mFoundNode; + if(!sheathNode) + return std::string(); + } + + return mesh; +} + void NpcAnimation::updateParts() { if (!mObjectRoot.get()) @@ -954,6 +1005,8 @@ void NpcAnimation::showCarriedLeft(bool show) } else removeIndividualPart(ESM::PRT_Shield); + + updateHolsteredShield(mShowCarriedLeft); } void NpcAnimation::attachArrow() @@ -1051,6 +1104,14 @@ void NpcAnimation::setWeaponGroup(const std::string &group, bool relativeDuratio void NpcAnimation::equipmentChanged() { + static const bool shieldSheathing = Settings::Manager::getBool("shield sheathing", "Game"); + if (shieldSheathing) + { + int weaptype; + MWMechanics::getActiveWeapon(mPtr, &weaptype); + showCarriedLeft(updateCarriedLeftVisible(weaptype)); + } + updateParts(); } diff --git a/apps/openmw/mwrender/npcanimation.hpp b/apps/openmw/mwrender/npcanimation.hpp index bed07dcdc..2d6d3a05f 100644 --- a/apps/openmw/mwrender/npcanimation.hpp +++ b/apps/openmw/mwrender/npcanimation.hpp @@ -98,6 +98,7 @@ private: protected: virtual void addControllers(); virtual bool isArrowAttached() const; + virtual std::string getShieldMesh(MWWorld::ConstPtr shield) const; public: /** diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst index 7bfa60c6c..20e041130 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -184,6 +184,22 @@ Otherwise they wait for the enemies or the player to do an attack first. Please note this setting has not been extensively tested and could have side effects with certain quests. This setting can be toggled in Advanced tab of the launcher. +shield sheathing +---------------- + +:Type: boolean +:Range: True/False +:Default: False + +If this setting is true, OpenMW will utilize shield sheathing-compatible assets to display holstered shields. + +To make use of this, you need to have an xbase_anim_sh.nif file with weapon bones that will be injected into the skeleton. +Also you can use additional _sh meshes for more precise shield placement. +Warning: this feature may conflict with mods that use pseudo-shields to emulate item in actor's hand (e.g. books, baskets, pick axes). +To avoid conflicts, you can use _sh mesh without "Bip01 Sheath" node for such "shields" meshes, or declare its bodypart as Clothing type, not as Armor. +Also you can use an _sh node with empty "Bip01 Sheath" node. +In this case the engine will use basic shield model, but will use transformations from the "Bip01 Sheath" node. + weapon sheathing ---------------- diff --git a/files/settings-default.cfg b/files/settings-default.cfg index f8c31eed7..e4cc58d09 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -255,6 +255,9 @@ strength influences hand to hand = 0 # Render holstered weapons (with quivers and scabbards), requires modded assets weapon sheathing = false +# Render holstered shield when it is not in actor's hands, requires modded assets +shield sheathing = false + # Allow non-standard ammunition solely to bypass normal weapon resistance or weakness only appropriate ammunition bypasses resistance = false