diff --git a/CHANGELOG.md b/CHANGELOG.md index d595eba8e..2033d3f08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Bug #2862: [macOS] Can't quit launcher using Command-Q or OpenMW->Quit Bug #2971: Compiler did not reject lines with naked expressions beginning with x.y Bug #3374: Touch spells not hitting kwama foragers + Bug #3486: [Mod] NPC Commands does not work Bug #3591: Angled hit distance too low Bug #3629: DB assassin attack never triggers creature spawning Bug #3876: Landscape texture painting is misaligned @@ -23,7 +24,10 @@ Bug #4215: OpenMW shows book text after last
tag Bug #4221: Characters get stuck in V-shaped terrain Bug #4251: Stationary NPCs do not return to their position after combat + Bug #4286: Scripted animations can be interrupted + Bug #4291: Non-persistent actors that started the game as dead do not play death animations Bug #4293: Faction members are not aware of faction ownerships in barter + Bug #4307: World cleanup should remove dead bodies only if death animation is finished Bug #4327: Missing animations during spell/weapon stance switching Bug #4368: Settings window ok button doesn't have key focus by default Bug #4393: NPCs walk back to where they were after using ResetActors diff --git a/apps/openmw/mwclass/actor.cpp b/apps/openmw/mwclass/actor.cpp index 17af4725e..73a4d37d7 100644 --- a/apps/openmw/mwclass/actor.cpp +++ b/apps/openmw/mwclass/actor.cpp @@ -31,7 +31,7 @@ namespace MWClass if (!model.empty()) { physics.addActor(ptr, model); - if (getCreatureStats(ptr).isDead()) + if (getCreatureStats(ptr).isDead() && getCreatureStats(ptr).isDeathAnimationFinished()) MWBase::Environment::get().getWorld()->enableActorCollision(ptr, false); } } diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 2e16b13aa..a07a5c893 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -135,8 +135,9 @@ namespace MWClass data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Flee, ref->mBase->mAiData.mFlee); data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Alarm, ref->mBase->mAiData.mAlarm); + // Persistent actors with 0 health do not play death animation if (data->mCreatureStats.isDead()) - data->mCreatureStats.setDeathAnimationFinished(true); + data->mCreatureStats.setDeathAnimationFinished(ptr.getClass().isPersistent(ptr)); // spells for (std::vector::const_iterator iter (ref->mBase->mSpells.mList.begin()); @@ -814,6 +815,9 @@ namespace MWClass if (ptr.getRefData().getCount() > 0 && !creatureStats.isDead()) return; + if (!creatureStats.isDeathAnimationFinished()) + return; + const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); static const float fCorpseRespawnDelay = gmst.find("fCorpseRespawnDelay")->getFloat(); static const float fCorpseClearDelay = gmst.find("fCorpseClearDelay")->getFloat(); diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 8e8b5c3ad..92e25baee 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -352,8 +352,10 @@ namespace MWClass data->mNpcStats.setNeedRecalcDynamicStats(true); } + + // Persistent actors with 0 health do not play death animation if (data->mNpcStats.isDead()) - data->mNpcStats.setDeathAnimationFinished(true); + data->mNpcStats.setDeathAnimationFinished(ptr.getClass().isPersistent(ptr)); // race powers const ESM::Race *race = MWBase::Environment::get().getWorld()->getStore().get().find(ref->mBase->mRace); @@ -1351,6 +1353,9 @@ namespace MWClass if (ptr.getRefData().getCount() > 0 && !creatureStats.isDead()) return; + if (!creatureStats.isDeathAnimationFinished()) + return; + const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); static const float fCorpseRespawnDelay = gmst.find("fCorpseRespawnDelay")->getFloat(); static const float fCorpseClearDelay = gmst.find("fCorpseClearDelay")->getFloat(); diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 07e5fa7d6..fbdb19d5b 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -561,6 +561,10 @@ void CharacterController::refreshIdleAnims(const WeaponInfo* weap, CharacterStat void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterState movement, JumpingState jump, bool force) { + // If the current animation is persistent, do not touch it + if (isPersistentAnimPlaying()) + return; + if (mPtr.getClass().isActor()) refreshHitRecoilAnims(); @@ -744,6 +748,11 @@ void CharacterController::playRandomDeath(float startpoint) { mDeathState = chooseRandomDeathState(); } + + // Do not interrupt scripted animation by death + if (isPersistentAnimPlaying()) + return; + playDeath(startpoint, mDeathState); } @@ -829,8 +838,8 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim mIdleState = CharState_Idle; } - - if(mDeathState == CharState_None) + // Do not update animation status for dead actors + if(mDeathState == CharState_None && (!cls.isActor() || !cls.getCreatureStats(mPtr).isDead())) refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true); mAnimation->runAnimation(0.f); @@ -1299,6 +1308,10 @@ bool CharacterController::updateWeaponState() } } + // Combat for actors with persistent animations obviously will be buggy + if (isPersistentAnimPlaying()) + return forcestateupdate; + float complete; bool animPlaying; if(mAttackingOrSpell) @@ -2013,15 +2026,17 @@ void CharacterController::update(float duration) { // initial start of death animation for actors that started the game as dead // not done in constructor since we need to give scripts a chance to set the mSkipAnim flag - if (!mSkipAnim && mDeathState != CharState_None && mCurrentDeath.empty()) + if (!mSkipAnim && mDeathState != CharState_None && mCurrentDeath.empty() && cls.isPersistent(mPtr)) { + // Fast-forward death animation to end for persisting corpses playDeath(1.f, mDeathState); } // We must always queue movement, even if there is none, to apply gravity. world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f)); } - osg::Vec3f moved = mAnimation->runAnimation(mSkipAnim ? 0.f : duration); + bool isPersist = isPersistentAnimPlaying(); + osg::Vec3f moved = mAnimation->runAnimation(mSkipAnim && !isPersist ? 0.f : duration); if(duration > 0.0f) moved /= duration; else @@ -2135,6 +2150,10 @@ bool CharacterController::playGroup(const std::string &groupname, int mode, int if(!mAnimation || !mAnimation->hasAnimation(groupname)) return false; + // We should not interrupt persistent animations by non-persistent ones + if (isPersistentAnimPlaying() && !persist) + return false; + // If this animation is a looped animation (has a "loop start" key) that is already playing // and has not yet reached the end of the loop, allow it to continue animating with its existing loop count // and remove any other animations that were queued. @@ -2164,23 +2183,28 @@ bool CharacterController::playGroup(const std::string &groupname, int mode, int if(mode != 0 || mAnimQueue.empty() || !isAnimPlaying(mAnimQueue.front().mGroup)) { - clearAnimQueue(); - mAnimQueue.push_back(entry); + clearAnimQueue(persist); mAnimation->disable(mCurrentIdle); mCurrentIdle.clear(); mIdleState = CharState_SpecialIdle; bool loopfallback = (entry.mGroup.compare(0,4,"idle") == 0); - mAnimation->play(groupname, Priority_Default, + mAnimation->play(groupname, persist && groupname != "idle" ? Priority_Persistent : Priority_Default, MWRender::Animation::BlendMask_All, false, 1.0f, ((mode==2) ? "loop start" : "start"), "stop", 0.0f, count-1, loopfallback); } else { mAnimQueue.resize(1); - mAnimQueue.push_back(entry); } + + // "PlayGroup idle" is a special case, used to remove to stop scripted animations playing + if (groupname == "idle") + entry.mPersist = false; + + mAnimQueue.push_back(entry); + return true; } @@ -2189,6 +2213,17 @@ void CharacterController::skipAnim() mSkipAnim = true; } +bool CharacterController::isPersistentAnimPlaying() +{ + if (!mAnimQueue.empty()) + { + AnimationQueueEntry& first = mAnimQueue.front(); + return first.mPersist && isAnimPlaying(first.mGroup); + } + + return false; +} + bool CharacterController::isAnimPlaying(const std::string &groupName) { if(mAnimation == NULL) @@ -2196,12 +2231,19 @@ bool CharacterController::isAnimPlaying(const std::string &groupName) return mAnimation->isPlaying(groupName); } - -void CharacterController::clearAnimQueue() +void CharacterController::clearAnimQueue(bool clearPersistAnims) { - if(!mAnimQueue.empty()) + // Do not interrupt scripted animations, if we want to keep them + if ((!isPersistentAnimPlaying() || clearPersistAnims) && !mAnimQueue.empty()) mAnimation->disable(mAnimQueue.front().mGroup); - mAnimQueue.clear(); + + for (AnimationQueue::iterator it = mAnimQueue.begin(); it != mAnimQueue.end();) + { + if (clearPersistAnims || !it->mPersist) + it = mAnimQueue.erase(it); + else + ++it; + } } void CharacterController::forceStateUpdate() @@ -2211,6 +2253,7 @@ void CharacterController::forceStateUpdate() clearAnimQueue(); refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true); + if(mDeathState != CharState_None) { playRandomDeath(); diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index a172620b9..381cf71a5 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -39,8 +39,8 @@ enum Priority { Priority_Knockdown, Priority_Torch, Priority_Storm, - Priority_Death, + Priority_Persistent, Num_Priorities }; @@ -215,12 +215,14 @@ class CharacterController : public MWRender::Animation::TextKeyListener void refreshMovementAnims(const WeaponInfo* weap, CharacterState movement, bool force=false); void refreshIdleAnims(const WeaponInfo* weap, CharacterState idle, bool force=false); - void clearAnimQueue(); + void clearAnimQueue(bool clearPersistAnims = false); bool updateWeaponState(); bool updateCreatureState(); void updateIdleStormState(bool inwater); + bool isPersistentAnimPlaying(); + void updateAnimQueue(); void updateHeadTracking(float duration); diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 3ccc06665..d96b9f809 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -1089,11 +1089,28 @@ namespace MWRender osg::Vec3f Animation::runAnimation(float duration) { + // If we have scripted animations, play only them + bool hasScriptedAnims = false; + for (AnimStateMap::iterator stateiter = mStates.begin(); stateiter != mStates.end(); stateiter++) + { + if (stateiter->second.mPriority.contains(int(MWMechanics::Priority_Persistent)) && stateiter->second.mPlaying) + { + hasScriptedAnims = true; + break; + } + } + osg::Vec3f movement(0.f, 0.f, 0.f); AnimStateMap::iterator stateiter = mStates.begin(); while(stateiter != mStates.end()) { AnimState &state = stateiter->second; + if (hasScriptedAnims && !state.mPriority.contains(int(MWMechanics::Priority_Persistent))) + { + ++stateiter; + continue; + } + const NifOsg::TextKeyMap &textkeys = state.mSource->getTextKeys(); NifOsg::TextKeyMap::const_iterator textkey(textkeys.upper_bound(state.getTime())); diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index 1b6495c11..fc3c2e245 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -945,8 +945,13 @@ namespace MWWorld { const MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); static const float fCorpseClearDelay = MWBase::Environment::get().getWorld()->getStore().get().find("fCorpseClearDelay")->getFloat(); - if (creatureStats.isDead() && !ptr.getClass().isPersistent(ptr) && creatureStats.getTimeOfDeath() + fCorpseClearDelay <= MWBase::Environment::get().getWorld()->getTimeStamp()) + if (creatureStats.isDead() && + creatureStats.isDeathAnimationFinished() && + !ptr.getClass().isPersistent(ptr) && + creatureStats.getTimeOfDeath() + fCorpseClearDelay <= MWBase::Environment::get().getWorld()->getTimeStamp()) + { MWBase::Environment::get().getWorld()->deleteObject(ptr); + } } void CellStore::respawn() diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst index 416f1bc1a..308a29546 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -77,6 +77,7 @@ This is how original Morrowind behaves. If this setting is false, player has to wait until end of death animation in all cases. This case is more safe, but makes using of summoned creatures exploit (looting summoned Dremoras and Golden Saints for expensive weapons) a lot harder. +Conflicts with mannequin mods, which use SkipAnim to prevent end of death animation. This setting can only be configured by editing the settings configuration file.