From 367bdcf0cc5678951b35953d4b8d2722020ab362 Mon Sep 17 00:00:00 2001 From: Matt <3397065-ZehMatt@users.noreply.gitlab.com> Date: Sat, 12 Feb 2022 23:50:41 +0000 Subject: [PATCH] #6091: Optimize isInCombat --- apps/openmw/mwlua/localscripts.cpp | 13 +- apps/openmw/mwmechanics/actors.cpp | 21 +- apps/openmw/mwmechanics/aisequence.cpp | 282 ++++++++++-------- apps/openmw/mwmechanics/aisequence.hpp | 52 +++- .../mwmechanics/mechanicsmanagerimp.cpp | 8 +- apps/openmw/mwmechanics/spelleffects.cpp | 4 +- 6 files changed, 219 insertions(+), 161 deletions(-) diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp index 552ce77541..c95eae43a7 100644 --- a/apps/openmw/mwlua/localscripts.cpp +++ b/apps/openmw/mwlua/localscripts.cpp @@ -113,15 +113,12 @@ namespace MWLua { const MWWorld::Ptr& ptr = self.ptr(); MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); - std::list>& list = ai.getUnderlyingList(); - for (auto it = list.begin(); it != list.end();) + + ai.erasePackagesIf([&](auto& entry) { - bool keep = LuaUtil::call(callback, *it).get(); - if (keep) - ++it; - else - it = list.erase(it); - } + bool keep = LuaUtil::call(callback, entry).template get(); + return !keep; + }); }; selfAPI["_startAiCombat"] = [](SelfObject& self, const LObject& target) { diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 2f447e12d6..b6275070e0 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -73,23 +73,20 @@ bool isCommanded(const MWWorld::Ptr& actor) // Check for command effects having ended and remove package if necessary void adjustCommandedActor (const MWWorld::Ptr& actor) { + if (!isCommanded(actor)) + return; + MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); - bool hasCommandPackage = false; - - auto it = stats.getAiSequence().begin(); - for (; it != stats.getAiSequence().end(); ++it) + stats.getAiSequence().erasePackageIf([](auto& entry) { - if ((*it)->getTypeId() == MWMechanics::AiPackageTypeId::Follow && - static_cast(it->get())->isCommanded()) + if (entry->getTypeId() == MWMechanics::AiPackageTypeId::Follow && + static_cast(entry.get())->isCommanded()) { - hasCommandPackage = true; - break; + return true; } - } - - if (!isCommanded(actor) && hasCommandPackage) - stats.getAiSequence().erase(it); + return false; + }); } void getRestorationPerHourOfSleep (const MWWorld::Ptr& ptr, float& health, float& magicka) diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index 86bc714964..a1f2b5c3e3 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -1,6 +1,7 @@ #include "aisequence.hpp" #include +#include #include #include @@ -29,6 +30,9 @@ void AiSequence::copy (const AiSequence& sequence) // We need to keep an AiWander storage, if present - it has a state machine. // Not sure about another temporary storages sequence.mAiState.copy(mAiState); + + mNumCombatPackages = sequence.mNumCombatPackages; + mNumPursuitPackages = sequence.mNumPursuitPackages; } AiSequence::AiSequence() : mDone (false), mLastAiPackage(AiPackageTypeId::None) {} @@ -58,6 +62,28 @@ AiSequence::~AiSequence() clear(); } +void AiSequence::onPackageAdded(const AiPackage& package) +{ + if (package.getTypeId() == AiPackageTypeId::Combat) + mNumCombatPackages++; + else if (package.getTypeId() == AiPackageTypeId::Pursue) + mNumPursuitPackages++; + + assert(mNumCombatPackages >= 0); + assert(mNumPursuitPackages >= 0); +} + +void AiSequence::onPackageRemoved(const AiPackage& package) +{ + if (package.getTypeId() == AiPackageTypeId::Combat) + mNumCombatPackages--; + else if (package.getTypeId() == AiPackageTypeId::Pursue) + mNumPursuitPackages--; + + assert(mNumCombatPackages >= 0); + assert(mNumPursuitPackages >= 0); +} + AiPackageTypeId AiSequence::getTypeId() const { if (mPackages.empty()) @@ -87,32 +113,30 @@ bool AiSequence::getCombatTargets(std::vector &targetActors) const return !targetActors.empty(); } -void AiSequence::erase(std::list>::const_iterator package) +AiPackages::iterator AiSequence::erase(AiPackages::iterator package) { // Not sure if manually terminated packages should trigger mDone, probably not? - for(auto it = mPackages.begin(); it != mPackages.end(); ++it) - { - if (package == it) - { - mPackages.erase(it); - return; - } - } - throw std::runtime_error("can't find package to erase"); + auto& ptr = *package; + onPackageRemoved(*ptr); + + return mPackages.erase(package); } bool AiSequence::isInCombat() const { - for (auto it = mPackages.begin(); it != mPackages.end(); ++it) - { - if ((*it)->getTypeId() == AiPackageTypeId::Combat) - return true; - } - return false; + return mNumCombatPackages > 0; +} + +bool AiSequence::isInPursuit() const +{ + return mNumPursuitPackages > 0; } bool AiSequence::isEngagedWithActor() const { + if (!isInCombat()) + return false; + for (auto it = mPackages.begin(); it != mPackages.end(); ++it) { if ((*it)->getTypeId() == AiPackageTypeId::Combat) @@ -127,16 +151,18 @@ bool AiSequence::isEngagedWithActor() const bool AiSequence::hasPackage(AiPackageTypeId typeId) const { - for (auto it = mPackages.begin(); it != mPackages.end(); ++it) + auto it = std::find_if(mPackages.begin(), mPackages.end(), [typeId](const auto& package) { - if ((*it)->getTypeId() == typeId) - return true; - } - return false; + return package->getTypeId() == typeId; + }); + return it != mPackages.end(); } bool AiSequence::isInCombat(const MWWorld::Ptr &actor) const { + if (!isInCombat()) + return false; + for (auto it = mPackages.begin(); it != mPackages.end(); ++it) { if ((*it)->getTypeId() == AiPackageTypeId::Combat) @@ -148,27 +174,31 @@ bool AiSequence::isInCombat(const MWWorld::Ptr &actor) const return false; } -// TODO: use std::list::remove_if for all these methods when we switch to C++20 -void AiSequence::stopCombat() +void AiSequence::removePackagesById(AiPackageTypeId id) { - for(auto it = mPackages.begin(); it != mPackages.end(); ) + for (auto it = mPackages.begin(); it != mPackages.end(); ) { - if ((*it)->getTypeId() == AiPackageTypeId::Combat) + if ((*it)->getTypeId() == id) { - it = mPackages.erase(it); + it = erase(it); } else ++it; } } +void AiSequence::stopCombat() +{ + removePackagesById(AiPackageTypeId::Combat); +} + void AiSequence::stopCombat(const std::vector& targets) { for(auto it = mPackages.begin(); it != mPackages.end(); ) { if ((*it)->getTypeId() == AiPackageTypeId::Combat && std::find(targets.begin(), targets.end(), (*it)->getTarget()) != targets.end()) { - it = mPackages.erase(it); + it = erase(it); } else ++it; @@ -177,15 +207,7 @@ void AiSequence::stopCombat(const std::vector& targets) void AiSequence::stopPursuit() { - for(auto it = mPackages.begin(); it != mPackages.end(); ) - { - if ((*it)->getTypeId() == AiPackageTypeId::Pursue) - { - it = mPackages.erase(it); - } - else - ++it; - } + removePackagesById(AiPackageTypeId::Pursue); } bool AiSequence::isPackageDone() const @@ -204,112 +226,117 @@ namespace void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& characterController, float duration, bool outOfRange) { - if(actor != getPlayer()) + if (actor == getPlayer()) { - if (mPackages.empty()) + // Players don't use this. + return; + } + + if (mPackages.empty()) + { + mLastAiPackage = AiPackageTypeId::None; + return; + } + + auto packageIt = mPackages.begin(); + MWMechanics::AiPackage* package = packageIt->get(); + if (!package->alwaysActive() && outOfRange) + return; + + auto packageTypeId = package->getTypeId(); + // workaround ai packages not being handled as in the vanilla engine + if (isActualAiPackage(packageTypeId)) + mLastAiPackage = packageTypeId; + // if active package is combat one, choose nearest target + if (packageTypeId == AiPackageTypeId::Combat) + { + auto itActualCombat = mPackages.end(); + + float nearestDist = std::numeric_limits::max(); + osg::Vec3f vActorPos = actor.getRefData().getPosition().asVec3(); + + float bestRating = 0.f; + + for (auto it = mPackages.begin(); it != mPackages.end();) { - mLastAiPackage = AiPackageTypeId::None; - return; - } + if ((*it)->getTypeId() != AiPackageTypeId::Combat) break; - auto packageIt = mPackages.begin(); - MWMechanics::AiPackage* package = packageIt->get(); - if (!package->alwaysActive() && outOfRange) - return; + MWWorld::Ptr target = (*it)->getTarget(); - auto packageTypeId = package->getTypeId(); - // workaround ai packages not being handled as in the vanilla engine - if (isActualAiPackage(packageTypeId)) - mLastAiPackage = packageTypeId; - // if active package is combat one, choose nearest target - if (packageTypeId == AiPackageTypeId::Combat) - { - auto itActualCombat = mPackages.end(); - - float nearestDist = std::numeric_limits::max(); - osg::Vec3f vActorPos = actor.getRefData().getPosition().asVec3(); - - float bestRating = 0.f; - - for (auto it = mPackages.begin(); it != mPackages.end();) + // target disappeared (e.g. summoned creatures) + if (target.isEmpty()) { - if ((*it)->getTypeId() != AiPackageTypeId::Combat) break; - - MWWorld::Ptr target = (*it)->getTarget(); - - // target disappeared (e.g. summoned creatures) - if (target.isEmpty()) - { - it = mPackages.erase(it); - } - else - { - float rating = MWMechanics::getBestActionRating(actor, target); - - const ESM::Position &targetPos = target.getRefData().getPosition(); - - float distTo = (targetPos.asVec3() - vActorPos).length2(); - - // Small threshold for changing target - if (it == mPackages.begin()) - distTo = std::max(0.f, distTo - 2500.f); - - // if a target has higher priority than current target or has same priority but closer - if (rating > bestRating || ((distTo < nearestDist) && rating == bestRating)) - { - nearestDist = distTo; - itActualCombat = it; - bestRating = rating; - } - ++it; - } - } - - assert(!mPackages.empty()); - - if (nearestDist < std::numeric_limits::max() && mPackages.begin() != itActualCombat) - { - assert(itActualCombat != mPackages.end()); - // move combat package with nearest target to the front - mPackages.splice(mPackages.begin(), mPackages, itActualCombat); - } - - packageIt = mPackages.begin(); - package = packageIt->get(); - packageTypeId = package->getTypeId(); - } - - try - { - if (package->execute(actor, characterController, mAiState, duration)) - { - // Put repeating noncombat AI packages on the end of the stack so they can be used again - if (isActualAiPackage(packageTypeId) && package->getRepeat()) - { - package->reset(); - mPackages.push_back(package->clone()); - } - // To account for the rare case where AiPackage::execute() queued another AI package - // (e.g. AiPursue executing a dialogue script that uses startCombat) - mPackages.erase(packageIt); - if (isActualAiPackage(packageTypeId)) - mDone = true; + it = erase(it); } else { - mDone = false; + float rating = MWMechanics::getBestActionRating(actor, target); + + const ESM::Position &targetPos = target.getRefData().getPosition(); + + float distTo = (targetPos.asVec3() - vActorPos).length2(); + + // Small threshold for changing target + if (it == mPackages.begin()) + distTo = std::max(0.f, distTo - 2500.f); + + // if a target has higher priority than current target or has same priority but closer + if (rating > bestRating || ((distTo < nearestDist) && rating == bestRating)) + { + nearestDist = distTo; + itActualCombat = it; + bestRating = rating; + } + ++it; } } - catch (std::exception& e) + + assert(!mPackages.empty()); + + if (nearestDist < std::numeric_limits::max() && mPackages.begin() != itActualCombat) { - Log(Debug::Error) << "Error during AiSequence::execute: " << e.what(); + assert(itActualCombat != mPackages.end()); + // move combat package with nearest target to the front + std::rotate(mPackages.begin(), itActualCombat, std::next(itActualCombat)); } + + packageIt = mPackages.begin(); + package = packageIt->get(); + packageTypeId = package->getTypeId(); + } + + try + { + if (package->execute(actor, characterController, mAiState, duration)) + { + // Put repeating noncombat AI packages on the end of the stack so they can be used again + if (isActualAiPackage(packageTypeId) && package->getRepeat()) + { + package->reset(); + mPackages.push_back(package->clone()); + } + // To account for the rare case where AiPackage::execute() queued another AI package + // (e.g. AiPursue executing a dialogue script that uses startCombat) + erase(packageIt); + if (isActualAiPackage(packageTypeId)) + mDone = true; + } + else + { + mDone = false; + } + } + catch (std::exception& e) + { + Log(Debug::Error) << "Error during AiSequence::execute: " << e.what(); } } void AiSequence::clear() { mPackages.clear(); + mNumCombatPackages = 0; + mNumPursuitPackages = 0; } void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, bool cancelOther) @@ -353,7 +380,7 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, boo { if((*it)->canCancel()) { - it = mPackages.erase(it); + it = erase(it); } else ++it; @@ -373,11 +400,13 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, boo if((*it)->getPriority() <= package.getPriority()) { + onPackageAdded(package); mPackages.insert(it, package.clone()); return; } } + onPackageAdded(package); mPackages.push_back(package.clone()); // Make sure that temporary storage is empty @@ -435,6 +464,8 @@ void AiSequence::fill(const ESM::AIPackageList &list) ESM::AITarget data = esmPackage.mTarget; package = std::make_unique(data.mId.toStringView(), data.mDuration, data.mX, data.mY, data.mZ, data.mShouldRepeat != 0); } + + onPackageAdded(*package); mPackages.push_back(std::move(package)); } } @@ -504,6 +535,7 @@ void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence) if (!package.get()) continue; + onPackageAdded(*package); mPackages.push_back(std::move(package)); } diff --git a/apps/openmw/mwmechanics/aisequence.hpp b/apps/openmw/mwmechanics/aisequence.hpp index 6cbfcf045d..0d23207b63 100644 --- a/apps/openmw/mwmechanics/aisequence.hpp +++ b/apps/openmw/mwmechanics/aisequence.hpp @@ -1,8 +1,9 @@ #ifndef GAME_MWMECHANICS_AISEQUENCE_H #define GAME_MWMECHANICS_AISEQUENCE_H -#include #include +#include +#include #include "aistate.hpp" #include "aipackagetypeid.hpp" @@ -22,8 +23,6 @@ namespace ESM } } - - namespace MWMechanics { class AiPackage; @@ -33,15 +32,20 @@ namespace MWMechanics struct AiTemporaryBase; typedef DerivedClassStorage AiState; + using AiPackages = std::vector>; + /// \brief Sequence of AI-packages for a single actor /** The top-most AI package is run each frame. When completed, it is removed from the stack. **/ class AiSequence { ///AiPackages to run though - std::list> mPackages; + AiPackages mPackages; ///Finished with top AIPackage, set for one frame - bool mDone; + bool mDone{}; + + int mNumCombatPackages{}; + int mNumPursuitPackages{}; ///Copy AiSequence void copy (const AiSequence& sequence); @@ -50,6 +54,11 @@ namespace MWMechanics AiPackageTypeId mLastAiPackage; AiState mAiState; + void onPackageAdded(const AiPackage& package); + void onPackageRemoved(const AiPackage& package); + + AiPackages::iterator erase(AiPackages::iterator package); + public: ///Default constructor AiSequence(); @@ -63,12 +72,31 @@ namespace MWMechanics virtual ~AiSequence(); /// Iterator may be invalidated by any function calls other than begin() or end(). - std::list>::const_iterator begin() const { return mPackages.begin(); } - std::list>::const_iterator end() const { return mPackages.end(); } + AiPackages::const_iterator begin() const { return mPackages.begin(); } + AiPackages::const_iterator end() const { return mPackages.end(); } - void erase(std::list>::const_iterator package); + /// Removes all packages controlled by the predicate. + template + void erasePackagesIf(const F&& pred) + { + mPackages.erase(std::remove_if(mPackages.begin(), mPackages.end(), [&](auto& entry) + { + const bool doRemove = pred(entry); + if (doRemove) + onPackageRemoved(*entry); + return doRemove; + }), mPackages.end()); + } - std::list>& getUnderlyingList() { return mPackages; } + /// Removes a single package controlled by the predicate. + template + void erasePackageIf(const F&& pred) + { + auto it = std::find_if(mPackages.begin(), mPackages.end(), pred); + if (it == mPackages.end()) + return; + erase(it); + } /// Returns currently executing AiPackage type /** \see enum class AiPackageTypeId **/ @@ -89,6 +117,12 @@ namespace MWMechanics /// Is there any combat package? bool isInCombat () const; + /// Is there any pursuit package. + bool isInPursuit() const; + + /// Removes all packages using the specified id. + void removePackagesById(AiPackageTypeId id); + /// Are we in combat with any other actor, who's also engaging us? bool isEngagedWithActor () const; diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 0816445271..dc349a1e6b 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -1318,7 +1318,7 @@ namespace MWMechanics // once the bounty has been paid. actor.getClass().getNpcStats(actor).setCrimeId(id); - if (!actor.getClass().getCreatureStats(actor).getAiSequence().hasPackage(AiPackageTypeId::Pursue)) + if (!actor.getClass().getCreatureStats(actor).getAiSequence().isInPursuit()) { actor.getClass().getCreatureStats(actor).getAiSequence().stack(AiPursue(player), actor); } @@ -1396,7 +1396,7 @@ namespace MWMechanics { // Attacker is in combat with us, but we are not in combat with the attacker yet. Time to fight back. // Note: accidental or collateral damage attacks are ignored. - if (!victim.getClass().getCreatureStats(victim).getAiSequence().hasPackage(AiPackageTypeId::Pursue)) + if (!victim.getClass().getCreatureStats(victim).getAiSequence().isInPursuit()) startCombat(victim, player); // Set the crime ID, which we will use to calm down participants @@ -1442,7 +1442,7 @@ namespace MWMechanics { // Attacker is in combat with us, but we are not in combat with the attacker yet. Time to fight back. // Note: accidental or collateral damage attacks are ignored. - if (!target.getClass().getCreatureStats(target).getAiSequence().hasPackage(AiPackageTypeId::Pursue)) + if (!target.getClass().getCreatureStats(target).getAiSequence().isInPursuit()) { // If an actor has OnPCHitMe declared in his script, his Fight = 0 and the attacker is player, // he will attack the player only if we will force him (e.g. via StartCombat console command) @@ -1467,7 +1467,7 @@ namespace MWMechanics const MWMechanics::AiSequence& seq = target.getClass().getCreatureStats(target).getAiSequence(); return target.getClass().isNpc() && !attacker.isEmpty() && !seq.isInCombat(attacker) && !isAggressive(target, attacker) && !seq.isEngagedWithActor() - && !target.getClass().getCreatureStats(target).getAiSequence().hasPackage(AiPackageTypeId::Pursue); + && !target.getClass().getCreatureStats(target).getAiSequence().isInPursuit(); } void MechanicsManager::actorKilled(const MWWorld::Ptr &victim, const MWWorld::Ptr &attacker) diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index d0584791a5..3ae6de23da 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -980,12 +980,10 @@ void removeMagicEffect(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellPara if(magnitudes.get(effect.mEffectId).getMagnitude() <= 0.f) { auto& seq = target.getClass().getCreatureStats(target).getAiSequence(); - auto it = std::find_if(seq.begin(), seq.end(), [&](const auto& package) + seq.erasePackageIf([&](const auto& package) { return package->getTypeId() == MWMechanics::AiPackageTypeId::Follow && static_cast(package.get())->isCommanded(); }); - if(it != seq.end()) - seq.erase(it); } break; case ESM::MagicEffect::ExtraSpell: