From 69aceb5c1ee028363b766f78f9c723522ea8848f Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Wed, 9 Oct 2019 20:57:24 +0400 Subject: [PATCH] Split greetings from AiWander (bug #4594) --- CHANGELOG.md | 1 + apps/openmw/mwmechanics/actors.cpp | 115 ++++++++++++++++++++++ apps/openmw/mwmechanics/actors.hpp | 2 + apps/openmw/mwmechanics/aitravel.cpp | 9 +- apps/openmw/mwmechanics/aiwander.cpp | 101 +++---------------- apps/openmw/mwmechanics/aiwander.hpp | 18 ---- apps/openmw/mwmechanics/creaturestats.cpp | 43 +++++++- apps/openmw/mwmechanics/creaturestats.hpp | 24 +++++ 8 files changed, 204 insertions(+), 109 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bafad417b..42044f1c8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Bug #4411: Reloading a saved game while falling prevents damage in some cases Bug #4456: AiActivate should not be cancelled after target activation Bug #4540: Rain delay when exiting water + Bug #4594: Actors without AI packages don't use Hello dialogue Bug #4600: Crash when no sound output is available or --no-sound is used. Bug #4639: Black screen after completing first mages guild mission + training Bug #4650: Focus is lost after pressing ESC in confirmation dialog inside savegame dialog diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 86c50ab705..462bf40e10 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -27,6 +27,7 @@ #include "../mwrender/vismask.hpp" #include "spellcasting.hpp" +#include "steering.hpp" #include "npcstats.hpp" #include "creaturestats.hpp" #include "movement.hpp" @@ -141,6 +142,9 @@ void getRestorationPerHourOfSleep (const MWWorld::Ptr& ptr, float& health, float namespace MWMechanics { + static const int GREETING_SHOULD_START = 4; //how many updates should pass before NPC can greet player + static const int GREETING_SHOULD_END = 10; + class GetStuntedMagickaDuration : public MWMechanics::EffectSourceVisitor { public: @@ -397,6 +401,113 @@ namespace MWMechanics MWBase::Environment::get().getDialogueManager()->say(actor, "idle"); } + void Actors::updateGreetingState(const MWWorld::Ptr& actor, bool turnOnly) + { + if (!actor.getClass().isActor() || actor == getPlayer()) + return; + + CreatureStats &stats = actor.getClass().getCreatureStats(actor); + int hello = stats.getAiSetting(CreatureStats::AI_Hello).getModified(); + if (hello == 0) + return; + + if (MWBase::Environment::get().getWorld()->isSwimming(actor)) + return; + + MWWorld::Ptr player = getPlayer(); + osg::Vec3f playerPos(player.getRefData().getPosition().asVec3()); + osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); + osg::Vec3f dir = playerPos - actorPos; + + const MWMechanics::AiSequence& seq = stats.getAiSequence(); + int packageId = seq.getTypeId(); + + if (seq.isInCombat() || (packageId != AiPackage::TypeIdWander && packageId != AiPackage::TypeIdTravel && packageId != -1)) + { + stats.setTurningToPlayer(false); + stats.setGreetingTimer(0); + stats.setGreetingState(Greet_None); + return; + } + + if (stats.isTurningToPlayer()) + { + // Reduce the turning animation glitch by using a *HUGE* value of + // epsilon... TODO: a proper fix might be in either the physics or the + // animation subsystem + if (zTurn(actor, stats.getAngleToPlayer(), osg::DegreesToRadians(5.f))) + { + stats.setTurningToPlayer(false); + // An original engine launches an endless idle2 when an actor greets player. + playAnimationGroup (actor, "idle2", 0, std::numeric_limits::max(), false); + } + } + + if (turnOnly) + return; + + // Play a random voice greeting if the player gets too close + float helloDistance = static_cast(hello); + static int iGreetDistanceMultiplier = MWBase::Environment::get().getWorld()->getStore() + .get().find("iGreetDistanceMultiplier")->mValue.getInteger(); + + helloDistance *= iGreetDistanceMultiplier; + + int greetingTimer = stats.getGreetingTimer(); + GreetingState greetingState = stats.getGreetingState(); + if (greetingState == Greet_None) + { + if ((playerPos - actorPos).length2() <= helloDistance*helloDistance && + !player.getClass().getCreatureStats(player).isDead() && !actor.getClass().getCreatureStats(actor).isParalyzed() + && MWBase::Environment::get().getWorld()->getLOS(player, actor) + && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, actor)) + greetingTimer++; + + if (greetingTimer >= GREETING_SHOULD_START) + { + greetingState = Greet_InProgress; + MWBase::Environment::get().getDialogueManager()->say(actor, "hello"); + greetingTimer = 0; + } + } + + if (greetingState == Greet_InProgress) + { + greetingTimer++; + + turnActorToFacePlayer(actor, dir); + + if (greetingTimer >= GREETING_SHOULD_END) + { + greetingState = Greet_Done; + greetingTimer = 0; + } + } + + if (greetingState == Greet_Done) + { + float resetDist = 2 * helloDistance; + if ((playerPos - actorPos).length2() >= resetDist*resetDist) + greetingState = Greet_None; + } + + stats.setGreetingTimer(greetingTimer); + stats.setGreetingState(greetingState); + } + + void Actors::turnActorToFacePlayer(const MWWorld::Ptr& actor, const osg::Vec3f& dir) + { + actor.getClass().getMovementSettings(actor).mPosition[1] = 0; + actor.getClass().getMovementSettings(actor).mPosition[0] = 0; + + CreatureStats &stats = actor.getClass().getCreatureStats(actor); + if (!stats.isTurningToPlayer()) + { + stats.setAngleToPlayer(std::atan2(dir.x(), dir.y())); + stats.setTurningToPlayer(true); + } + } + void Actors::engageCombat (const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, std::map >& cachedAllies, bool againstPlayer) { // No combat for totally static creatures @@ -1409,11 +1520,13 @@ namespace MWMechanics static float timerUpdateAITargets = 0; static float timerUpdateHeadTrack = 0; static float timerUpdateEquippedLight = 0; + static float timerUpdateHello = 0; const float updateEquippedLightInterval = 1.0f; // target lists get updated once every 1.0 sec if (timerUpdateAITargets >= 1.0f) timerUpdateAITargets = 0; if (timerUpdateHeadTrack >= 0.3f) timerUpdateHeadTrack = 0; + if (timerUpdateHello >= 0.25f) timerUpdateHello = 0; if (mTimerDisposeSummonsCorpses >= 0.2f) mTimerDisposeSummonsCorpses = 0; if (timerUpdateEquippedLight >= updateEquippedLightInterval) timerUpdateEquippedLight = 0; @@ -1532,6 +1645,7 @@ namespace MWMechanics if (isConscious(iter->first)) { stats.getAiSequence().execute(iter->first, *ctrl, duration); + updateGreetingState(iter->first, timerUpdateHello > 0); playIdleDialogue(iter->first); } } @@ -1554,6 +1668,7 @@ namespace MWMechanics timerUpdateAITargets += duration; timerUpdateHeadTrack += duration; timerUpdateEquippedLight += duration; + timerUpdateHello += duration; mTimerDisposeSummonsCorpses += duration; // Animation/movement update diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 68b3bb9767..04a7be8afe 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -121,6 +121,8 @@ namespace MWMechanics void engageCombat(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, std::map >& cachedAllies, bool againstPlayer); void playIdleDialogue(const MWWorld::Ptr& actor); + void updateGreetingState(const MWWorld::Ptr& actor, bool turnOnly); + void turnActorToFacePlayer(const MWWorld::Ptr& actor, const osg::Vec3f& dir); void updateHeadTracking(const MWWorld::Ptr& actor, const MWWorld::Ptr& targetActor, MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance); diff --git a/apps/openmw/mwmechanics/aitravel.cpp b/apps/openmw/mwmechanics/aitravel.cpp index d97edfe2a0..0f3b22e117 100644 --- a/apps/openmw/mwmechanics/aitravel.cpp +++ b/apps/openmw/mwmechanics/aitravel.cpp @@ -43,11 +43,16 @@ namespace MWMechanics bool AiTravel::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { + auto& stats = actor.getClass().getCreatureStats(actor); + + if (stats.isTurningToPlayer() || stats.getGreetingState() == Greet_InProgress) + return false; + const osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); const osg::Vec3f targetPos(mX, mY, mZ); - actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, false); - actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); + stats.setMovementFlag(CreatureStats::Flag_Run, false); + stats.setDrawState(DrawState_Nothing); if (!isWithinMaxRange(targetPos, actorPos)) return false; diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index cd13b18204..273246261b 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -17,7 +17,6 @@ #include "pathgrid.hpp" #include "creaturestats.hpp" -#include "steering.hpp" #include "movement.hpp" #include "coordinateconverter.hpp" #include "actorutil.hpp" @@ -26,8 +25,6 @@ namespace MWMechanics { static const int COUNT_BEFORE_RESET = 10; static const float DOOR_CHECK_INTERVAL = 1.5f; - static const int GREETING_SHOULD_START = 4; //how many reaction intervals should pass before NPC can greet player - static const int GREETING_SHOULD_END = 10; // to prevent overcrowding static const int DESTINATION_TOLERANCE = 64; @@ -176,6 +173,17 @@ namespace MWMechanics storage.setState(AiWanderStorage::Wander_Walking); } + GreetingState greetingState = cStats.getGreetingState(); + if (greetingState == Greet_InProgress) + { + if (storage.mState == AiWanderStorage::Wander_Walking) + { + stopWalking(actor, storage, false); + mObstacleCheck.clear(); + storage.setState(AiWanderStorage::Wander_IdleNow); + } + } + doPerFrameActionsForState(actor, duration, storage); float& lastReaction = storage.mReaction; @@ -245,13 +253,7 @@ namespace MWMechanics if(mDistance && cellChange) mDistance = 0; - // Allow interrupting a walking actor to trigger a greeting AiWanderStorage::WanderState& wanderState = storage.mState; - if ((wanderState == AiWanderStorage::Wander_IdleNow) || (wanderState == AiWanderStorage::Wander_Walking)) - { - playGreetingIfPlayerGetsTooClose(actor, storage); - } - if ((wanderState == AiWanderStorage::Wander_MoveNow) && storage.mCanWanderAlongPathGrid) { // Construct a new path if there isn't one @@ -416,19 +418,9 @@ namespace MWMechanics } } - bool& rotate = storage.mTurnActorGivingGreetingToFacePlayer; - if (rotate) - { - // Reduce the turning animation glitch by using a *HUGE* value of - // epsilon... TODO: a proper fix might be in either the physics or the - // animation subsystem - if (zTurn(actor, storage.mTargetAngleRadians, osg::DegreesToRadians(5.f))) - rotate = false; - } - // Check if idle animation finished - AiWanderStorage::GreetingState& greetingState = storage.mSaidGreeting; - if (!checkIdle(actor, storage.mIdleAnimation) && (greetingState == AiWanderStorage::Greet_Done || greetingState == AiWanderStorage::Greet_None)) + GreetingState greetingState = actor.getClass().getCreatureStats(actor).getGreetingState(); + if (!checkIdle(actor, storage.mIdleAnimation) && (greetingState == Greet_Done || greetingState == Greet_None)) { if (mPathFinder.isPathConstructed()) storage.setState(AiWanderStorage::Wander_Walking); @@ -517,74 +509,7 @@ namespace MWMechanics } } - void AiWander::playGreetingIfPlayerGetsTooClose(const MWWorld::Ptr& actor, AiWanderStorage& storage) - { - // Play a random voice greeting if the player gets too close - int hello = actor.getClass().getCreatureStats(actor).getAiSetting(CreatureStats::AI_Hello).getModified(); - float helloDistance = static_cast(hello); - static int iGreetDistanceMultiplier = MWBase::Environment::get().getWorld()->getStore() - .get().find("iGreetDistanceMultiplier")->mValue.getInteger(); - - helloDistance *= iGreetDistanceMultiplier; - - MWWorld::Ptr player = getPlayer(); - osg::Vec3f playerPos(player.getRefData().getPosition().asVec3()); - osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); - int& greetingTimer = storage.mGreetingTimer; - AiWanderStorage::GreetingState& greetingState = storage.mSaidGreeting; - if (greetingState == AiWanderStorage::Greet_None) - { - if ((playerPos - actorPos).length2() <= helloDistance*helloDistance && - !player.getClass().getCreatureStats(player).isDead() && !actor.getClass().getCreatureStats(actor).isParalyzed() - && MWBase::Environment::get().getWorld()->getLOS(player, actor) - && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, actor)) - greetingTimer++; - - if (greetingTimer >= GREETING_SHOULD_START) - { - greetingState = AiWanderStorage::Greet_InProgress; - MWBase::Environment::get().getDialogueManager()->say(actor, "hello"); - greetingTimer = 0; - } - } - - if (greetingState == AiWanderStorage::Greet_InProgress) - { - greetingTimer++; - - if (storage.mState == AiWanderStorage::Wander_Walking) - { - stopWalking(actor, storage, false); - mObstacleCheck.clear(); - storage.setState(AiWanderStorage::Wander_IdleNow); - } - - turnActorToFacePlayer(actorPos, playerPos, storage); - - if (greetingTimer >= GREETING_SHOULD_END) - { - greetingState = AiWanderStorage::Greet_Done; - greetingTimer = 0; - } - } - - if (greetingState == AiWanderStorage::Greet_Done) - { - float resetDist = 2 * helloDistance; - if ((playerPos - actorPos).length2() >= resetDist*resetDist) - greetingState = AiWanderStorage::Greet_None; - } - } - - void AiWander::turnActorToFacePlayer(const osg::Vec3f& actorPosition, const osg::Vec3f& playerPosition, AiWanderStorage& storage) - { - osg::Vec3f dir = playerPosition - actorPosition; - - float faceAngleRadians = std::atan2(dir.x(), dir.y()); - storage.mTargetAngleRadians = faceAngleRadians; - storage.mTurnActorGivingGreetingToFacePlayer = true; - } void AiWander::setPathToAnAllowedNode(const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos) { diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index 0b967983f8..72f9b32280 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -25,21 +25,8 @@ namespace MWMechanics /// \brief This class holds the variables AiWander needs which are deleted if the package becomes inactive. struct AiWanderStorage : AiTemporaryBase { - // the z rotation angle to reach - // when mTurnActorGivingGreetingToFacePlayer is true - float mTargetAngleRadians; - bool mTurnActorGivingGreetingToFacePlayer; float mReaction; // update some actions infrequently - enum GreetingState - { - Greet_None, - Greet_InProgress, - Greet_Done - }; - GreetingState mSaidGreeting; - int mGreetingTimer; - const MWWorld::CellStore* mCell; // for detecting cell change // AiWander states @@ -72,11 +59,7 @@ namespace MWMechanics int mStuckCount; AiWanderStorage(): - mTargetAngleRadians(0), - mTurnActorGivingGreetingToFacePlayer(false), mReaction(0), - mSaidGreeting(Greet_None), - mGreetingTimer(0), mCell(nullptr), mState(Wander_ChooseAction), mIsWanderingManually(false), @@ -136,7 +119,6 @@ namespace MWMechanics bool checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); short unsigned getRandomIdle(); void setPathToAnAllowedNode(const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos); - void playGreetingIfPlayerGetsTooClose(const MWWorld::Ptr& actor, AiWanderStorage& storage); void evadeObstacles(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage); void turnActorToFacePlayer(const osg::Vec3f& actorPosition, const osg::Vec3f& playerPosition, AiWanderStorage& storage); void doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage); diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp index 5a80779da9..2365876695 100644 --- a/apps/openmw/mwmechanics/creaturestats.cpp +++ b/apps/openmw/mwmechanics/creaturestats.cpp @@ -23,12 +23,53 @@ namespace MWMechanics mKnockdown(false), mKnockdownOneFrame(false), mKnockdownOverOneFrame(false), mHitRecovery(false), mBlock(false), mMovementFlags(0), mFallHeight(0), mRecalcMagicka(false), mLastRestock(0,0), mGoldPool(0), mActorId(-1), mHitAttemptActorId(-1), - mDeathAnimation(-1), mTimeOfDeath(), mLevel (0) + mDeathAnimation(-1), mTimeOfDeath(), mGreetingState(Greet_None), + mGreetingTimer(0), mTargetAngleRadians(0), mIsTurningToPlayer(false), mLevel (0) { for (int i=0; i<4; ++i) mAiSettings[i] = 0; } + int MWMechanics::CreatureStats::getGreetingTimer() const + { + return mGreetingTimer; + } + + void MWMechanics::CreatureStats::setGreetingTimer(int timer) + { + mGreetingTimer = timer; + } + + float MWMechanics::CreatureStats::getAngleToPlayer() const + { + return mTargetAngleRadians; + } + + void MWMechanics::CreatureStats::setAngleToPlayer(float angle) + { + mTargetAngleRadians = angle; + } + + GreetingState MWMechanics::CreatureStats::getGreetingState() const + { + return mGreetingState; + } + + void MWMechanics::CreatureStats::setGreetingState(GreetingState state) + { + mGreetingState = state; + } + + bool MWMechanics::CreatureStats::isTurningToPlayer() const + { + return mIsTurningToPlayer; + } + + void MWMechanics::CreatureStats::setTurningToPlayer(bool turning) + { + mIsTurningToPlayer = turning; + } + const AiSequence& CreatureStats::getAiSequence() const { return mAiSequence; diff --git a/apps/openmw/mwmechanics/creaturestats.hpp b/apps/openmw/mwmechanics/creaturestats.hpp index 503ac7d68a..6ec4cc3a17 100644 --- a/apps/openmw/mwmechanics/creaturestats.hpp +++ b/apps/openmw/mwmechanics/creaturestats.hpp @@ -19,6 +19,13 @@ namespace ESM namespace MWMechanics { + enum GreetingState + { + Greet_None, + Greet_InProgress, + Greet_Done + }; + /// \brief Common creature stats /// /// @@ -70,6 +77,11 @@ namespace MWMechanics MWWorld::TimeStamp mTimeOfDeath; + GreetingState mGreetingState; + int mGreetingTimer; + float mTargetAngleRadians; + bool mIsTurningToPlayer; + public: typedef std::pair SummonKey; // private: @@ -85,6 +97,18 @@ namespace MWMechanics public: CreatureStats(); + int getGreetingTimer() const; + void setGreetingTimer(int timer); + + float getAngleToPlayer() const; + void setAngleToPlayer(float angle); + + GreetingState getGreetingState() const; + void setGreetingState(GreetingState state); + + bool isTurningToPlayer() const; + void setTurningToPlayer(bool turning); + DrawState_ getDrawState() const; void setDrawState(DrawState_ state);