Skip to content

Commit

Permalink
Merge branch 'sworc' into 'master'
Browse files Browse the repository at this point in the history
Make StartCombat a no-op for dead targets and don't always play attack lines

Closes #7769 and #5413

See merge request OpenMW/openmw!3803
  • Loading branch information
jvoisin committed Feb 5, 2024
2 parents a5d88d4 + 340d142 commit 838785d
Show file tree
Hide file tree
Showing 14 changed files with 161 additions and 87 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
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
Bug #5413: Enemies do a battlecry everytime the player summons a creature
Bug #5714: Touch spells cast using ExplodeSpell don't always explode
Bug #5849: Paralysis breaks landing
Bug #5870: Disposing of actors who were selected in the console doesn't deselect them like vanilla
Expand Down Expand Up @@ -132,6 +133,7 @@
Bug #7758: Water walking is not taken into account to compute path cost on the water
Bug #7761: Rain and ambient loop sounds are mutually exclusive
Bug #7765: OpenMW-CS: Touch Record option is broken
Bug #7769: Sword of the Perithia: Broken NPCs
Bug #7770: Sword of the Perithia: Script execution failure
Bug #7780: Non-ASCII texture paths in NIF files don't work
Bug #7785: OpenMW-CS initialising Skill and Attribute fields to 0 instead of -1 on non-FortifyStat spells
Expand Down
4 changes: 3 additions & 1 deletion apps/openmw/mwbase/mechanicsmanager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ namespace MWBase
virtual bool awarenessCheck(const MWWorld::Ptr& ptr, const MWWorld::Ptr& observer) = 0;

/// Makes \a ptr fight \a target. Also shouts a combat taunt.
virtual void startCombat(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) = 0;
virtual void startCombat(
const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, const std::set<MWWorld::Ptr>* targetAllies)
= 0;

/// Removes an actor and its allies from combat with the actor's targets.
virtual void stopCombat(const MWWorld::Ptr& ptr) = 0;
Expand Down
97 changes: 46 additions & 51 deletions apps/openmw/mwmechanics/actors.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -577,8 +577,8 @@ namespace MWMechanics
}
}

void Actors::engageCombat(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2,
std::map<const MWWorld::Ptr, const std::set<MWWorld::Ptr>>& cachedAllies, bool againstPlayer) const
void Actors::engageCombat(
const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, SidingCache& cachedAllies, bool againstPlayer) const
{
// No combat for totally static creatures
if (!actor1.getClass().isMobile(actor1))
Expand Down Expand Up @@ -606,9 +606,7 @@ namespace MWMechanics

// Get actors allied with actor1. Includes those following or escorting actor1, actors following or escorting
// those actors, (recursive) and any actor currently being followed or escorted by actor1
std::set<MWWorld::Ptr> allies1;

getActorsSidingWith(actor1, allies1, cachedAllies);
const std::set<MWWorld::Ptr>& allies1 = cachedAllies.getActorsSidingWith(actor1);

const auto mechanicsManager = MWBase::Environment::get().getMechanicsManager();
// If an ally of actor1 has been attacked by actor2 or has attacked actor2, start combat between actor1 and
Expand All @@ -620,7 +618,7 @@ namespace MWMechanics

if (creatureStats2.matchesActorId(ally.getClass().getCreatureStats(ally).getHitAttemptActorId()))
{
mechanicsManager->startCombat(actor1, actor2);
mechanicsManager->startCombat(actor1, actor2, &cachedAllies.getActorsSidingWith(actor2));
// Also set the same hit attempt actor. Otherwise, if fighting the player, they may stop combat
// if the player gets out of reach, while the ally would continue combat with the player
creatureStats1.setHitAttemptActorId(ally.getClass().getCreatureStats(ally).getHitAttemptActorId());
Expand All @@ -633,9 +631,8 @@ namespace MWMechanics
aggressive = true;
}

std::set<MWWorld::Ptr> playerAllies;
MWWorld::Ptr player = MWMechanics::getPlayer();
getActorsSidingWith(player, playerAllies, cachedAllies);
const std::set<MWWorld::Ptr>& playerAllies = cachedAllies.getActorsSidingWith(player);

bool isPlayerFollowerOrEscorter = playerAllies.find(actor1) != playerAllies.end();

Expand All @@ -646,20 +643,17 @@ namespace MWMechanics
// Check that actor2 is in combat with actor1
if (creatureStats2.getAiSequence().isInCombat(actor1))
{
std::set<MWWorld::Ptr> allies2;

getActorsSidingWith(actor2, allies2, cachedAllies);

const std::set<MWWorld::Ptr>& allies2 = cachedAllies.getActorsSidingWith(actor2);
// Check that an ally of actor2 is also in combat with actor1
for (const MWWorld::Ptr& ally2 : allies2)
{
if (ally2 != actor2 && ally2.getClass().getCreatureStats(ally2).getAiSequence().isInCombat(actor1))
{
mechanicsManager->startCombat(actor1, actor2);
mechanicsManager->startCombat(actor1, actor2, &allies2);
// Also have actor1's allies start combat
for (const MWWorld::Ptr& ally1 : allies1)
if (ally1 != player)
mechanicsManager->startCombat(ally1, actor2);
mechanicsManager->startCombat(ally1, actor2, &allies2);
return;
}
}
Expand Down Expand Up @@ -747,7 +741,7 @@ namespace MWMechanics
bool LOS = world->getLOS(actor1, actor2) && mechanicsManager->awarenessCheck(actor2, actor1);

if (LOS)
mechanicsManager->startCombat(actor1, actor2);
mechanicsManager->startCombat(actor1, actor2, &cachedAllies.getActorsSidingWith(actor2));
}
}

