Commit b65f379b authored by MiroslavR's avatar MiroslavR
Browse files

Save scripted animation state (Fixes #1931, #2150, #3393)

parent 3374630e
#include <string>
#include <iostream>
#include <limits>
#include <components/misc/stringops.hpp>
#include "convertacdt.hpp"
namespace ESSImport
......@@ -49,4 +55,49 @@ namespace ESSImport
npcStats.mTimeToStartDrowning = actorData.mACDT.mBreathMeter;
}
void convertANIS (const ANIS& anis, ESM::AnimationState& state)
{
static const char* animGroups[] =
{
"Idle", "Idle2", "Idle3", "Idle4", "Idle5", "Idle6", "Idle7", "Idle8", "Idle9", "Idlehh", "Idle1h", "Idle2c",
"Idle2w", "IdleSwim", "IdleSpell", "IdleCrossbow", "IdleSneak", "IdleStorm", "Torch", "Hit1", "Hit2", "Hit3",
"Hit4", "Hit5", "SwimHit1", "SwimHit2", "SwimHit3", "Death1", "Death2", "Death3", "Death4", "Death5",
"DeathKnockDown", "DeathKnockOut", "KnockDown", "KnockOut", "SwimDeath", "SwimDeath2", "SwimDeath3",
"SwimDeathKnockDown", "SwimDeathKnockOut", "SwimKnockOut", "SwimKnockDown", "SwimWalkForward",
"SwimWalkBack", "SwimWalkLeft", "SwimWalkRight", "SwimRunForward", "SwimRunBack", "SwimRunLeft",
"SwimRunRight", "SwimTurnLeft", "SwimTurnRight", "WalkForward", "WalkBack", "WalkLeft", "WalkRight",
"TurnLeft", "TurnRight", "RunForward", "RunBack", "RunLeft", "RunRight", "SneakForward", "SneakBack",
"SneakLeft", "SneakRight", "Jump", "WalkForwardhh", "WalkBackhh", "WalkLefthh", "WalkRighthh",
"TurnLefthh", "TurnRighthh", "RunForwardhh", "RunBackhh", "RunLefthh", "RunRighthh", "SneakForwardhh",
"SneakBackhh", "SneakLefthh", "SneakRighthh", "Jumphh", "WalkForward1h", "WalkBack1h", "WalkLeft1h",
"WalkRight1h", "TurnLeft1h", "TurnRight1h", "RunForward1h", "RunBack1h", "RunLeft1h", "RunRight1h",
"SneakForward1h", "SneakBack1h", "SneakLeft1h", "SneakRight1h", "Jump1h", "WalkForward2c", "WalkBack2c",
"WalkLeft2c", "WalkRight2c", "TurnLeft2c", "TurnRight2c", "RunForward2c", "RunBack2c", "RunLeft2c",
"RunRight2c", "SneakForward2c", "SneakBack2c", "SneakLeft2c", "SneakRight2c", "Jump2c", "WalkForward2w",
"WalkBack2w", "WalkLeft2w", "WalkRight2w", "TurnLeft2w", "TurnRight2w", "RunForward2w", "RunBack2w",
"RunLeft2w", "RunRight2w", "SneakForward2w", "SneakBack2w", "SneakLeft2w", "SneakRight2w", "Jump2w",
"SpellCast", "SpellTurnLeft", "SpellTurnRight", "Attack1", "Attack2", "Attack3", "SwimAttack1",
"SwimAttack2", "SwimAttack3", "HandToHand", "Crossbow", "BowAndArrow", "ThrowWeapon", "WeaponOneHand",
"WeaponTwoHand", "WeaponTwoWide", "Shield", "PickProbe", "InventoryHandToHand", "InventoryWeaponOneHand",
"InventoryWeaponTwoHand", "InventoryWeaponTwoWide"
};
if (anis.mGroupIndex < (sizeof(animGroups) / sizeof(*animGroups)))
{
std::string group(animGroups[anis.mGroupIndex]);
Misc::StringUtils::lowerCaseInPlace(group);
ESM::AnimationState::ScriptedAnimation scriptedAnim;
scriptedAnim.mGroup = group;
scriptedAnim.mTime = anis.mTime;
scriptedAnim.mAbsolute = true;
// Neither loop count nor queueing seems to be supported by the ess format.
scriptedAnim.mLoopCount = std::numeric_limits<size_t>::max();
state.mScriptedAnims.push_back(scriptedAnim);
}
else
// TODO: Handle 0xFF index, which seems to be used for finished animations.
std::cerr << "unknown animation group index: " << static_cast<unsigned int>(anis.mGroupIndex) << std::endl;
}
}
......@@ -4,6 +4,7 @@
#include <components/esm/creaturestats.hpp>
#include <components/esm/npcstats.hpp>
#include <components/esm/loadskil.hpp>
#include <components/esm/animationstate.hpp>
#include "importacdt.hpp"
......@@ -18,6 +19,8 @@ namespace ESSImport
void convertACSC (const ACSC& acsc, ESM::CreatureStats& cStats);
void convertNpcData (const ActorData& actorData, ESM::NpcStats& npcStats);
void convertANIS (const ANIS& anis, ESM::AnimationState& state);
}
#endif
......@@ -34,6 +34,9 @@ namespace
objstate.mCount = 0;
convertSCRI(cellref.mSCRI, objstate.mLocals);
objstate.mHasLocals = !objstate.mLocals.mVariables.empty();
if (cellref.mHasANIS)
convertANIS(cellref.mANIS, objstate.mAnimationState);
}
bool isIndexedRefId(const std::string& indexedRefId)
......
......@@ -123,8 +123,13 @@ namespace ESSImport
if (esm.isNextSub("ND3D"))
esm.skipHSub();
mHasANIS = false;
if (esm.isNextSub("ANIS"))
esm.skipHSub();
{
mHasANIS = true;
esm.getHT(mANIS);
}
}
}
......@@ -55,6 +55,12 @@ namespace ESSImport
unsigned char mCorpseClearCountdown; // hours?
unsigned char mUnknown3[71];
};
struct ANIS
{
unsigned char mGroupIndex;
unsigned char mUnknown[3];
float mTime;
};
#pragma pack(pop)
struct ActorData : public ESM::CellRef
......@@ -77,6 +83,9 @@ namespace ESSImport
SCRI mSCRI;
bool mHasANIS;
ANIS mANIS; // scripted animation state
void load(ESM::ESMReader& esm);
};
......
......@@ -159,12 +159,14 @@ namespace MWBase
virtual void forceStateUpdate(const MWWorld::Ptr &ptr) = 0;
///< Forces an object to refresh its animation state.
virtual bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number=1) = 0;
virtual bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number=1, bool persist=false) = 0;
///< Run animation for a MW-reference. Calls to this function for references that are currently not
/// in the scene should be ignored.
///
/// \param mode 0 normal, 1 immediate start, 2 immediate loop
/// \param count How many times the animation should be run
/// \param persist Whether the animation state should be stored in saved games
/// and persist after cell unload.
/// \return Success or error
virtual void skipAnimation(const MWWorld::Ptr& ptr) = 0;
......@@ -173,6 +175,9 @@ namespace MWBase
virtual bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) = 0;
/// Save the current animation state of managed references to their RefData.
virtual void persistAnimationStates() = 0;
/// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently
/// paused we may want to do it manually (after equipping permanent enchantment)
virtual void updateMagicEffects (const MWWorld::Ptr& ptr) = 0;
......
......@@ -1389,12 +1389,12 @@ namespace MWMechanics
iter->second->getCharacterController()->forceStateUpdate();
}
bool Actors::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number)
bool Actors::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist)
{
PtrActorMap::iterator iter = mActors.find(ptr);
if(iter != mActors.end())
{
return iter->second->getCharacterController()->playGroup(groupName, mode, number);
return iter->second->getCharacterController()->playGroup(groupName, mode, number, persist);
}
else
{
......@@ -1417,6 +1417,12 @@ namespace MWMechanics
return false;
}
void Actors::persistAnimationStates()
{
for (PtrActorMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter)
iter->second->getCharacterController()->persistAnimationState();
}
void Actors::getObjectsInRange(const osg::Vec3f& position, float radius, std::vector<MWWorld::Ptr>& out)
{
for (PtrActorMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter)
......
......@@ -109,9 +109,10 @@ namespace MWMechanics
void forceStateUpdate(const MWWorld::Ptr &ptr);
bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number);
bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist=false);
void skipAnimation(const MWWorld::Ptr& ptr);
bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName);
void persistAnimationStates();
void getObjectsInRange(const osg::Vec3f& position, float radius, std::vector<MWWorld::Ptr>& out);
......
......@@ -762,12 +762,17 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim
refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true);
mAnimation->runAnimation(0.f);
unpersistAnimationState();
}
CharacterController::~CharacterController()
{
if (mAnimation)
{
persistAnimationState();
mAnimation->setTextKeyListener(NULL);
}
}
void split(const std::string &s, char delim, std::vector<std::string> &elems) {
......@@ -1557,14 +1562,14 @@ void CharacterController::update(float duration)
{
if(mAnimQueue.size() > 1)
{
if(mAnimation->isPlaying(mAnimQueue.front().first) == false)
if(mAnimation->isPlaying(mAnimQueue.front().mGroup) == false)
{
mAnimation->disable(mAnimQueue.front().first);
mAnimation->disable(mAnimQueue.front().mGroup);
mAnimQueue.pop_front();
mAnimation->play(mAnimQueue.front().first, Priority_Default,
mAnimation->play(mAnimQueue.front().mGroup, Priority_Default,
MWRender::Animation::BlendMask_All, false,
1.0f, "start", "stop", 0.0f, mAnimQueue.front().second);
1.0f, "start", "stop", 0.0f, mAnimQueue.front().mLoopCount);
}
}
}
......@@ -1837,14 +1842,14 @@ void CharacterController::update(float duration)
}
else if(mAnimQueue.size() > 1)
{
if(mAnimation->isPlaying(mAnimQueue.front().first) == false)
if(mAnimation->isPlaying(mAnimQueue.front().mGroup) == false)
{
mAnimation->disable(mAnimQueue.front().first);
mAnimation->disable(mAnimQueue.front().mGroup);
mAnimQueue.pop_front();
mAnimation->play(mAnimQueue.front().first, Priority_Default,
mAnimation->play(mAnimQueue.front().mGroup, Priority_Default,
MWRender::Animation::BlendMask_All, false,
1.0f, "start", "stop", 0.0f, mAnimQueue.front().second);
1.0f, "start", "stop", 0.0f, mAnimQueue.front().mLoopCount);
}
}
......@@ -1951,8 +1956,74 @@ void CharacterController::update(float duration)
mAnimation->enableHeadAnimation(cls.isActor() && !cls.getCreatureStats(mPtr).isDead());
}
void CharacterController::persistAnimationState()
{
ESM::AnimationState& state = mPtr.getRefData().getAnimationState();
state.mScriptedAnims.clear();
for (AnimationQueue::const_iterator iter = mAnimQueue.begin(); iter != mAnimQueue.end(); ++iter)
{
if (!iter->mPersist)
continue;
ESM::AnimationState::ScriptedAnimation anim;
anim.mGroup = iter->mGroup;
if (iter == mAnimQueue.begin())
{
anim.mLoopCount = mAnimation->getCurrentLoopCount(anim.mGroup);
float complete;
mAnimation->getInfo(anim.mGroup, &complete, NULL);
anim.mTime = complete;
}
else
{
anim.mLoopCount = iter->mLoopCount;
anim.mTime = 0.f;
}
state.mScriptedAnims.push_back(anim);
}
}
void CharacterController::unpersistAnimationState()
{
const ESM::AnimationState& state = mPtr.getRefData().getAnimationState();
if (!state.mScriptedAnims.empty())
{
clearAnimQueue();
for (ESM::AnimationState::ScriptedAnimations::const_iterator iter = state.mScriptedAnims.begin(); iter != state.mScriptedAnims.end(); ++iter)
{
AnimationQueueEntry entry;
entry.mGroup = iter->mGroup;
entry.mLoopCount = iter->mLoopCount;
entry.mPersist = true;
mAnimQueue.push_back(entry);
}
const ESM::AnimationState::ScriptedAnimation& anim = state.mScriptedAnims.front();
float complete = anim.mTime;
if (anim.mAbsolute)
{
float start = mAnimation->getTextKeyTime(anim.mGroup+": start");
float stop = mAnimation->getTextKeyTime(anim.mGroup+": stop");
float time = std::max(start, std::min(stop, anim.mTime));
complete = (time - start) / (stop - start);
}
bool CharacterController::playGroup(const std::string &groupname, int mode, int count)
mAnimation->disable(mCurrentIdle);
mCurrentIdle.clear();
mIdleState = CharState_SpecialIdle;
mAnimation->play(anim.mGroup,
Priority_Default, MWRender::Animation::BlendMask_All, false, 1.0f,
"start", "stop", complete, anim.mLoopCount);
}
}
bool CharacterController::playGroup(const std::string &groupname, int mode, int count, bool persist)
{
if(!mAnimation || !mAnimation->hasAnimation(groupname))
{
......@@ -1962,10 +2033,16 @@ bool CharacterController::playGroup(const std::string &groupname, int mode, int
else
{
count = std::max(count, 1);
if(mode != 0 || mAnimQueue.empty() || !isAnimPlaying(mAnimQueue.front().first))
AnimationQueueEntry entry;
entry.mGroup = groupname;
entry.mLoopCount = count-1;
entry.mPersist = persist;
if(mode != 0 || mAnimQueue.empty() || !isAnimPlaying(mAnimQueue.front().mGroup))
{
clearAnimQueue();
mAnimQueue.push_back(std::make_pair(groupname, count-1));
mAnimQueue.push_back(entry);
mAnimation->disable(mCurrentIdle);
mCurrentIdle.clear();
......@@ -1978,9 +2055,9 @@ bool CharacterController::playGroup(const std::string &groupname, int mode, int
else if(mode == 0)
{
if (!mAnimQueue.empty())
mAnimation->stopLooping(mAnimQueue.front().first);
mAnimation->stopLooping(mAnimQueue.front().mGroup);
mAnimQueue.resize(1);
mAnimQueue.push_back(std::make_pair(groupname, count-1));
mAnimQueue.push_back(entry);
}
}
return true;
......@@ -2002,7 +2079,7 @@ bool CharacterController::isAnimPlaying(const std::string &groupName)
void CharacterController::clearAnimQueue()
{
if(!mAnimQueue.empty())
mAnimation->disable(mAnimQueue.front().first);
mAnimation->disable(mAnimQueue.front().mGroup);
mAnimQueue.clear();
}
......
......@@ -147,7 +147,13 @@ class CharacterController : public MWRender::Animation::TextKeyListener
MWWorld::Ptr mPtr;
MWRender::Animation *mAnimation;
typedef std::deque<std::pair<std::string,size_t> > AnimationQueue;
struct AnimationQueueEntry
{
std::string mGroup;
size_t mLoopCount;
bool mPersist;
};
typedef std::deque<AnimationQueueEntry> AnimationQueue;
AnimationQueue mAnimQueue;
CharacterState mIdleState;
......@@ -236,7 +242,10 @@ public:
void update(float duration);
bool playGroup(const std::string &groupname, int mode, int count);
void persistAnimationState();
void unpersistAnimationState();
bool playGroup(const std::string &groupname, int mode, int count, bool persist=false);
void skipAnim();
bool isAnimPlaying(const std::string &groupName);
......
......@@ -842,12 +842,12 @@ namespace MWMechanics
mActors.forceStateUpdate(ptr);
}
bool MechanicsManager::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number)
bool MechanicsManager::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist)
{
if(ptr.getClass().isActor())
return mActors.playAnimationGroup(ptr, groupName, mode, number);
return mActors.playAnimationGroup(ptr, groupName, mode, number, persist);
else
return mObjects.playAnimationGroup(ptr, groupName, mode, number);
return mObjects.playAnimationGroup(ptr, groupName, mode, number, persist);
}
void MechanicsManager::skipAnimation(const MWWorld::Ptr& ptr)
{
......@@ -864,6 +864,12 @@ namespace MWMechanics
return false;
}
void MechanicsManager::persistAnimationStates()
{
mActors.persistAnimationStates();
mObjects.persistAnimationStates();
}
void MechanicsManager::updateMagicEffects(const MWWorld::Ptr &ptr)
{
mActors.updateMagicEffects(ptr);
......
......@@ -146,9 +146,10 @@ namespace MWMechanics
/// Attempt to play an animation group
/// @return Success or error
virtual bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number);
virtual bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist=false);
virtual void skipAnimation(const MWWorld::Ptr& ptr);
virtual bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string &groupName);
virtual void persistAnimationStates();
/// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently
/// paused we may want to do it manually (after equipping permanent enchantment)
......
......@@ -79,12 +79,12 @@ void Objects::update(float duration, bool paused)
}
}
bool Objects::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number)
bool Objects::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist)
{
PtrControllerMap::iterator iter = mObjects.find(ptr);
if(iter != mObjects.end())
{
return iter->second->playGroup(groupName, mode, number);
return iter->second->playGroup(groupName, mode, number, persist);
}
else
{
......@@ -99,6 +99,12 @@ void Objects::skipAnimation(const MWWorld::Ptr& ptr)
iter->second->skipAnim();
}
void Objects::persistAnimationStates()
{
for (PtrControllerMap::iterator iter = mObjects.begin(); iter != mObjects.end(); ++iter)
iter->second->persistAnimationState();
}
void Objects::getObjectsInRange(const osg::Vec3f& position, float radius, std::vector<MWWorld::Ptr>& out)
{
for (PtrControllerMap::iterator iter = mObjects.begin(); iter != mObjects.end(); ++iter)
......
......@@ -38,8 +38,9 @@ namespace MWMechanics
void update(float duration, bool paused);
///< Update object animations
bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number);
bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist=false);
void skipAnimation(const MWWorld::Ptr& ptr);
void persistAnimationStates();
void getObjectsInRange (const osg::Vec3f& position, float radius, std::vector<MWWorld::Ptr>& out);
};
......
......@@ -843,6 +843,15 @@ namespace MWRender
return iter->second.getTime();
}
size_t Animation::getCurrentLoopCount(const std::string& groupname) const
{
AnimStateMap::const_iterator iter = mStates.find(groupname);
if(iter == mStates.end())
return 0;
return iter->second.mLoopCount;
}
void Animation::disable(const std::string &groupname)
{
AnimStateMap::iterator iter = mStates.find(groupname);
......
......@@ -415,6 +415,8 @@ public:
/// Get the current absolute position in the animation track for the animation that is currently playing from the given group.
float getCurrentTime(const std::string& groupname) const;
size_t getCurrentLoopCount(const std::string& groupname) const;
/** Disables the specified animation group;
* \param groupname Animation group to disable.
*/
......
......@@ -56,7 +56,7 @@ namespace MWScript
throw std::runtime_error ("animation mode out of range");
}
MWBase::Environment::get().getMechanicsManager()->playAnimationGroup (ptr, group, mode, std::numeric_limits<int>::max());
MWBase::Environment::get().getMechanicsManager()->playAnimationGroup (ptr, group, mode, std::numeric_limits<int>::max(), true);
}
};
......@@ -89,7 +89,7 @@ namespace MWScript
throw std::runtime_error ("animation mode out of range");
}
MWBase::Environment::get().getMechanicsManager()->playAnimationGroup (ptr, group, mode, loops);
MWBase::Environment::get().getMechanicsManager()->playAnimationGroup (ptr, group, mode, loops, true);
}
};
......
......@@ -219,6 +219,9 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot
else
slot = character->updateSlot (slot, profile);
// Make sure the animation state held by references is up to date before saving the game.
MWBase::Environment::get().getMechanicsManager()->persistAnimationStates();
// Write to a memory stream first. If there is an exception during the save process, we don't want to trash the
// existing save file we are overwriting.
std::stringstream stream;
......
......@@ -31,6 +31,8 @@ namespace MWWorld
mDeletedByContentFile = refData.mDeletedByContentFile;
mFlags = refData.mFlags;
mAnimationState = refData.mAnimationState;
mCustomData = refData.mCustomData ? refData.mCustomData->clone() : 0;
}
......@@ -65,6 +67,7 @@ namespace MWWorld
mEnabled (objectState.mEnabled != 0),
mCount (objectState.mCount),
mPosition (objectState.mPosition),
mAnimationState(objectState.mAnimationState),
mCustomData (0),
mChanged(true), mFlags(objectState.mFlags) // Loading from a savegame -> assume changed
{
......@@ -96,6 +99,8 @@ namespace MWWorld
objectState.mCount = mCount;
objectState.mPosition = mPosition;
objectState.mFlags = mFlags;
objectState.mAnimationState = mAnimationState;
}
RefData& RefData::operator= (const RefData& refData)
......@@ -269,4 +274,15 @@ namespace MWWorld
else
return false;
}
const ESM::AnimationState& RefData::getAnimationState() const
{
return mAnimationState;
}
ESM::AnimationState& RefData::getAnimationState()
{
return mAnimationState;
}
}
......@@ -2,6 +2,7 @@
#define GAME_MWWORLD_REFDATA_H
#include <components/esm/defs.hpp>
#include <components/esm/animationstate.hpp>
#include "../mwscript/locals.hpp"
......@@ -42,6 +43,8 @@ namespace MWWorld
ESM::Position mPosition;
ESM::AnimationState mAnimationState;
CustomData *mCustomData;
void copy (const RefData& refData);
......@@ -132,6 +135,9 @@ namespace MWWorld