mirror of
https://gitlab.com/OpenMW/openmw.git
synced 2025-01-31 06:32:39 +00:00
ce263af393
Remove WindowManager wrappers. It's not safe to use WindowManager in all places and it's not required. Environment stores resource system providing VFS required to call these functions. In the case of ObjectPaging it's available from the member variable. Also ObjectPaging::createChunk may access WindowManager when it's already destructed when exiting the game because it's destructed before CellPreloader finishes all background jobs. Engine::mResourceSystem is destructed after all other systems so it's safe to use it.
498 lines
20 KiB
C++
498 lines
20 KiB
C++
#include "activespells.hpp"
|
|
|
|
#include <optional>
|
|
|
|
#include <components/debug/debuglog.hpp>
|
|
|
|
#include <components/misc/rng.hpp>
|
|
#include <components/misc/stringops.hpp>
|
|
#include <components/misc/resourcehelpers.hpp>
|
|
|
|
#include <components/esm3/loadmgef.hpp>
|
|
|
|
#include <components/settings/settings.hpp>
|
|
|
|
#include "creaturestats.hpp"
|
|
#include "spellcasting.hpp"
|
|
#include "spelleffects.hpp"
|
|
|
|
#include "../mwbase/environment.hpp"
|
|
#include "../mwbase/world.hpp"
|
|
|
|
#include "../mwrender/animation.hpp"
|
|
|
|
#include "../mwworld/esmstore.hpp"
|
|
#include "../mwworld/class.hpp"
|
|
#include "../mwworld/inventorystore.hpp"
|
|
|
|
namespace
|
|
{
|
|
bool merge(std::vector<ESM::ActiveEffect>& present, const std::vector<ESM::ActiveEffect>& queued)
|
|
{
|
|
// Can't merge if we already have an effect with the same effect index
|
|
auto problem = std::find_if(queued.begin(), queued.end(), [&] (const auto& qEffect)
|
|
{
|
|
return std::find_if(present.begin(), present.end(), [&] (const auto& pEffect) { return pEffect.mEffectIndex == qEffect.mEffectIndex; }) != present.end();
|
|
});
|
|
if(problem != queued.end())
|
|
return false;
|
|
present.insert(present.end(), queued.begin(), queued.end());
|
|
return true;
|
|
}
|
|
|
|
void addEffects(std::vector<ESM::ActiveEffect>& effects, const ESM::EffectList& list, bool ignoreResistances = false)
|
|
{
|
|
int currentEffectIndex = 0;
|
|
for(const auto& enam : list.mList)
|
|
{
|
|
ESM::ActiveEffect effect;
|
|
effect.mEffectId = enam.mEffectID;
|
|
effect.mArg = MWMechanics::EffectKey(enam).mArg;
|
|
effect.mMagnitude = 0.f;
|
|
effect.mMinMagnitude = enam.mMagnMin;
|
|
effect.mMaxMagnitude = enam.mMagnMax;
|
|
effect.mEffectIndex = currentEffectIndex++;
|
|
effect.mFlags = ESM::ActiveEffect::Flag_None;
|
|
if(ignoreResistances)
|
|
effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Resistances;
|
|
effect.mDuration = -1;
|
|
effect.mTimeLeft = -1;
|
|
effects.emplace_back(effect);
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace MWMechanics
|
|
{
|
|
ActiveSpells::IterationGuard::IterationGuard(ActiveSpells& spells) : mActiveSpells(spells)
|
|
{
|
|
mActiveSpells.mIterating = true;
|
|
}
|
|
|
|
ActiveSpells::IterationGuard::~IterationGuard()
|
|
{
|
|
mActiveSpells.mIterating = false;
|
|
}
|
|
|
|
ActiveSpells::ActiveSpellParams::ActiveSpellParams(const CastSpell& cast, const MWWorld::Ptr& caster)
|
|
: mId(cast.mId), mDisplayName(cast.mSourceName), mCasterActorId(-1), mSlot(cast.mSlot), mType(cast.mType), mWorsenings(-1)
|
|
{
|
|
if(!caster.isEmpty() && caster.getClass().isActor())
|
|
mCasterActorId = caster.getClass().getCreatureStats(caster).getActorId();
|
|
}
|
|
|
|
ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ESM::Spell* spell, const MWWorld::Ptr& actor, bool ignoreResistances)
|
|
: mId(spell->mId), mDisplayName(spell->mName), mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()), mSlot(0)
|
|
, mType(spell->mData.mType == ESM::Spell::ST_Ability ? ESM::ActiveSpells::Type_Ability : ESM::ActiveSpells::Type_Permanent), mWorsenings(-1)
|
|
{
|
|
assert(spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power);
|
|
addEffects(mEffects, spell->mEffects, ignoreResistances);
|
|
}
|
|
|
|
ActiveSpells::ActiveSpellParams::ActiveSpellParams(const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, int slotIndex, const MWWorld::Ptr& actor)
|
|
: mId(item.getCellRef().getRefId()), mDisplayName(item.getClass().getName(item)), mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId())
|
|
, mSlot(slotIndex), mType(ESM::ActiveSpells::Type_Enchantment), mWorsenings(-1)
|
|
{
|
|
assert(enchantment->mData.mType == ESM::Enchantment::ConstantEffect);
|
|
addEffects(mEffects, enchantment->mEffects);
|
|
}
|
|
|
|
ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ESM::ActiveSpells::ActiveSpellParams& params)
|
|
: mId(params.mId), mEffects(params.mEffects), mDisplayName(params.mDisplayName), mCasterActorId(params.mCasterActorId)
|
|
, mSlot(params.mItem.isSet() ? params.mItem.mIndex : 0)
|
|
, mType(params.mType), mWorsenings(params.mWorsenings), mNextWorsening({params.mNextWorsening})
|
|
{}
|
|
|
|
ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ActiveSpellParams& params, const MWWorld::Ptr& actor)
|
|
: mId(params.mId), mDisplayName(params.mDisplayName), mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId())
|
|
, mSlot(params.mSlot), mType(params.mType), mWorsenings(-1)
|
|
{}
|
|
|
|
ESM::ActiveSpells::ActiveSpellParams ActiveSpells::ActiveSpellParams::toEsm() const
|
|
{
|
|
ESM::ActiveSpells::ActiveSpellParams params;
|
|
params.mId = mId;
|
|
params.mEffects = mEffects;
|
|
params.mDisplayName = mDisplayName;
|
|
params.mCasterActorId = mCasterActorId;
|
|
params.mItem.unset();
|
|
if(mSlot)
|
|
{
|
|
// Note that we're storing the inventory slot as a RefNum instead of an int as a matter of future proofing
|
|
// mSlot needs to be replaced with a RefNum once inventory items get persistent RefNum (#4508 #6148)
|
|
params.mItem = { static_cast<unsigned int>(mSlot), 0 };
|
|
}
|
|
params.mType = mType;
|
|
params.mWorsenings = mWorsenings;
|
|
params.mNextWorsening = mNextWorsening.toEsm();
|
|
return params;
|
|
}
|
|
|
|
void ActiveSpells::ActiveSpellParams::worsen()
|
|
{
|
|
++mWorsenings;
|
|
if(!mWorsenings)
|
|
mNextWorsening = MWBase::Environment::get().getWorld()->getTimeStamp();
|
|
mNextWorsening += CorprusStats::sWorseningPeriod;
|
|
}
|
|
|
|
bool ActiveSpells::ActiveSpellParams::shouldWorsen() const
|
|
{
|
|
return mWorsenings >= 0 && MWBase::Environment::get().getWorld()->getTimeStamp() >= mNextWorsening;
|
|
}
|
|
|
|
void ActiveSpells::ActiveSpellParams::resetWorsenings()
|
|
{
|
|
mWorsenings = -1;
|
|
}
|
|
|
|
void ActiveSpells::update(const MWWorld::Ptr& ptr, float duration)
|
|
{
|
|
const auto& creatureStats = ptr.getClass().getCreatureStats(ptr);
|
|
assert(&creatureStats.getActiveSpells() == this);
|
|
IterationGuard guard{*this};
|
|
// Erase no longer active spells and effects
|
|
for(auto spellIt = mSpells.begin(); spellIt != mSpells.end();)
|
|
{
|
|
if(spellIt->mType != ESM::ActiveSpells::Type_Temporary && spellIt->mType != ESM::ActiveSpells::Type_Consumable)
|
|
{
|
|
++spellIt;
|
|
continue;
|
|
}
|
|
bool removedSpell = false;
|
|
for(auto effectIt = spellIt->mEffects.begin(); effectIt != spellIt->mEffects.end();)
|
|
{
|
|
if(effectIt->mFlags & ESM::ActiveEffect::Flag_Remove && effectIt->mTimeLeft <= 0.f)
|
|
{
|
|
auto effect = *effectIt;
|
|
effectIt = spellIt->mEffects.erase(effectIt);
|
|
onMagicEffectRemoved(ptr, *spellIt, effect);
|
|
removedSpell = applyPurges(ptr, &spellIt, &effectIt);
|
|
if(removedSpell)
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
++effectIt;
|
|
}
|
|
}
|
|
if(removedSpell)
|
|
continue;
|
|
if(spellIt->mEffects.empty())
|
|
spellIt = mSpells.erase(spellIt);
|
|
else
|
|
++spellIt;
|
|
}
|
|
|
|
for(const auto& spell : mQueue)
|
|
addToSpells(ptr, spell);
|
|
mQueue.clear();
|
|
|
|
// Vanilla only does this on cell change I think
|
|
const auto& spells = creatureStats.getSpells();
|
|
for(const ESM::Spell* spell : spells)
|
|
{
|
|
if(spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power && !isSpellActive(spell->mId))
|
|
mSpells.emplace_back(ActiveSpellParams{spell, ptr});
|
|
}
|
|
|
|
if(ptr.getClass().hasInventoryStore(ptr) && !(creatureStats.isDead() && !creatureStats.isDeathAnimationFinished()))
|
|
{
|
|
auto& store = ptr.getClass().getInventoryStore(ptr);
|
|
if(store.getInvListener() != nullptr)
|
|
{
|
|
bool playNonLooping = !store.isFirstEquip();
|
|
const auto world = MWBase::Environment::get().getWorld();
|
|
for(int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++)
|
|
{
|
|
auto slot = store.getSlot(slotIndex);
|
|
if(slot == store.end())
|
|
continue;
|
|
const auto& enchantmentId = slot->getClass().getEnchantment(*slot);
|
|
if(enchantmentId.empty())
|
|
continue;
|
|
const ESM::Enchantment* enchantment = world->getStore().get<ESM::Enchantment>().find(enchantmentId);
|
|
if(enchantment->mData.mType != ESM::Enchantment::ConstantEffect)
|
|
continue;
|
|
if(std::find_if(mSpells.begin(), mSpells.end(), [&] (const ActiveSpellParams& params)
|
|
{
|
|
return params.mSlot == slotIndex && params.mType == ESM::ActiveSpells::Type_Enchantment && params.mId == slot->getCellRef().getRefId();
|
|
}) != mSpells.end())
|
|
continue;
|
|
const ActiveSpellParams& params = mSpells.emplace_back(ActiveSpellParams{*slot, enchantment, slotIndex, ptr});
|
|
for(const auto& effect : params.mEffects)
|
|
MWMechanics::playEffects(ptr, *world->getStore().get<ESM::MagicEffect>().find(effect.mEffectId), playNonLooping);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update effects
|
|
for(auto spellIt = mSpells.begin(); spellIt != mSpells.end();)
|
|
{
|
|
const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spellIt->mCasterActorId); //Maybe make this search outside active grid?
|
|
bool removedSpell = false;
|
|
std::optional<ActiveSpellParams> reflected;
|
|
for(auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();)
|
|
{
|
|
auto result = applyMagicEffect(ptr, caster, *spellIt, *it, duration);
|
|
if(result == MagicApplicationResult::REFLECTED)
|
|
{
|
|
if(!reflected)
|
|
{
|
|
static const bool keepOriginalCaster = Settings::Manager::getBool("classic reflected absorb spells behavior", "Game");
|
|
if(keepOriginalCaster)
|
|
reflected = {*spellIt, caster};
|
|
else
|
|
reflected = {*spellIt, ptr};
|
|
}
|
|
auto& reflectedEffect = reflected->mEffects.emplace_back(*it);
|
|
reflectedEffect.mFlags = ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption;
|
|
it = spellIt->mEffects.erase(it);
|
|
}
|
|
else if(result == MagicApplicationResult::REMOVED)
|
|
it = spellIt->mEffects.erase(it);
|
|
else
|
|
++it;
|
|
removedSpell = applyPurges(ptr, &spellIt, &it);
|
|
if(removedSpell)
|
|
break;
|
|
}
|
|
if(reflected)
|
|
{
|
|
const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find("VFX_Reflect");
|
|
MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr);
|
|
if(animation && !reflectStatic->mModel.empty())
|
|
{
|
|
const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS();
|
|
animation->addEffect(
|
|
Misc::ResourceHelpers::correctMeshPath(reflectStatic->mModel, vfs),
|
|
ESM::MagicEffect::Reflect, false, std::string());
|
|
}
|
|
caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected);
|
|
}
|
|
if(removedSpell)
|
|
continue;
|
|
|
|
bool remove = false;
|
|
if(spellIt->mType == ESM::ActiveSpells::Type_Ability || spellIt->mType == ESM::ActiveSpells::Type_Permanent)
|
|
{
|
|
try
|
|
{
|
|
remove = !spells.hasSpell(spellIt->mId);
|
|
}
|
|
catch(const std::runtime_error& e)
|
|
{
|
|
remove = true;
|
|
Log(Debug::Error) << "Removing active effect: " << e.what();
|
|
}
|
|
}
|
|
else if(spellIt->mType == ESM::ActiveSpells::Type_Enchantment)
|
|
{
|
|
const auto& store = ptr.getClass().getInventoryStore(ptr);
|
|
auto slot = store.getSlot(spellIt->mSlot);
|
|
remove = slot == store.end() || slot->getCellRef().getRefId() != spellIt->mId;
|
|
}
|
|
if(remove)
|
|
{
|
|
auto params = *spellIt;
|
|
spellIt = mSpells.erase(spellIt);
|
|
for(const auto& effect : params.mEffects)
|
|
onMagicEffectRemoved(ptr, params, effect);
|
|
applyPurges(ptr, &spellIt);
|
|
continue;
|
|
}
|
|
++spellIt;
|
|
}
|
|
}
|
|
|
|
void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell)
|
|
{
|
|
if(spell.mType != ESM::ActiveSpells::Type_Consumable)
|
|
{
|
|
auto found = std::find_if(mSpells.begin(), mSpells.end(), [&] (const auto& existing)
|
|
{
|
|
return spell.mId == existing.mId && spell.mCasterActorId == existing.mCasterActorId && spell.mSlot == existing.mSlot;
|
|
});
|
|
if(found != mSpells.end())
|
|
{
|
|
if(merge(found->mEffects, spell.mEffects))
|
|
return;
|
|
auto params = *found;
|
|
mSpells.erase(found);
|
|
for(const auto& effect : params.mEffects)
|
|
onMagicEffectRemoved(ptr, params, effect);
|
|
}
|
|
}
|
|
mSpells.emplace_back(spell);
|
|
}
|
|
|
|
ActiveSpells::ActiveSpells() : mIterating(false)
|
|
{}
|
|
|
|
ActiveSpells::TIterator ActiveSpells::begin() const
|
|
{
|
|
return mSpells.begin();
|
|
}
|
|
|
|
ActiveSpells::TIterator ActiveSpells::end() const
|
|
{
|
|
return mSpells.end();
|
|
}
|
|
|
|
bool ActiveSpells::isSpellActive(std::string_view id) const
|
|
{
|
|
return std::find_if(mSpells.begin(), mSpells.end(), [&] (const auto& spell)
|
|
{
|
|
return Misc::StringUtils::ciEqual(spell.mId, id);
|
|
}) != mSpells.end();
|
|
}
|
|
|
|
void ActiveSpells::addSpell(const ActiveSpellParams& params)
|
|
{
|
|
mQueue.emplace_back(params);
|
|
}
|
|
|
|
void ActiveSpells::addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor)
|
|
{
|
|
mQueue.emplace_back(ActiveSpellParams{spell, actor, true});
|
|
}
|
|
|
|
void ActiveSpells::purge(ParamsPredicate predicate, const MWWorld::Ptr& ptr)
|
|
{
|
|
assert(&ptr.getClass().getCreatureStats(ptr).getActiveSpells() == this);
|
|
mPurges.emplace(predicate);
|
|
if(!mIterating)
|
|
{
|
|
IterationGuard guard{*this};
|
|
applyPurges(ptr);
|
|
}
|
|
}
|
|
|
|
void ActiveSpells::purge(EffectPredicate predicate, const MWWorld::Ptr& ptr)
|
|
{
|
|
assert(&ptr.getClass().getCreatureStats(ptr).getActiveSpells() == this);
|
|
mPurges.emplace(predicate);
|
|
if(!mIterating)
|
|
{
|
|
IterationGuard guard{*this};
|
|
applyPurges(ptr);
|
|
}
|
|
}
|
|
|
|
bool ActiveSpells::applyPurges(const MWWorld::Ptr& ptr, std::list<ActiveSpellParams>::iterator* currentSpell, std::vector<ActiveEffect>::iterator* currentEffect)
|
|
{
|
|
bool removedCurrentSpell = false;
|
|
while(!mPurges.empty())
|
|
{
|
|
auto predicate = mPurges.front();
|
|
mPurges.pop();
|
|
for(auto spellIt = mSpells.begin(); spellIt != mSpells.end();)
|
|
{
|
|
bool isCurrentSpell = currentSpell && *currentSpell == spellIt;
|
|
std::visit([&] (auto&& variant)
|
|
{
|
|
using T = std::decay_t<decltype(variant)>;
|
|
if constexpr (std::is_same_v<T, ParamsPredicate>)
|
|
{
|
|
if(variant(*spellIt))
|
|
{
|
|
auto params = *spellIt;
|
|
spellIt = mSpells.erase(spellIt);
|
|
if(isCurrentSpell)
|
|
{
|
|
*currentSpell = spellIt;
|
|
removedCurrentSpell = true;
|
|
}
|
|
for(const auto& effect : params.mEffects)
|
|
onMagicEffectRemoved(ptr, params, effect);
|
|
}
|
|
else
|
|
++spellIt;
|
|
}
|
|
else
|
|
{
|
|
static_assert(std::is_same_v<T, EffectPredicate>, "Non-exhaustive visitor");
|
|
for(auto effectIt = spellIt->mEffects.begin(); effectIt != spellIt->mEffects.end();)
|
|
{
|
|
if(variant(*spellIt, *effectIt))
|
|
{
|
|
auto effect = *effectIt;
|
|
if(isCurrentSpell && currentEffect)
|
|
{
|
|
auto distance = std::distance(spellIt->mEffects.begin(), *currentEffect);
|
|
if(effectIt <= *currentEffect)
|
|
distance--;
|
|
effectIt = spellIt->mEffects.erase(effectIt);
|
|
*currentEffect = spellIt->mEffects.begin() + distance;
|
|
}
|
|
else
|
|
effectIt = spellIt->mEffects.erase(effectIt);
|
|
onMagicEffectRemoved(ptr, *spellIt, effect);
|
|
}
|
|
else
|
|
++effectIt;
|
|
}
|
|
++spellIt;
|
|
}
|
|
}, predicate);
|
|
}
|
|
}
|
|
return removedCurrentSpell;
|
|
}
|
|
|
|
void ActiveSpells::removeEffects(const MWWorld::Ptr& ptr, std::string_view id)
|
|
{
|
|
purge([=] (const ActiveSpellParams& params)
|
|
{
|
|
return params.mId == id;
|
|
}, ptr);
|
|
}
|
|
|
|
void ActiveSpells::purgeEffect(const MWWorld::Ptr& ptr, short effectId)
|
|
{
|
|
purge([=] (const ActiveSpellParams&, const ESM::ActiveEffect& effect)
|
|
{
|
|
return effect.mEffectId == effectId;
|
|
}, ptr);
|
|
}
|
|
|
|
void ActiveSpells::purge(const MWWorld::Ptr& ptr, int casterActorId)
|
|
{
|
|
purge([=] (const ActiveSpellParams& params)
|
|
{
|
|
return params.mCasterActorId == casterActorId;
|
|
}, ptr);
|
|
}
|
|
|
|
void ActiveSpells::clear(const MWWorld::Ptr& ptr)
|
|
{
|
|
mQueue.clear();
|
|
purge([] (const ActiveSpellParams& params) { return true; }, ptr);
|
|
}
|
|
|
|
void ActiveSpells::skipWorsenings(double hours)
|
|
{
|
|
for(auto& spell : mSpells)
|
|
{
|
|
if(spell.mWorsenings >= 0)
|
|
spell.mNextWorsening += hours;
|
|
}
|
|
}
|
|
|
|
void ActiveSpells::writeState(ESM::ActiveSpells &state) const
|
|
{
|
|
for(const auto& spell : mSpells)
|
|
state.mSpells.emplace_back(spell.toEsm());
|
|
for(const auto& spell : mQueue)
|
|
state.mQueue.emplace_back(spell.toEsm());
|
|
}
|
|
|
|
void ActiveSpells::readState(const ESM::ActiveSpells &state)
|
|
{
|
|
for(const ESM::ActiveSpells::ActiveSpellParams& spell : state.mSpells)
|
|
mSpells.emplace_back(ActiveSpellParams{spell});
|
|
for(const ESM::ActiveSpells::ActiveSpellParams& spell : state.mQueue)
|
|
mQueue.emplace_back(ActiveSpellParams{spell});
|
|
}
|
|
}
|