More cleanup of scripted animations

macos_ci_fix
Mads Buvik Sandvei 5 months ago committed by jvoisin
parent 7cdf702a14
commit f6a6c278dd

@ -11,10 +11,12 @@
Bug #4508: Can't stack enchantment buffs from different instances of the same self-cast generic magic apparel
Bug #4610: Casting a Bound Weapon spell cancels the casting animation by equipping the weapon prematurely
Bug #4742: Actors with wander never stop walking after Loopgroup Walkforward
Bug #4743: PlayGroup doesn't play non-looping animations correctly
Bug #4754: Stack of ammunition cannot be equipped partially
Bug #4816: GetWeaponDrawn returns 1 before weapon is attached
Bug #5057: Weapon swing sound plays at same pitch whether it hits or misses
Bug #5062: Root bone rotations for NPC animation don't work the same as for creature animation
Bug #5066: Quirks with starting and stopping scripted animations
Bug #5129: Stuttering animation on Centurion Archer
Bug #5280: Unskinned shapes in skinned equipment are rendered in the wrong place
Bug #5371: Keyframe animation tracks are used for any file that begins with an X
@ -91,6 +93,7 @@
Bug #7636: Animations bug out when switching between 1st and 3rd person, while playing a scripted animation
Bug #7637: Actors can sometimes move while playing scripted animations
Bug #7639: NPCs don't use hand-to-hand if their other melee skills were damaged during combat
Bug #7641: loopgroup loops the animation one time too many for actors
Bug #7642: Items in repair and recharge menus aren't sorted alphabetically
Bug #7647: NPC walk cycle bugs after greeting player
Bug #7654: Tooltips for enchantments with invalid effects cause crashes