Expand Down Expand Up @@ -1093,7 +1087,7 @@ namespace MWMechanics
}
}

void Actors::updateCrimePursuit(const MWWorld::Ptr& ptr, float duration) const
void Actors::updateCrimePursuit(const MWWorld::Ptr& ptr, float duration, SidingCache& cachedAllies) const
{
const MWWorld::Ptr player = getPlayer();
if (ptr == player)
Expand Down Expand Up @@ -1133,7 +1127,7 @@ namespace MWMechanics
= esmStore.get<ESM::GameSetting>().find("iCrimeThresholdMultiplier")->mValue.getInteger();
if (playerStats.getBounty() >= cutoff * iCrimeThresholdMultiplier)
{
mechanicsManager->startCombat(ptr, player);
mechanicsManager->startCombat(ptr, player, &cachedAllies.getActorsSidingWith(player));
creatureStats.setHitAttemptActorId(
playerClass.getCreatureStats(player)
.getActorId()); // Stops the guard from quitting combat if player is unreachable
Expand Down Expand Up @@ -1508,8 +1502,7 @@ namespace MWMechanics

/// \todo move update logic to Actor class where appropriate

std::map<const MWWorld::Ptr, const std::set<MWWorld::Ptr>>
cachedAllies; // will be filled as engageCombat iterates
SidingCache cachedAllies{ *this, true }; // will be filled as engageCombat iterates

const bool aiActive = MWBase::Environment::get().getMechanicsManager()->isAIActive();
const int attackedByPlayerId = player.getClass().getCreatureStats(player).getHitAttemptActorId();
Expand Down Expand Up @@ -1597,7 +1590,7 @@ namespace MWMechanics
updateHeadTracking(actor.getPtr(), mActors, isPlayer, ctrl);

if (actor.getPtr().getClass().isNpc() && !isPlayer)
updateCrimePursuit(actor.getPtr(), duration);
updateCrimePursuit(actor.getPtr(), duration, cachedAllies);

if (!isPlayer)
{
Expand Down Expand Up @@ -2115,7 +2108,7 @@ namespace MWMechanics
for (const auto& package : stats.getAiSequence())
{
if (excludeInfighting && !sameActor && package->getTypeId() == AiPackageTypeId::Combat
&& package->getTarget() == actorPtr)
&& package->targetIs(actorPtr))
break;
if (package->sideWithTarget() && !package->getTarget().isEmpty())
{
Expand All @@ -2135,7 +2128,7 @@ namespace MWMechanics
}
list.push_back(package->getTarget());
}
else if (package->getTarget() == actorPtr)
else if (package->targetIs(actorPtr))
{
list.push_back(iteratedActor);
}
Expand All @@ -2154,7 +2147,7 @@ namespace MWMechanics
std::vector<MWWorld::Ptr> list;
forEachFollowingPackage(
mActors, actorPtr, getPlayer(), [&](const Actor& actor, const std::shared_ptr<AiPackage>& package) {
if (package->followTargetThroughDoors() && package->getTarget() == actorPtr)
if (package->followTargetThroughDoors() && package->targetIs(actorPtr))
list.push_back(actor.getPtr());
else if (package->getTypeId() != AiPackageTypeId::Combat
&& package->getTypeId() != AiPackageTypeId::Wander)
Expand All @@ -2181,38 +2174,12 @@ namespace MWMechanics
getActorsSidingWith(follower, out, excludeInfighting);
}

void Actors::getActorsSidingWith(const MWWorld::Ptr& actor, std::set<MWWorld::Ptr>& out,
std::map<const MWWorld::Ptr, const std::set<MWWorld::Ptr>>& cachedAllies) const
{
// If we have already found actor's allies, use the cache
std::map<const MWWorld::Ptr, const std::set<MWWorld::Ptr>>::const_iterator search = cachedAllies.find(actor);
if (search != cachedAllies.end())
out.insert(search->second.begin(), search->second.end());
else
{
for (const MWWorld::Ptr& follower : getActorsSidingWith(actor, true))
if (out.insert(follower).second && follower != actor)
getActorsSidingWith(follower, out, cachedAllies);

// Cache ptrs and their sets of allies
cachedAllies.insert(std::make_pair(actor, out));
for (const MWWorld::Ptr& iter : out)
{
if (iter == actor)
continue;
search = cachedAllies.find(iter);
if (search == cachedAllies.end())
cachedAllies.insert(std::make_pair(iter, out));
}
}
}

std::vector<int> Actors::getActorsFollowingIndices(const MWWorld::Ptr& actor) const
{
std::vector<int> list;
forEachFollowingPackage(
mActors, actor, getPlayer(), [&](const Actor&, const std::shared_ptr<AiPackage>& package) {
if (package->followTargetThroughDoors() && package->getTarget() == actor)
if (package->followTargetThroughDoors() && package->targetIs(actor))
{
list.push_back(static_cast<const AiFollow*>(package.get())->getFollowIndex());
return false;
Expand All @@ -2230,7 +2197,7 @@ namespace MWMechanics
std::map<int, MWWorld::Ptr> map;
forEachFollowingPackage(
mActors, actor, getPlayer(), [&](const Actor& otherActor, const std::shared_ptr<AiPackage>& package) {
if (package->followTargetThroughDoors() && package->getTarget() == actor)
if (package->followTargetThroughDoors() && package->targetIs(actor))
{
const int index = static_cast<const AiFollow*>(package.get())->getFollowIndex();
map[index] = otherActor.getPtr();
Expand Down Expand Up @@ -2405,4 +2372,32 @@ namespace MWMechanics
seq.fastForward(ptr);
}
}

const std::set<MWWorld::Ptr>& SidingCache::getActorsSidingWith(const MWWorld::Ptr& actor)
{
// If we have already found actor's allies, use the cache
auto search = mCache.find(actor);
if (search != mCache.end())
return search->second;
std::set<MWWorld::Ptr>& out = mCache[actor];
for (const MWWorld::Ptr& follower : mActors.getActorsSidingWith(actor, mExcludeInfighting))
{
if (out.insert(follower).second && follower != actor)
{
const auto& allies = getActorsSidingWith(follower);
out.insert(allies.begin(), allies.end());
}
}

// Cache ptrs and their sets of allies
for (const MWWorld::Ptr& iter : out)
{
if (iter == actor)
continue;
search = mCache.find(iter);
if (search == mCache.end())
mCache.emplace(iter, out);
}
return out;
}
}
27 changes: 20 additions & 7 deletions apps/openmw/mwmechanics/actors.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ namespace MWMechanics
class Actor;
class CharacterController;
class CreatureStats;
class SidingCache;

class Actors
{
Expand Down Expand Up @@ -186,7 +187,7 @@ namespace MWMechanics

void calculateRestoration(const MWWorld::Ptr& ptr, float duration) const;

void updateCrimePursuit(const MWWorld::Ptr& ptr, float duration) const;
void updateCrimePursuit(const MWWorld::Ptr& ptr, float duration, SidingCache& cachedAllies) const;

void killDeadActors();

Expand All @@ -198,13 +199,25 @@ namespace MWMechanics
@Notes: If againstPlayer = true then actor2 should be the Player.
If one of the combatants is creature it should be actor1.
*/
void engageCombat(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2,
std::map<const MWWorld::Ptr, const std::set<MWWorld::Ptr>>& cachedAllies, bool againstPlayer) const;
void engageCombat(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, SidingCache& cachedAllies,
bool againstPlayer) const;
};

class SidingCache
{
const Actors& mActors;
const bool mExcludeInfighting;
std::map<MWWorld::Ptr, std::set<MWWorld::Ptr>> mCache;

/// Recursive version of getActorsSidingWith that takes, adds to and returns a cache of
/// actors mapped to their allies. Excludes infighting
void getActorsSidingWith(const MWWorld::Ptr& actor, std::set<MWWorld::Ptr>& out,
std::map<const MWWorld::Ptr, const std::set<MWWorld::Ptr>>& cachedAllies) const;
public:
SidingCache(const Actors& actors, bool excludeInfighting)
: mActors(actors)
, mExcludeInfighting(excludeInfighting)
{
}

/// Recursive version of getActorsSidingWith that takes, returns a cached set of allies
const std::set<MWWorld::Ptr>& getActorsSidingWith(const MWWorld::Ptr& actor);
};
}

Expand Down
25 changes: 18 additions & 7 deletions apps/openmw/mwmechanics/aicombat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,15 @@ namespace MWMechanics
{
storage.startCombatMove(isRangedCombat, distToTarget, rangeAttack, actor, target);
// start new attack
storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat);
bool canShout = true;
ESM::RefId spellId = storage.mCurrentAction->getSpell();
if (!spellId.empty())
{
const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get<ESM::Spell>().find(spellId);
if (spell->mEffects.mList.empty() || spell->mEffects.mList[0].mRange != ESM::RT_Target)
canShout = false;
}
storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat, canShout);
}

// If actor uses custom destination it has to try to rebuild path because environment can change
Expand Down Expand Up @@ -635,7 +643,7 @@ namespace MWMechanics
}

void AiCombatStorage::startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController,
const ESM::Weapon* weapon, bool distantCombat)
const ESM::Weapon* weapon, bool distantCombat, bool canShout)
{
if (mReadyToAttack && characterController.readyToStartAttack())
{
Expand All @@ -658,12 +666,15 @@ namespace MWMechanics
baseDelay = store.get<ESM::GameSetting>().find("fCombatDelayNPC")->mValue.getFloat();
}

// Say a provoking combat phrase
const int iVoiceAttackOdds
= store.get<ESM::GameSetting>().find("iVoiceAttackOdds")->mValue.getInteger();
if (Misc::Rng::roll0to99(prng) < iVoiceAttackOdds)
if (canShout)
{
MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("attack"));
// Say a provoking combat phrase
const int iVoiceAttackOdds
= store.get<ESM::GameSetting>().find("iVoiceAttackOdds")->mValue.getInteger();
if (Misc::Rng::roll0to99(prng) < iVoiceAttackOdds)
{
MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("attack"));
}
}
mAttackCooldown = std::min(baseDelay + 0.01 * Misc::Rng::roll0to99(prng), baseDelay + 0.9);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/openmw/mwmechanics/aicombat.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ namespace MWMechanics
void updateCombatMove(float duration);
void stopCombatMove();
void startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController,
const ESM::Weapon* weapon, bool distantCombat);
const ESM::Weapon* weapon, bool distantCombat, bool canShout);
void updateAttack(const MWWorld::Ptr& actor, CharacterController& characterController);
void stopAttack();

