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.