@ -20,6 +20,7 @@
#include "character.hpp"
#include <array>
#include <unordered_set>
#include <components/esm/records.hpp>
#include <components/misc/mathutil.hpp>
@ -1189,7 +1190,7 @@ namespace MWMechanics
if (!animPlaying)
{
int mask = MWRender::Animation::BlendMask_Torso | MWRender::Animation::BlendMask_RightArm;
mAnimation->play("idlestorm", Priority_Storm, mask, true, 1.0f, "start", "stop", 0.0f, ~0ul);
mAnimation->play("idlestorm", Priority_Storm, mask, true, 1.0f, "start", "stop", 0.0f, ~0ul, true);
}
else
{
@ -1246,8 +1247,47 @@ namespace MWMechanics
}
}
bool CharacterController::isLoopingAnimation(std::string_view group) const
{
// In Morrowind, a some animation groups are always considered looping, regardless
// of loop start/stop keys.
// To be match vanilla behavior we probably only need to check this list, but we don't
// want to prevent modded animations with custom group names from looping either.
static const std::unordered_set<std::string_view> loopingAnimations = { "walkforward", "walkback", "walkleft",
"walkright", "swimwalkforward", "swimwalkback", "swimwalkleft", "swimwalkright", "runforward", "runback",
"runleft", "runright", "swimrunforward", "swimrunback", "swimrunleft", "swimrunright", "sneakforward",
"sneakback", "sneakleft", "sneakright", "turnleft", "turnright", "swimturnleft", "swimturnright",
"spellturnleft", "spellturnright", "torch", "idle", "idle2", "idle3", "idle4", "idle5", "idle6", "idle7",
"idle8", "idle9", "idlesneak", "idlestorm", "idleswim", "jump", "inventoryhandtohand",
"inventoryweapononehand", "inventoryweapontwohand", "inventoryweapontwowide" };
static const std::vector<std::string_view> shortGroups = getAllWeaponTypeShortGroups();
if (mAnimation && mAnimation->getTextKeyTime(std::string(group) + ": loop start") >= 0)
return true;
// Most looping animations have variants for each weapon type shortgroup.
// Just remove the shortgroup instead of enumerating all of the possible animation groupnames.
// Make sure we pick the longest shortgroup so e.g. "bow" doesn't get picked over "crossbow"
// when the shortgroup is crossbow.
std::size_t suffixLength = 0;
for (std::string_view suffix : shortGroups)
{
if (suffix.length() > suffixLength && group.ends_with(suffix))
{
suffixLength = suffix.length();
}
}
group.remove_suffix(suffixLength);
return loopingAnimations.count(group) > 0;
}
bool CharacterController::updateWeaponState()
{
// If the current animation is scripted, we can't do anything here.
if (isScriptedAnimPlaying())
return false;
const auto world = MWBase::Environment::get().getWorld();
auto& prng = world->getPrng();
MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager();
@ -1481,10 +1521,6 @@ namespace MWMechanics
sndMgr->stopSound3D(mPtr, wolfRun);
}
// Combat for actors with scripted animations obviously will be buggy
if (isScriptedAnimPlaying())
return forcestateupdate;
float complete = 0.f;
bool animPlaying = false;
ESM::WeaponType::Class weapclass = getWeaponType(mWeaponType)->mWeaponClass;
@ -1857,33 +1893,58 @@ namespace MWMechanics
if (!mAnimation->isPlaying(mAnimQueue.front().mGroup))
{
// Remove the finished animation, unless it's a scripted animation that was interrupted by e.g. a rebuild of
// the animation object.
if (mAnimQueue.size() > 1 || !mAnimQueue.front().mScripted || mAnimQueue.front().mLoopCount == 0)
// Playing animations through mwscript is weird. If an animation is
// a looping animation (idle or other cyclical animations), then they
// will end as expected. However, if they are non-looping animations, they
// will stick around forever or until another animation appears in the queue.
bool shouldPlayOrRestart = mAnimQueue.size() > 1;
if (shouldPlayOrRestart || !mAnimQueue.front().mScripted
|| (mAnimQueue.front().mLoopCount == 0 && mAnimQueue.front().mLooping))
{
mAnimation->setPlayScriptedOnly(false);
mAnimation->disable(mAnimQueue.front().mGroup);
mAnimQueue.pop_front();
shouldPlayOrRestart = true;
}
else
// A non-looping animation will stick around forever, so only restart if the animation
// actually was removed for some reason.
shouldPlayOrRestart = !mAnimation->getInfo(mAnimQueue.front().mGroup)
&& mAnimation->hasAnimation(mAnimQueue.front().mGroup);
if (!mAnimQueue.empty())
if (shouldPlayOrRestart)
{
// Move on to the remaining items of the queue
bool loopfallback = mAnimQueue.front().mGroup.starts_with("idle");
mAnimation->play(mAnimQueue.front().mGroup,
mAnimQueue.front().mScripted ? Priority_Scripted : Priority_Default,
MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.0f,
mAnimQueue.front().mLoopCount, loopfallback);
playAnimQueue();
}
}
else
{
mAnimQueue.front().mLoopCount = mAnimation->getCurrentLoopCount(mAnimQueue.front().mGroup);
float complete;
size_t loopcount;
mAnimation->getInfo(mAnimQueue.front().mGroup, &complete, nullptr, &loopcount);
mAnimQueue.front().mLoopCount = loopcount;
mAnimQueue.front().mTime = complete;
}
if (!mAnimQueue.empty())
mAnimation->setLoopingEnabled(mAnimQueue.front().mGroup, mAnimQueue.size() <= 1);
}
void CharacterController::playAnimQueue(bool loopStart)
{
if (!mAnimQueue.empty())
{
clearStateAnimation(mCurrentIdle);
mIdleState = CharState_SpecialIdle;
auto priority = mAnimQueue.front().mScripted ? Priority_Scripted : Priority_Default;
mAnimation->setPlayScriptedOnly(mAnimQueue.front().mScripted);
mAnimation->play(mAnimQueue.front().mGroup, priority, MWRender::Animation::BlendMask_All, false, 1.0f,
(loopStart ? "loop start" : "start"), "stop", mAnimQueue.front().mTime, mAnimQueue.front().mLoopCount,
mAnimQueue.front().mLooping);
}
}
void CharacterController::update(float duration)
{
MWBase::World* world = MWBase::Environment::get().getWorld();
@ -2455,10 +2516,11 @@ namespace MWMechanics
if (iter == mAnimQueue.begin())
{
anim.mLoopCount = mAnimation->getCurrentLoopCount(anim.mGroup);
float complete;
mAnimation->getInfo(anim.mGroup, &complete, nullptr);
size_t loopcount;
mAnimation->getInfo(anim.mGroup, &complete, nullptr, &loopcount);
anim.mTime = complete;
anim.mLoopCount = loopcount;
}
else
{
@ -2484,26 +2546,20 @@ namespace MWMechanics
entry.mGroup = iter->mGroup;
entry.mLoopCount = iter->mLoopCount;
entry.mScripted = true;
entry.mLooping = isLoopingAnimation(entry.mGroup);
entry.mTime = iter->mTime;
if (iter->mAbsolute)
{
float start = mAnimation->getTextKeyTime(iter->mGroup + ": start");
float stop = mAnimation->getTextKeyTime(iter->mGroup + ": stop");
float time = std::clamp(iter->mTime, start, stop);
entry.mTime = (time - start) / (stop - start);
}
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::clamp(anim.mTime, start, stop);
complete = (time - start) / (stop - start);
}
clearStateAnimation(mCurrentIdle);
mIdleState = CharState_SpecialIdle;
bool loopfallback = mAnimQueue.front().mGroup.starts_with("idle");
mAnimation->play(anim.mGroup, Priority_Scripted, MWRender::Animation::BlendMask_All, false, 1.0f, "start",
"stop", complete, anim.mLoopCount, loopfallback);
playAnimQueue();
}
}
@ -2516,13 +2572,14 @@ namespace MWMechanics
if (isScriptedAnimPlaying() && !scripted)
return true;
// If this animation is a looped animation (has a "loop start" key) that is already playing
bool looping = isLoopingAnimation(groupname);
// If this animation is a looped animation 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.
// This emulates observed behavior from the original allows the script "OutsideBanner" to animate banners
// correctly.
if (!mAnimQueue.empty() && mAnimQueue.front().mGroup == groupname
&& mAnimation->getTextKeyTime(mAnimQueue.front().mGroup + ": loop start") >= 0
if (!mAnimQueue.empty() && mAnimQueue.front().mGroup == groupname && looping
&& mAnimation->isPlaying(groupname))
{
float endOfLoop = mAnimation->getTextKeyTime(mAnimQueue.front().mGroup + ": loop stop");
@ -2537,36 +2594,43 @@ namespace MWMechanics
}
}
count = std::max(count, 1);
// The loop count in vanilla is weird.
// if played with a count of 0, all objects play exactly once from start to stop.
// But if the count is x > 0, actors and non-actors behave differently. actors will loop
// exactly x times, while non-actors will loop x+1 instead.
if (mPtr.getClass().isActor())
count--;
count = std::max(count, 0);
AnimationQueueEntry entry;
entry.mGroup = groupname;
entry.mLoopCount = count - 1;
entry.mLoopCount = count;
entry.mTime = 0.f;
entry.mScripted = scripted;
entry.mLooping = looping;
bool playImmediately = false;
if (mode != 0 || mAnimQueue.empty() || !isAnimPlaying(mAnimQueue.front().mGroup))
{
clearAnimQueue(scripted);
clearStateAnimation(mCurrentIdle);
mIdleState = CharState_SpecialIdle;
bool loopfallback = entry.mGroup.starts_with("idle");
mAnimation->play(groupname, scripted && groupname != "idle" ? Priority_Scripted : Priority_Default,
MWRender::Animation::BlendMask_All, false, 1.0f, ((mode == 2) ? "loop start" : "start"), "stop", 0.0f,
count - 1, loopfallback);
playImmediately = true;
}
else
{
mAnimQueue.resize(1);
}
// "PlayGroup idle" is a special case, used to remove to stop scripted animations playing
// "PlayGroup idle" is a special case, used to stop and remove scripted animations playing
if (groupname == "idle")
entry.mScripted = false;
mAnimQueue.push_back(entry);
if (playImmediately)
playAnimQueue(mode == 2);
return true;
}
@ -2577,11 +2641,10 @@ namespace MWMechanics
bool CharacterController::isScriptedAnimPlaying() const
{
// If the front of the anim queue is scripted, morrowind treats it as if it's
// still playing even if it's actually done.
if (!mAnimQueue.empty())
{
const AnimationQueueEntry& first = mAnimQueue.front();
return first.mScripted && isAnimPlaying(first.mGroup);
}
return mAnimQueue.front().mScripted;
return false;
}
@ -2611,6 +2674,7 @@ namespace MWMechanics
if (clearScriptedAnims)
{
mAnimation->setPlayScriptedOnly(false);
mAnimQueue.clear();
return;
}
@ -2645,6 +2709,8 @@ namespace MWMechanics
playRandomDeath();
}
updateAnimQueue();
mAnimation->runAnimation(0.f);
}

@ -135,6 +135,8 @@ namespace MWMechanics
{
std::string mGroup;
size_t mLoopCount;
float mTime;
bool mLooping;
bool mScripted;
};
typedef std::deque<AnimationQueueEntry> AnimationQueue;
@ -219,6 +221,7 @@ namespace MWMechanics
bool isMovementAnimationControlled() const;
void updateAnimQueue();
void playAnimQueue(bool useLoopStart = false);
void updateHeadTracking(float duration);
@ -245,6 +248,8 @@ namespace MWMechanics
void prepareHit();
bool isLoopingAnimation(std::string_view group) const;
public:
CharacterController(const MWWorld::Ptr& ptr, MWRender::Animation* anim);
virtual ~CharacterController();

@ -8,6 +8,8 @@
#include <components/esm3/loadweap.hpp>
#include <set>
namespace MWMechanics
{
template <enum ESM::Weapon::Type>
@ -416,4 +418,18 @@ namespace MWMechanics
return &Weapon<ESM::Weapon::ShortBladeOneHand>::getValue();
}
std::vector<std::string_view> getAllWeaponTypeShortGroups()
{
// Go via a set to eliminate duplicates.
std::set<std::string_view> shortGroupSet;
for (int type = ESM::Weapon::Type::First; type <= ESM::Weapon::Type::Last; type++)
{
std::string_view shortGroup = getWeaponType(type)->mShortGroup;
if (!shortGroup.empty())
shortGroupSet.insert(shortGroup);
}
return std::vector<std::string_view>(shortGroupSet.begin(), shortGroupSet.end());
}
}

@ -1,6 +1,9 @@
#ifndef GAME_MWMECHANICS_WEAPONTYPE_H
#define GAME_MWMECHANICS_WEAPONTYPE_H
#include <string_view>
#include <vector>
namespace ESM
{
struct WeaponType;
@ -21,6 +24,8 @@ namespace MWMechanics
MWWorld::ContainerStoreIterator getActiveWeapon(const MWWorld::Ptr& actor, int* weaptype);
const ESM::WeaponType* getWeaponType(const int weaponType);
std::vector<std::string_view> getAllWeaponTypeShortGroups();
}
#endif

@ -529,6 +529,7 @@ namespace MWRender
, mBodyPitchRadians(0.f)
, mHasMagicEffects(false)
, mAlpha(1.f)
, mPlayScriptedOnly(false)
{
for (size_t i = 0; i < sNumBlendMasks; i++)
mAnimationTimePtr[i] = std::make_shared<AnimationTime>();
@ -1020,7 +1021,7 @@ namespace MWRender
return false;
}
bool Animation::getInfo(std::string_view groupname, float* complete, float* speedmult) const
bool Animation::getInfo(std::string_view groupname, float* complete, float* speedmult, size_t* loopcount) const
{
AnimStateMap::const_iterator iter = mStates.find(groupname);
if (iter == mStates.end())
@ -1029,6 +1030,8 @@ namespace MWRender
*complete = 0.0f;
if (speedmult)
*speedmult = 0.0f;
if (loopcount)
*loopcount = 0;
return false;
}
@ -1042,6 +1045,9 @@ namespace MWRender
}
if (speedmult)
*speedmult = iter->second.mSpeedMult;
if (loopcount)
*loopcount = iter->second.mLoopCount;
return true;
}
@ -1054,15 +1060,6 @@ 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(std::string_view groupname)
{
AnimStateMap::iterator iter = mStates.find(groupname);
@ -1141,23 +1138,12 @@ 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_Scripted)) && 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_Scripted)))
if (mPlayScriptedOnly && !state.mPriority.contains(MWMechanics::Priority_Scripted))
{
++stateiter;
continue;
@ -1263,10 +1249,6 @@ namespace MWRender
osg::Quat(mHeadPitchRadians, osg::Vec3f(1, 0, 0)) * osg::Quat(yaw, osg::Vec3f(0, 0, 1)));
}
// Scripted animations should not cause movement
if (hasScriptedAnims)
return osg::Vec3f(0, 0, 0);
return movement;
}