Expand Down
2 changes: 2 additions & 0 deletions apps/openmw/mwmechanics/aicombataction.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace MWMechanics
virtual float getCombatRange(bool& isRanged) const = 0;
virtual float getActionCooldown() const { return 0.f; }
virtual const ESM::Weapon* getWeapon() const { return nullptr; }
virtual ESM::RefId getSpell() const { return {}; }
virtual bool isAttackingOrSpell() const { return true; }
virtual bool isFleeing() const { return false; }
};
Expand Down Expand Up @@ -43,6 +44,7 @@ namespace MWMechanics
void prepare(const MWWorld::Ptr& actor) override;

float getCombatRange(bool& isRanged) const override;
ESM::RefId getSpell() const override { return mSpellId; }
};

class ActionEnchantedItem : public Action
Expand Down
20 changes: 20 additions & 0 deletions apps/openmw/mwmechanics/aipackage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,26 @@ MWWorld::Ptr MWMechanics::AiPackage::getTarget() const
return mCachedTarget;
}

bool MWMechanics::AiPackage::targetIs(const MWWorld::Ptr& ptr) const
{
if (mTargetActorId == -2)
return ptr.isEmpty();
else if (mTargetActorId == -1)
{
if (mTargetActorRefId.empty())
{
mTargetActorId = -2;
return ptr.isEmpty();
}
if (!ptr.isEmpty() && ptr.getCellRef().getRefId() == mTargetActorRefId)
return getTarget() == ptr;
return false;
}
if (ptr.isEmpty() || !ptr.getClass().isActor())
return false;
return ptr.getClass().getCreatureStats(ptr).getActorId() == mTargetActorId;
}

void MWMechanics::AiPackage::reset()
{
// reset all members
Expand Down
Loading

0 comments on commit 838785d

Please sign in to comment.