@ -292,6 +292,8 @@ namespace MWRender
osg::ref_ptr<SceneUtil::LightListCallback> mLightListCallback;
bool mPlayScriptedOnly;
const NodeMap& getNodeMap() const;
/* Sets the appropriate animations on the bone groups based on priority.
@ -441,7 +443,8 @@ namespace MWRender
* \param speedmult Stores the animation speed multiplier
* \return True if the animation is active, false otherwise.
*/
bool getInfo(std::string_view groupname, float* complete = nullptr, float* speedmult = nullptr) const;
bool getInfo(std::string_view groupname, float* complete = nullptr, float* speedmult = nullptr,
size_t* loopcount = nullptr) const;
/// Get the absolute position in the animation track of the first text key with the given group.
float getStartTime(const std::string& groupname) const;
@ -453,8 +456,6 @@ namespace MWRender
/// 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.
*/
@ -477,6 +478,9 @@ namespace MWRender
MWWorld::MovementDirectionFlags getSupportedMovementDirections(
std::span<const std::string_view> prefixes) const;
bool getPlayScriptedOnly() const { return mPlayScriptedOnly; }
void setPlayScriptedOnly(bool playScriptedOnly) { mPlayScriptedOnly = playScriptedOnly; }
virtual bool useShieldAnimations() const { return false; }
virtual bool getWeaponsShown() const { return false; }
virtual void showWeapons(bool showWeapon) {}

@ -923,13 +923,18 @@ namespace MWRender
if (mViewMode == VM_FirstPerson)
{
NodeMap::iterator found = mNodeMap.find("bip01 neck");
if (found != mNodeMap.end())
// If there is no active animation, then the bip01 neck node will not be updated each frame, and the
// RotateController will accumulate rotations.
if (mStates.size() > 0)
{
osg::MatrixTransform* node = found->second.get();
mFirstPersonNeckController = new RotateController(mObjectRoot.get());
node->addUpdateCallback(mFirstPersonNeckController);
mActiveControllers.emplace_back(node, mFirstPersonNeckController);
NodeMap::iterator found = mNodeMap.find("bip01 neck");
if (found != mNodeMap.end())
{
osg::MatrixTransform* node = found->second.get();
mFirstPersonNeckController = new RotateController(mObjectRoot.get());
node->addUpdateCallback(mFirstPersonNeckController);
mActiveControllers.emplace_back(node, mFirstPersonNeckController);
}
}
}
else if (mViewMode == VM_Normal)

@ -91,7 +91,7 @@ namespace MWScript
throw std::runtime_error("animation mode out of range");
}
MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(ptr, group, mode, loops + 1, true);
MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(ptr, group, mode, loops, true);
}
};

@ -24,6 +24,7 @@ namespace ESM
enum Type
{
First = -4,
PickProbe = -4,
HandToHand = -3,
Spell = -2,
@ -41,7 +42,8 @@ namespace ESM
MarksmanCrossbow = 10,
MarksmanThrown = 11,
Arrow = 12,
Bolt = 13
Bolt = 13,
Last = 13
};
enum AttackType

Loading…
Cancel
Save