Overhaul magic effects to work with onApply and onEnd events

pull/3145/head
Evil Eye 3 years ago
parent 5ce2eff3bd
commit dc1fe62dde

@ -1,6 +1,7 @@
0.48.0
------
Bug #1751: Birthsign abilities increase modified attribute values instead of base ones
Bug #3246: ESSImporter: Most NPCs are dead on save load
Bug #3514: Editing a reference's position after loading an esp file makes the reference disappear
Bug #3737: Scripts from The Underground 2 .esp do not play (all patched versions)
@ -16,13 +17,18 @@
Bug #5453: Magic effect VFX are offset for creatures
Bug #5483: AutoCalc flag is not used to calculate spells cost
Bug #5508: Engine binary links to Qt without using it
Bug #5596: Effects in constant spells should not be merged
Bug #5621: Drained stats cannot be restored
Bug #5755: Active grid object paging - disappearing textures
Bug #5788: Texture editing parses the selected indexes wrongly
Bug #5801: A multi-effect spell with the intervention effects and recall always favors Almsivi intervention
Bug #5842: GetDisposition adds temporary disposition change from different actors
Bug #5863: GetEffect should return true after the player has teleported
Bug #6037: Morrowind Content Language Cannot be Set to English in OpenMW Launcher
Bug #6051: NaN water height in ESM file is not handled gracefully
Bug #6066: addtopic "return" does not work from within script. No errors thrown
Bug #6067: esp loader fails in for certain subrecord orders
Bug #6087: Bound items added directly to the inventory disappear if their corresponding spell effect ends
Bug #6101: Disarming trapped unlocked owned objects isn't considered a crime
Bug #6107: Fatigue is incorrectly recalculated when fortify effect is applied or removed
Bug #6115: Showmap overzealous matching
@ -36,6 +42,7 @@
Bug #6174: Spellmaking and Enchanting sliders differences from vanilla
Bug #6184: Command and Calm and Demoralize and Frenzy and Rally magic effects inconsistencies with vanilla
Bug #6197: Infinite Casting Loop
Bug #6223: Some Constant Effect Bound Items inconsistencies
Bug #6273: Respawning NPCs rotation is inconsistent
Bug #6282: Laura craft doesn't follow the player character
Bug #6283: Avis Dorsey follows you after her death
@ -45,8 +52,10 @@
Feature #2554: Modifying an object triggers the instances table to scroll to the corresponding record
Feature #2780: A way to see current OpenMW version in the console
Feature #3616: Allow Zoom levels on the World Map
Feature #4297: Implement APPLIED_ONCE flag for magic effects
Feature #4595: Unique object identifier
Feature #4737: Handle instance move from one cell to another
Feature #5198: Implement "Magic effect expired" event
Feature #5489: MCP: Telekinesis fix for activators
Feature #5996: Support Lua scripts in OpenMW
Feature #6017: Separate persistent and temporary cell references when saving

@ -124,11 +124,9 @@ public:
{
mContext->mPlayer.mObject.mCreatureStats.mLevel = npc.mNpdt.mLevel;
mContext->mPlayerBase = npc;
ESM::SpellState::SpellParams empty;
// FIXME: player start spells and birthsign spells aren't listed here,
// need to fix openmw to account for this
for (const auto & spell : npc.mSpells.mList)
mContext->mPlayer.mObject.mCreatureStats.mSpells.mSpells[spell] = empty;
mContext->mPlayer.mObject.mCreatureStats.mSpells.mSpells = npc.mSpells.mList;
// Clear the list now that we've written it, this prevents issues cropping up with
// ensureCustomData() in OpenMW tripping over no longer existing spells, where an error would be fatal.

@ -92,8 +92,8 @@ add_openmw_dir (mwmechanics
drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor aibreathe
aicast aiescort aiface aiactivate aicombat recharge repair enchanting pathfinding pathgrid security spellcasting spellresistance
disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction actor summoning
character actors objects aistate trading weaponpriority spellpriority weapontype spellutil tickableeffects
spellabsorption linkedeffects
character actors objects aistate trading weaponpriority spellpriority weapontype spellutil
spellabsorption spelleffects
)
add_openmw_dir (mwstate

@ -277,8 +277,6 @@ namespace MWBase
virtual float getAngleToPlayer(const MWWorld::Ptr& ptr) const = 0;
virtual MWMechanics::GreetingState getGreetingState(const MWWorld::Ptr& ptr) const = 0;
virtual bool isTurningToPlayer(const MWWorld::Ptr& ptr) const = 0;
virtual void restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId) = 0;
};
}

@ -539,7 +539,7 @@ namespace MWBase
virtual void castSpell (const MWWorld::Ptr& actor, bool manualSpell=false) = 0;
virtual void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) = 0;
virtual void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, int slot) = 0;
virtual void launchProjectile (MWWorld::Ptr& actor, MWWorld::Ptr& projectile,
const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr& bow, float speed, float attackStrength) = 0;
virtual void updateProjectilesCasters() = 0;
@ -591,7 +591,7 @@ namespace MWBase
virtual void explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster,
const MWWorld::Ptr& ignore, ESM::RangeType rangeType, const std::string& id,
const std::string& sourceName, const bool fromProjectile=false) = 0;
const std::string& sourceName, const bool fromProjectile=false, int slot = 0) = 0;
virtual void activate (const MWWorld::Ptr& object, const MWWorld::Ptr& actor) = 0;

@ -72,13 +72,6 @@ namespace
return {question, {r2, r1, r0}, sound};
}
}
void updatePlayerHealth()
{
MWWorld::Ptr player = MWMechanics::getPlayer();
MWMechanics::NpcStats& npcStats = player.getClass().getNpcStats(player);
npcStats.updateHealth();
}
}
namespace MWGui
@ -372,8 +365,6 @@ namespace MWGui
MWBase::Environment::get().getWindowManager()->removeDialog(mPickClassDialog);
mPickClassDialog = nullptr;
}
updatePlayerHealth();
}
void CharacterCreation::onPickClassDialogDone(WindowBase* parWindow)
@ -448,8 +439,6 @@ namespace MWGui
MWBase::Environment::get().getWindowManager()->removeDialog(mRaceDialog);
mRaceDialog = nullptr;
}
updatePlayerHealth();
}
void CharacterCreation::onRaceDialogBack()
@ -477,8 +466,6 @@ namespace MWGui
MWBase::Environment::get().getWindowManager()->removeDialog(mBirthSignDialog);
mBirthSignDialog = nullptr;
}
updatePlayerHealth();
}
void CharacterCreation::onBirthSignDialogDone(WindowBase* parWindow)
@ -527,7 +514,6 @@ namespace MWGui
// Do not delete dialog, so that choices are remembered in case we want to go back and adjust them later
mCreateClassDialog->setVisible(false);
}
updatePlayerHealth();
}
void CharacterCreation::onCreateClassDialogDone(WindowBase* parWindow)
@ -719,8 +705,6 @@ namespace MWGui
MWBase::Environment::get().getWorld()->getStore().get<ESM::Class>().find(mGenerateClass);
mPlayerClass = *klass;
updatePlayerHealth();
}
void CharacterCreation::onGenerateClassBack()

@ -262,7 +262,7 @@ namespace MWGui
}
// Clean up summoned creatures as well
std::map<ESM::SummonKey, int>& creatureMap = creatureStats.getSummonedCreatureMap();
auto& creatureMap = creatureStats.getSummonedCreatureMap();
for (const auto& creature : creatureMap)
MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(ptr, creature.second);
creatureMap.clear();

@ -82,10 +82,7 @@ namespace MWGui
MWBase::Environment::get().getWorld()->advanceTime(mDays * 24);
// We should not worsen corprus when in prison
for (auto& spell : player.getClass().getCreatureStats(player).getCorprusSpells())
{
spell.second.mNextWorsening += mDays * 24;
}
player.getClass().getCreatureStats(player).getActiveSpells().skipWorsenings(mDays * 24);
std::set<int> skills;
for (int day=0; day<mDays; ++day)

@ -99,10 +99,8 @@ namespace MWGui
std::vector<const ESM::Spell*> spellsToSort;
for (MWMechanics::Spells::TIterator iter = merchantSpells.begin(); iter!=merchantSpells.end(); ++iter)
for (const ESM::Spell* spell : merchantSpells)
{
const ESM::Spell* spell = iter->first;
if (spell->mData.mType!=ESM::Spell::ST_Spell)
continue; // don't try to sell diseases, curses or powers
@ -115,10 +113,10 @@ namespace MWGui
continue;
}
if (playerHasSpell(iter->first->mId))
if (playerHasSpell(spell->mId))
continue;
spellsToSort.push_back(iter->first);
spellsToSort.push_back(spell);
}
std::stable_sort(spellsToSort.begin(), spellsToSort.end(), sortSpells);

@ -520,10 +520,8 @@ namespace MWGui
std::vector<short> knownEffects;
for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it)
for (const ESM::Spell* spell : spells)
{
const ESM::Spell* spell = it->first;
// only normal spells count
if (spell->mData.mType != ESM::Spell::ST_Spell)
continue;

@ -24,50 +24,33 @@
namespace MWGui
{
void EffectSourceVisitor::visit (MWMechanics::EffectKey key, int effectIndex,
const std::string& sourceName, const std::string& sourceId, int casterActorId,
float magnitude, float remainingTime, float totalTime)
{
MagicEffectInfo newEffectSource;
newEffectSource.mKey = key;
newEffectSource.mMagnitude = static_cast<int>(magnitude);
newEffectSource.mPermanent = mIsPermanent;
newEffectSource.mRemainingTime = remainingTime;
newEffectSource.mSource = sourceName;
newEffectSource.mTotalTime = totalTime;
mEffectSources[key.mId].push_back(newEffectSource);
}
void SpellIcons::updateWidgets(MyGUI::Widget *parent, bool adjustSize)
{
// TODO: Tracking add/remove/expire would be better than force updating every frame
MWWorld::Ptr player = MWMechanics::getPlayer();
const MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player);
EffectSourceVisitor visitor;
// permanent item enchantments & permanent spells
visitor.mIsPermanent = true;
MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player);
store.visitEffectSources(visitor);
stats.getSpells().visitEffectSources(visitor);
// now add lasting effects
visitor.mIsPermanent = false;
stats.getActiveSpells().visitEffectSources(visitor);
std::map <int, std::vector<MagicEffectInfo> >& effects = visitor.mEffectSources;
std::map<int, std::vector<MagicEffectInfo>> effects;
for(const auto& params : stats.getActiveSpells())
{
for(const auto& effect : params.getEffects())
{
if(!effect.mMagnitude)
continue;
MagicEffectInfo newEffectSource;
newEffectSource.mKey = MWMechanics::EffectKey(effect.mEffectId, effect.mArg);
newEffectSource.mMagnitude = static_cast<int>(effect.mMagnitude);
newEffectSource.mPermanent = effect.mDuration == -1.f;
newEffectSource.mRemainingTime = effect.mTimeLeft;
newEffectSource.mSource = params.getDisplayName();
newEffectSource.mTotalTime = effect.mDuration;
effects[effect.mEffectId].push_back(newEffectSource);
}
}
int w=2;
for (auto& effectInfoPair : effects)
for (const auto& [effectId, effectInfos] : effects)
{
const int effectId = effectInfoPair.first;
const ESM::MagicEffect* effect =
MWBase::Environment::get().getWorld ()->getStore ().get<ESM::MagicEffect>().find(effectId);
@ -78,7 +61,6 @@ namespace MWGui
static const float fadeTime = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fMagicStartIconBlink")->mValue.getFloat();
std::vector<MagicEffectInfo>& effectInfos = effectInfoPair.second;
bool addNewLine = false;
for (const MagicEffectInfo& effectInfo : effectInfos)
{

@ -37,20 +37,6 @@ namespace MWGui
bool mPermanent; // the effect is permanent
};
class EffectSourceVisitor : public MWMechanics::EffectSourceVisitor
{
public:
bool mIsPermanent;
std::map <int, std::vector<MagicEffectInfo> > mEffectSources;
virtual ~EffectSourceVisitor() {}
void visit (MWMechanics::EffectKey key, int effectIndex,
const std::string& sourceName, const std::string& sourceId, int casterActorId,
float magnitude, float remainingTime = -1, float totalTime = -1) override;
};
class SpellIcons
{
public:

@ -92,9 +92,8 @@ namespace MWGui
std::string filter = Misc::StringUtils::lowerCaseUtf8(mFilter);
for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it)
for (const ESM::Spell* spell : spells)
{
const ESM::Spell* spell = it->first;
if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell)
continue;

@ -5,347 +5,442 @@
#include <components/esm/loadmgef.hpp>
#include "creaturestats.hpp"
#include "spellcasting.hpp"
#include "spelleffects.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
#include "../mwworld/esmstore.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/inventorystore.hpp"
namespace MWMechanics
namespace
{
void ActiveSpells::update(float duration) const
bool merge(std::vector<ESM::ActiveEffect>& present, const std::vector<ESM::ActiveEffect>& queued)
{
bool rebuild = false;
// Erase no longer active spells and effects
if (duration > 0)
// 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)
{
TContainer::iterator iter (mSpells.begin());
while (iter!=mSpells.end())
{
if (!timeToExpire (iter))
{
mSpells.erase (iter++);
rebuild = true;
}
else
{
bool interrupt = false;
std::vector<ActiveEffect>& effects = iter->second.mEffects;
for (std::vector<ActiveEffect>::iterator effectIt = effects.begin(); effectIt != effects.end();)
{
if (effectIt->mTimeLeft <= 0)
{
rebuild = true;
// Note: it we expire a Corprus effect, we should remove the whole spell.
if (effectIt->mEffectId == ESM::MagicEffect::Corprus)
{
iter = mSpells.erase (iter);
interrupt = true;
break;
}
effectIt = effects.erase(effectIt);
}
else
{
effectIt->mTimeLeft -= duration;
++effectIt;
}
}
if (!interrupt)
++iter;
}
}
}
if (mSpellsChanged)
{
mSpellsChanged = false;
rebuild = true;
}
if (rebuild)
rebuildEffects();
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 ActiveSpells::rebuildEffects() const
void addEffects(std::vector<ESM::ActiveEffect>& effects, const ESM::EffectList& list, bool ignoreResistances = false)
{
mEffects = MagicEffects();
for (TIterator iter (begin()); iter!=end(); ++iter)
int currentEffectIndex = 0;
for(const auto& enam : list.mList)
{
const std::vector<ActiveEffect>& effects = iter->second.mEffects;
for (std::vector<ActiveEffect>::const_iterator effectIt = effects.begin(); effectIt != effects.end(); ++effectIt)
{
if (effectIt->mTimeLeft > 0)
mEffects.add(MWMechanics::EffectKey(effectIt->mEffectId, effectIt->mArg), MWMechanics::EffectParam(effectIt->mMagnitude));
}
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);
}
}
}
ActiveSpells::ActiveSpells()
: mSpellsChanged (false)
{}
namespace MWMechanics
{
ActiveSpells::IterationGuard::IterationGuard(ActiveSpells& spells) : mActiveSpells(spells)
{
mActiveSpells.mIterating = true;
}
const MagicEffects& ActiveSpells::getMagicEffects() const
ActiveSpells::IterationGuard::~IterationGuard()
{
update(0.f);
return mEffects;
mActiveSpells.mIterating = false;
}
ActiveSpells::TIterator ActiveSpells::begin() const
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)
{
return mSpells.begin();
if(!caster.isEmpty() && caster.getClass().isActor())
mCasterActorId = caster.getClass().getCreatureStats(caster).getActorId();
}
ActiveSpells::TIterator ActiveSpells::end() const
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)
{
return mSpells.end();
assert(spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power);
addEffects(mEffects, spell->mEffects, ignoreResistances);
}
double ActiveSpells::timeToExpire (const TIterator& iterator) const
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)
{
const std::vector<ActiveEffect>& effects = iterator->second.mEffects;
assert(enchantment->mData.mType == ESM::Enchantment::ConstantEffect);
addEffects(mEffects, enchantment->mEffects);
}
float duration = 0;
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})
{}
for (std::vector<ActiveEffect>::const_iterator iter (effects.begin());
iter!=effects.end(); ++iter)
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)
{
if (iter->mTimeLeft > duration)
duration = iter->mTimeLeft;
// 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;
}
if (duration < 0)
return 0;
return duration;
void ActiveSpells::ActiveSpellParams::worsen()
{
++mWorsenings;
if(!mWorsenings)
mNextWorsening = MWBase::Environment::get().getWorld()->getTimeStamp();
mNextWorsening += CorprusStats::sWorseningPeriod;
}
bool ActiveSpells::isSpellActive(const std::string& id) const
bool ActiveSpells::ActiveSpellParams::shouldWorsen() const
{
for (TContainer::iterator iter = mSpells.begin(); iter != mSpells.end(); ++iter)
{
if (Misc::StringUtils::ciEqual(iter->first, id))
return true;
}
return false;
return mWorsenings >= 0 && MWBase::Environment::get().getWorld()->getTimeStamp() >= mNextWorsening;
}
const ActiveSpells::TContainer& ActiveSpells::getActiveSpells() const
void ActiveSpells::ActiveSpellParams::resetWorsenings()
{
return mSpells;
mWorsenings = -1;
}
void ActiveSpells::addSpell(const std::string &id, bool stack, const std::vector<ActiveEffect>& effects,
const std::string &displayName, int casterActorId)
void ActiveSpells::update(const MWWorld::Ptr& ptr, float duration)
{
TContainer::iterator it(mSpells.find(id));
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;
}
ActiveSpellParams params;
params.mEffects = effects;
params.mDisplayName = displayName;
params.mCasterActorId = casterActorId;
for(const auto& spell : mQueue)
addToSpells(ptr, spell);
mQueue.clear();
if (it == end() || stack)
// Vanilla only does this on cell change I think
const auto& spells = creatureStats.getSpells();
for(const ESM::Spell* spell : spells)
{
mSpells.insert(std::make_pair(id, params));
if(spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power && !isSpellActive(spell->mId))
mSpells.emplace_back(ActiveSpellParams{spell, ptr});
}
else
if(ptr.getClass().hasInventoryStore(ptr) && !(creatureStats.isDead() && !creatureStats.isDeathAnimationFinished()))
{
// addSpell() is called with effects for a range.
// but a spell may have effects with different ranges (e.g. Touch & Target)
// so, if we see new effects for same spell assume additional
// spell effects and add to existing effects of spell
mergeEffects(params.mEffects, it->second.mEffects);
it->second = params;
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;
ActiveSpellParams params(*slot, enchantment, slotIndex, ptr);
mSpells.emplace_back(params);
for(const auto& effect : params.mEffects)
MWMechanics::playEffects(ptr, *world->getStore().get<ESM::MagicEffect>().find(effect.mEffectId), playNonLooping);
}
}
}
mSpellsChanged = true;
}
void ActiveSpells::mergeEffects(std::vector<ActiveEffect>& addTo, const std::vector<ActiveEffect>& from)
{
for (std::vector<ActiveEffect>::const_iterator effect(from.begin()); effect != from.end(); ++effect)
// Update effects
for(auto spellIt = mSpells.begin(); spellIt != mSpells.end();)
{
// if effect is not in addTo, add it
bool missing = true;
for (std::vector<ActiveEffect>::const_iterator iter(addTo.begin()); iter != addTo.end(); ++iter)
const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spellIt->mCasterActorId); //Maybe make this search outside active grid?
bool removedSpell = false;
for(auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();)
{
if ((effect->mEffectId == iter->mEffectId) && (effect->mArg == iter->mArg))
{
missing = false;
bool remove = applyMagicEffect(ptr, caster, *spellIt, *it, duration);
if(remove)
it = spellIt->mEffects.erase(it);
else
++it;
removedSpell = applyPurges(ptr, &spellIt, &it);
if(removedSpell)
break;
}
}
if (missing)
if(removedSpell)
continue;
bool remove = false;
if(spellIt->mType == ESM::ActiveSpells::Type_Ability || spellIt->mType == ESM::ActiveSpells::Type_Permanent)
remove = !spells.hasSpell(spellIt->mId);
else if(spellIt->mType == ESM::ActiveSpells::Type_Enchantment)
{
addTo.push_back(*effect);
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::removeEffects(const std::string &id)
void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell)
{
for (TContainer::iterator spell = mSpells.begin(); spell != mSpells.end(); ++spell)
if(spell.mType != ESM::ActiveSpells::Type_Consumable)
{
if (spell->first == id)
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())
{
spell->second.mEffects.clear();
mSpellsChanged = true;
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);
}
void ActiveSpells::visitEffectSources(EffectSourceVisitor &visitor) const
ActiveSpells::ActiveSpells() : mIterating(false)
{}
ActiveSpells::TIterator ActiveSpells::begin() const
{
for (TContainer::const_iterator it = begin(); it != end(); ++it)
{
for (std::vector<ActiveEffect>::const_iterator effectIt = it->second.mEffects.begin();
effectIt != it->second.mEffects.end(); ++effectIt)
{
std::string name = it->second.mDisplayName;
return mSpells.begin();
}
float magnitude = effectIt->mMagnitude;
if (magnitude)
visitor.visit(MWMechanics::EffectKey(effectIt->mEffectId, effectIt->mArg), effectIt->mEffectIndex, name, it->first, it->second.mCasterActorId, magnitude, effectIt->mTimeLeft, effectIt->mDuration);
}
}
ActiveSpells::TIterator ActiveSpells::end() const
{
return mSpells.end();
}
void ActiveSpells::purgeAll(float chance, bool spellOnly)
bool ActiveSpells::isSpellActive(const std::string& id) const
{
for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); )
return std::find_if(mSpells.begin(), mSpells.end(), [&] (const auto& spell)
{
const std::string spellId = it->first;
return Misc::StringUtils::ciEqual(spell.mId, id);
}) != mSpells.end();
}
// if spellOnly is true, dispell only spells. Leave potions, enchanted items etc.
if (spellOnly)
{
const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().search(spellId);
if (!spell || spell->mData.mType != ESM::Spell::ST_Spell)
{
++it;
continue;
}
}
void ActiveSpells::addSpell(const ActiveSpellParams& params)
{
mQueue.emplace_back(params);
}
if (Misc::Rng::roll0to99() < chance)
mSpells.erase(it++);
else
++it;
}
mSpellsChanged = true;
void ActiveSpells::addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor)
{
mQueue.emplace_back(ActiveSpellParams{spell, actor, true});
}
void ActiveSpells::purgeEffect(short effectId)
void ActiveSpells::purge(ParamsPredicate predicate, const MWWorld::Ptr& ptr)
{
for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it)
assert(&ptr.getClass().getCreatureStats(ptr).getActiveSpells() == this);
mPurges.emplace(predicate);
if(!mIterating)
{
for (std::vector<ActiveEffect>::iterator effectIt = it->second.mEffects.begin();
effectIt != it->second.mEffects.end();)
{
if (effectIt->mEffectId == effectId)
effectIt = it->second.mEffects.erase(effectIt);
else
++effectIt;
}
IterationGuard guard{*this};
applyPurges(ptr);
}
mSpellsChanged = true;
}
void ActiveSpells::purgeEffect(short effectId, const std::string& sourceId, int effectIndex)
void ActiveSpells::purge(EffectPredicate predicate, const MWWorld::Ptr& ptr)
{
for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it)
assert(&ptr.getClass().getCreatureStats(ptr).getActiveSpells() == this);
mPurges.emplace(predicate);
if(!mIterating)
{
for (std::vector<ActiveEffect>::iterator effectIt = it->second.mEffects.begin();
effectIt != it->second.mEffects.end();)
{
if (effectIt->mEffectId == effectId && it->first == sourceId && (effectIndex < 0 || effectIndex == effectIt->mEffectIndex))
effectIt = it->second.mEffects.erase(effectIt);
else
++effectIt;
}
IterationGuard guard{*this};
applyPurges(ptr);
}
mSpellsChanged = true;
}
void ActiveSpells::purge(int casterActorId)
bool ActiveSpells::applyPurges(const MWWorld::Ptr& ptr, std::list<ActiveSpellParams>::iterator* currentSpell, std::vector<ActiveEffect>::iterator* currentEffect)
{
for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it)
bool removedCurrentSpell = false;
while(!mPurges.empty())
{
for (std::vector<ActiveEffect>::iterator effectIt = it->second.mEffects.begin();
effectIt != it->second.mEffects.end();)
auto predicate = mPurges.front();
mPurges.pop();
for(auto spellIt = mSpells.begin(); spellIt != mSpells.end();)
{
if (it->second.mCasterActorId == casterActorId)
effectIt = it->second.mEffects.erase(effectIt);
else
++effectIt;
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);
}
}
mSpellsChanged = true;
return removedCurrentSpell;
}
void ActiveSpells::purgeCorprusDisease()
void ActiveSpells::removeEffects(const MWWorld::Ptr& ptr, const std::string &id)
{
for (TContainer::iterator iter = mSpells.begin(); iter!=mSpells.end();)
purge([=] (const ActiveSpellParams& params)
{
bool hasCorprusEffect = false;
for (std::vector<ActiveEffect>::iterator effectIt = iter->second.mEffects.begin();
effectIt != iter->second.mEffects.end();++effectIt)
{
if (effectIt->mEffectId == ESM::MagicEffect::Corprus)
{
hasCorprusEffect = true;
break;
}
}
if (hasCorprusEffect)
{
mSpells.erase(iter++);
mSpellsChanged = true;
}
else
++iter;
}
return params.mId == id;
}, ptr);
}
void ActiveSpells::clear()
void ActiveSpells::purgeEffect(const MWWorld::Ptr& ptr, short effectId)
{
mSpells.clear();
mSpellsChanged = true;
purge([=] (const ActiveSpellParams&, const ESM::ActiveEffect& effect)
{
return effect.mEffectId == effectId;
}, ptr);
}
void ActiveSpells::writeState(ESM::ActiveSpells &state) const
void ActiveSpells::purge(const MWWorld::Ptr& ptr, int casterActorId)
{
for (TContainer::const_iterator it = mSpells.begin(); it != mSpells.end(); ++it)
purge([=] (const ActiveSpellParams& params)
{
// Stupid copying of almost identical structures. ESM::TimeStamp <-> MWWorld::TimeStamp
ESM::ActiveSpells::ActiveSpellParams params;
params.mEffects = it->second.mEffects;
params.mCasterActorId = it->second.mCasterActorId;
params.mDisplayName = it->second.mDisplayName;
return params.mCasterActorId == casterActorId;
}, ptr);
}
state.mSpells.insert (std::make_pair(it->first, params));
}
void ActiveSpells::clear(const MWWorld::Ptr& ptr)
{
mQueue.clear();
purge([] (const ActiveSpellParams& params) { return true; }, ptr);
}
void ActiveSpells::readState(const ESM::ActiveSpells &state)
void ActiveSpells::skipWorsenings(double hours)
{
for (ESM::ActiveSpells::TContainer::const_iterator it = state.mSpells.begin(); it != state.mSpells.end(); ++it)
for(auto& spell : mSpells)
{
// Stupid copying of almost identical structures. ESM::TimeStamp <-> MWWorld::TimeStamp
ActiveSpellParams params;
params.mEffects = it->second.mEffects;
params.mCasterActorId = it->second.mCasterActorId;
params.mDisplayName = it->second.mDisplayName;
mSpells.insert (std::make_pair(it->first, params));
mSpellsChanged = true;
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});
}
}

@ -1,40 +1,83 @@
#ifndef GAME_MWMECHANICS_ACTIVESPELLS_H
#define GAME_MWMECHANICS_ACTIVESPELLS_H
#include <map>
#include <vector>
#include <functional>
#include <list>
#include <queue>
#include <string>
#include <variant>
#include <vector>
#include <components/esm/activespells.hpp>
#include "../mwworld/timestamp.hpp"
#include "../mwworld/ptr.hpp"
#include "magiceffects.hpp"
#include "spellcasting.hpp"
namespace ESM
{
struct Enchantment;
struct Spell;
}
namespace MWMechanics
{
/// \brief Lasting spell effects
///
/// \note The name of this class is slightly misleading, since it also handels lasting potion
/// \note The name of this class is slightly misleading, since it also handles lasting potion
/// effects.
class ActiveSpells
{
public:
typedef ESM::ActiveEffect ActiveEffect;
struct ActiveSpellParams
using ActiveEffect = ESM::ActiveEffect;
class ActiveSpellParams
{
std::vector<ActiveEffect> mEffects;
MWWorld::TimeStamp mTimeStamp;
std::string mDisplayName;
std::string mId;
std::vector<ActiveEffect> mEffects;
std::string mDisplayName;
int mCasterActorId;
int mSlot;
ESM::ActiveSpells::EffectType mType;
int mWorsenings;
MWWorld::TimeStamp mNextWorsening;
ActiveSpellParams(const ESM::ActiveSpells::ActiveSpellParams& params);
ActiveSpellParams(const ESM::Spell* spell, const MWWorld::Ptr& actor, bool ignoreResistances = false);
ActiveSpellParams(const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, int slotIndex, const MWWorld::Ptr& actor);
ESM::ActiveSpells::ActiveSpellParams toEsm() const;
friend class ActiveSpells;
public:
ActiveSpellParams(const CastSpell& cast, const MWWorld::Ptr& caster);
const std::string& getId() const { return mId; }
const std::vector<ActiveEffect>& getEffects() const { return mEffects; }
std::vector<ActiveEffect>& getEffects() { return mEffects; }
// The caster that inflicted this spell on us
int mCasterActorId;
ESM::ActiveSpells::EffectType getType() const { return mType; }
int getCasterActorId() const { return mCasterActorId; }
int getWorsenings() const { return mWorsenings; }
const std::string& getDisplayName() const { return mDisplayName; }
// Increments worsenings count and sets the next timestamp
void worsen();
bool shouldWorsen() const;
void resetWorsenings();
};
typedef std::multimap<std::string, ActiveSpellParams > TContainer;
typedef TContainer::const_iterator TIterator;
typedef std::list<ActiveSpellParams>::const_iterator TIterator;
void readState (const ESM::ActiveSpells& state);
void writeState (ESM::ActiveSpells& state) const;
@ -43,24 +86,29 @@ namespace MWMechanics
TIterator end() const;
void update(float duration) const;
void update(const MWWorld::Ptr& ptr, float duration);
private:
using ParamsPredicate = std::function<bool(const ActiveSpellParams&)>;
using EffectPredicate = std::function<bool(const ActiveSpellParams&, const ESM::ActiveEffect&)>;
using Predicate = std::variant<ParamsPredicate, EffectPredicate>;
mutable TContainer mSpells;
mutable MagicEffects mEffects;
mutable bool mSpellsChanged;
struct IterationGuard
{
ActiveSpells& mActiveSpells;
void rebuildEffects() const;
IterationGuard(ActiveSpells& spells);
~IterationGuard();
};
/// Add any effects that are in "from" and not in "addTo" to "addTo"
void mergeEffects(std::vector<ActiveEffect>& addTo, const std::vector<ActiveEffect>& from);
std::list<ActiveSpellParams> mSpells;
std::vector<ActiveSpellParams> mQueue;
std::queue<Predicate> mPurges;
bool mIterating;
double timeToExpire (const TIterator& iterator) const;
///< Returns time (in in-game hours) until the spell pointed to by \a iterator
/// expires.
void addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell);
const TContainer& getActiveSpells() const;
bool applyPurges(const MWWorld::Ptr& ptr, std::list<ActiveSpellParams>::iterator* currentSpell = nullptr, std::vector<ActiveEffect>::iterator* currentEffect = nullptr);
public:
@ -70,40 +118,31 @@ namespace MWMechanics
///
/// \brief addSpell
/// \param id ID for stacking purposes.
/// \param stack If false, the spell is not added if one with the same ID exists already.
/// \param effects
/// \param displayName Name for display in magic menu.
///
void addSpell (const std::string& id, bool stack, const std::vector<ActiveEffect>& effects,
const std::string& displayName, int casterActorId);
void addSpell (const ActiveSpellParams& params);
/// Bypasses resistances
void addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor);
/// Removes the active effects from this spell/potion/.. with \a id
void removeEffects (const std::string& id);
void removeEffects (const MWWorld::Ptr& ptr, const std::string& id);
/// Remove all active effects with this effect id
void purgeEffect (short effectId);
/// Remove all active effects with this effect id and source id
void purgeEffect (short effectId, const std::string& sourceId, int effectIndex=-1);
void purgeEffect (const MWWorld::Ptr& ptr, short effectId);
/// Remove all active effects, if roll succeeds (for each effect)
void purgeAll(float chance, bool spellOnly = false);
void purge(EffectPredicate predicate, const MWWorld::Ptr& ptr);
void purge(ParamsPredicate predicate, const MWWorld::Ptr& ptr);
/// Remove all effects with CASTER_LINKED flag that were cast by \a casterActorId
void purge (int casterActorId);
/// Remove all effects that were cast by \a casterActorId
void purge (const MWWorld::Ptr& ptr, int casterActorId);
/// Remove all spells
void clear();
void clear(const MWWorld::Ptr& ptr);
bool isSpellActive (const std::string& id) const;
///< case insensitive
void purgeCorprusDisease();
const MagicEffects& getMagicEffects() const;
void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor) const;
void skipWorsenings(double hours);
};
}

File diff suppressed because it is too large Load Diff

@ -41,15 +41,11 @@ namespace MWMechanics
{
std::map<std::string, int> mDeathCount;
void addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor);
void removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor);
void adjustMagicEffects (const MWWorld::Ptr& creature);
void adjustMagicEffects (const MWWorld::Ptr& creature, float duration);
void calculateDynamicStats (const MWWorld::Ptr& ptr);
void calculateCreatureStatModifiers (const MWWorld::Ptr& ptr, float duration);
void calculateNpcStatModifiers (const MWWorld::Ptr& ptr, float duration);
void calculateRestoration (const MWWorld::Ptr& ptr, float duration);
@ -208,7 +204,6 @@ namespace MWMechanics
private:
void updateVisibility (const MWWorld::Ptr& ptr, CharacterController* ctrl);
void applyCureEffects (const MWWorld::Ptr& actor);
PtrActorMap mActors;
float mTimerDisposeSummonsCorpses;

@ -208,14 +208,14 @@ namespace MWMechanics
}
}
for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it)
for (const ESM::Spell* spell : spells)
{
float rating = rateSpell(it->first, actor, enemy);
float rating = rateSpell(spell, actor, enemy);
if (rating > bestActionRating)
{
bestActionRating = rating;
bestAction.reset(new ActionSpell(it->first->mId));
antiFleeRating = vanillaRateSpell(it->first, actor, enemy);
bestAction.reset(new ActionSpell(spell->mId));
antiFleeRating = vanillaRateSpell(spell, actor, enemy);
}
}
@ -266,9 +266,9 @@ namespace MWMechanics
}
}
for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it)
for (const ESM::Spell* spell : spells)
{
float rating = rateSpell(it->first, actor, enemy);
float rating = rateSpell(spell, actor, enemy);
if (rating > bestActionRating)
{
bestActionRating = rating;

@ -545,20 +545,12 @@ namespace MWMechanics
mAiSequence.writeState(state.mAiSequence);
mMagicEffects.writeState(state.mMagicEffects);
state.mSummonedCreatureMap = mSummonedCreatures;
state.mSummonedCreatures = mSummonedCreatures;
state.mSummonGraveyard = mSummonGraveyard;
state.mHasAiSettings = true;
for (int i=0; i<4; ++i)
mAiSettings[i].writeState (state.mAiSettings[i]);
for (auto it = mCorprusSpells.begin(); it != mCorprusSpells.end(); ++it)
{
for (int i=0; i<ESM::Attribute::Length; ++i)
state.mCorprusSpells[it->first].mWorsenings[i] = mCorprusSpells.at(it->first).mWorsenings[i];
state.mCorprusSpells[it->first].mNextWorsening = mCorprusSpells.at(it->first).mNextWorsening.toEsm();
}
}
void CreatureStats::readState (const ESM::CreatureStats& state)
@ -618,7 +610,7 @@ namespace MWMechanics
// We can't use mActiveSpells::getMagicEffects here because it doesn't include expired effects
auto spell = std::find_if(mActiveSpells.begin(), mActiveSpells.end(), [&] (const auto& spell)
{
const auto& effects = spell.second.mEffects;
const auto& effects = spell.getEffects();
return std::find_if(effects.begin(), effects.end(), [&] (const auto& effect)
{
return effect.mEffectId == effectId;
@ -629,21 +621,12 @@ namespace MWMechanics
}
}
mSummonedCreatures = state.mSummonedCreatureMap;
mSummonedCreatures = state.mSummonedCreatures;
mSummonGraveyard = state.mSummonGraveyard;
if (state.mHasAiSettings)
for (int i=0; i<4; ++i)
mAiSettings[i].readState(state.mAiSettings[i]);
mCorprusSpells.clear();
for (auto it = state.mCorprusSpells.begin(); it != state.mCorprusSpells.end(); ++it)
{
for (int i=0; i<ESM::Attribute::Length; ++i)
mCorprusSpells[it->first].mWorsenings[i] = state.mCorprusSpells.at(it->first).mWorsenings[i];
mCorprusSpells[it->first].mNextWorsening = MWWorld::TimeStamp(state.mCorprusSpells.at(it->first).mNextWorsening);
}
}
void CreatureStats::setLastRestockTime(MWWorld::TimeStamp tradeTime)
@ -710,7 +693,7 @@ namespace MWMechanics
return mTimeOfDeath;
}
std::map<ESM::SummonKey, int>& CreatureStats::getSummonedCreatureMap()
std::multimap<int, int>& CreatureStats::getSummonedCreatureMap()
{
return mSummonedCreatures;
}
@ -719,23 +702,4 @@ namespace MWMechanics
{
return mSummonGraveyard;
}
std::map<std::string, CorprusStats> &CreatureStats::getCorprusSpells()
{
return mCorprusSpells;
}
void CreatureStats::addCorprusSpell(const std::string& sourceId, CorprusStats& stats)
{
mCorprusSpells[sourceId] = stats;
}
void CreatureStats::removeCorprusSpell(const std::string& sourceId)
{
auto corprusIt = mCorprusSpells.find(sourceId);
if (corprusIt != mCorprusSpells.end())
{
mCorprusSpells.erase(corprusIt);
}
}
}

@ -1,6 +1,7 @@
#ifndef GAME_MWMECHANICS_CREATURESTATS_H
#define GAME_MWMECHANICS_CREATURESTATS_H
#include <map>
#include <set>
#include <string>
#include <stdexcept>
@ -87,14 +88,12 @@ namespace MWMechanics
float mSideMovementAngle;
private:
std::map<ESM::SummonKey, int> mSummonedCreatures; // <SummonKey, ActorId>
std::multimap<int, int> mSummonedCreatures; // <Effect, ActorId>
// Contains ActorIds of summoned creatures with an expired lifetime that have not been deleted yet.
// This may be necessary when the creature is in an inactive cell.
std::vector<int> mSummonGraveyard;
std::map<std::string, CorprusStats> mCorprusSpells;
protected:
int mLevel;
@ -236,7 +235,7 @@ namespace MWMechanics
void setBlock(bool value);
bool getBlock() const;
std::map<ESM::SummonKey, int>& getSummonedCreatureMap(); // <SummonKey, ActorId of summoned creature>
std::multimap<int, int>& getSummonedCreatureMap(); // <Effect, ActorId of summoned creature>
std::vector<int>& getSummonedCreatureGraveyard(); // ActorIds
enum Flag
@ -297,12 +296,6 @@ namespace MWMechanics
static void cleanup();
std::map<std::string, CorprusStats> & getCorprusSpells();
void addCorprusSpell(const std::string& sourceId, CorprusStats& stats);
void removeCorprusSpell(const std::string& sourceId);
float getSideMovementAngle() const { return mSideMovementAngle; }
void setSideMovementAngle(float angle) { mSideMovementAngle = angle; }
};

@ -30,12 +30,11 @@ namespace MWMechanics
MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find(
"fDiseaseXferChance")->mValue.getFloat();
MagicEffects& actorEffects = actor.getClass().getCreatureStats(actor).getMagicEffects();
const MagicEffects& actorEffects = actor.getClass().getCreatureStats(actor).getMagicEffects();
Spells& spells = carrier.getClass().getCreatureStats(carrier).getSpells();
for (Spells::TIterator it = spells.begin(); it != spells.end(); ++it)
for (const ESM::Spell* spell : spells)
{
const ESM::Spell* spell = it->first;
if (actor.getClass().getCreatureStats(actor).getSpells().hasSpell(spell->mId))
continue;
@ -56,7 +55,7 @@ namespace MWMechanics
if (Misc::Rng::rollDice(10000) < x)
{
// Contracted disease!
actor.getClass().getCreatureStats(actor).getSpells().add(it->first);
actor.getClass().getCreatureStats(actor).getSpells().add(spell);
MWBase::Environment::get().getWorld()->applyLoopingParticles(actor);
std::string msg = "sMagicContractDisease";

@ -1,75 +0,0 @@
#include "linkedeffects.hpp"
#include <components/misc/rng.hpp>
#include <components/settings/settings.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
#include "../mwrender/animation.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp"
#include "creaturestats.hpp"
namespace MWMechanics
{
bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect,
const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects)
{
if (caster.isEmpty() || caster == target || !target.getClass().isActor())
return false;
bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful;
bool isUnreflectable = magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable;
if (!isHarmful || isUnreflectable)
return false;
float reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).getMagnitude();
if (Misc::Rng::roll0to99() >= reflect)
return false;
const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find ("VFX_Reflect");
MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target);
if (animation && !reflectStatic->mModel.empty())
animation->addEffect("meshes\\" + reflectStatic->mModel, ESM::MagicEffect::Reflect, false, std::string());
reflectedEffects.mList.emplace_back(effect);
return true;
}
void absorbStat(const ESM::ENAMstruct& effect, const ESM::ActiveEffect& appliedEffect,
const MWWorld::Ptr& caster, const MWWorld::Ptr& target, bool reflected, const std::string& source)
{
if (caster.isEmpty() || caster == target)
return;
if (!target.getClass().isActor() || !caster.getClass().isActor())
return;
// Make sure callers don't do something weird
if (effect.mEffectID < ESM::MagicEffect::AbsorbAttribute || effect.mEffectID > ESM::MagicEffect::AbsorbSkill)
throw std::runtime_error("invalid absorb stat effect");
if (appliedEffect.mMagnitude == 0)
return;
std::vector<ActiveSpells::ActiveEffect> absorbEffects;
ActiveSpells::ActiveEffect absorbEffect = appliedEffect;
absorbEffect.mMagnitude *= -1;
absorbEffect.mEffectIndex = appliedEffect.mEffectIndex;
absorbEffects.emplace_back(absorbEffect);
// Morrowind negates reflected Absorb spells so the original caster won't be harmed.
if (reflected && Settings::Manager::getBool("classic reflected absorb spells behavior", "Game"))
{
target.getClass().getCreatureStats(target).getActiveSpells().addSpell(std::string(), true,
absorbEffects, source, caster.getClass().getCreatureStats(caster).getActorId());
return;
}
caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(std::string(), true,
absorbEffects, source, target.getClass().getCreatureStats(target).getActorId());
}
}

@ -1,32 +0,0 @@
#ifndef MWMECHANICS_LINKEDEFFECTS_H
#define MWMECHANICS_LINKEDEFFECTS_H
#include <string>
namespace ESM
{
struct ActiveEffect;
struct EffectList;
struct ENAMstruct;
struct MagicEffect;
struct Spell;
}
namespace MWWorld
{
class Ptr;
}
namespace MWMechanics
{
// Try to reflect a spell effect. If it's reflected, it's also put into the passed reflected effects list.
bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect,
const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects);
// Try to absorb a stat (skill, attribute, etc.) from the target and transfer it to the caster.
void absorbStat(const ESM::ENAMstruct& effect, const ESM::ActiveEffect& appliedEffect,
const MWWorld::Ptr& caster, const MWWorld::Ptr& target, bool reflected, const std::string& source);
}
#endif

@ -193,22 +193,22 @@ namespace MWMechanics
void MagicEffects::writeState(ESM::MagicEffects &state) const
{
// Don't need to save Modifiers, they are recalculated every frame anyway.
for (Collection::const_iterator iter (begin()); iter!=end(); ++iter)
for (const auto& [key, params] : mCollection)
{
if (iter->second.getBase() != 0)
if (params.getBase() != 0 || params.getModifier() != 0.f)
{
// Don't worry about mArg, never used by magic effect script instructions
state.mEffects.insert(std::make_pair(iter->first.mId, iter->second.getBase()));
state.mEffects[key.mId] = {params.getBase(), params.getModifier()};
}
}
}
void MagicEffects::readState(const ESM::MagicEffects &state)
{
for (std::map<int, int>::const_iterator it = state.mEffects.begin(); it != state.mEffects.end(); ++it)
for (const auto& [key, params] : state.mEffects)
{
mCollection[EffectKey(it->first)].setBase(it->second);
mCollection[EffectKey(key)].setBase(params.first);
mCollection[EffectKey(key)].setModifier(params.second);
}
}
}

@ -69,16 +69,6 @@ namespace MWMechanics
return param -= right;
}
// Used by effect management classes (ActiveSpells, InventoryStore, Spells) to list active effect sources for GUI display
struct EffectSourceVisitor
{
virtual ~EffectSourceVisitor() { }
virtual void visit (EffectKey key, int effectIndex,
const std::string& sourceName, const std::string& sourceId, int casterActorId,
float magnitude, float remainingTime = -1, float totalTime = -1) = 0;
};
/// \brief Effects currently affecting a NPC or creature
class MagicEffects
{

@ -84,7 +84,7 @@ namespace MWMechanics
// reset
creatureStats.setLevel(player->mNpdt.mLevel);
creatureStats.getSpells().clear(true);
creatureStats.modifyMagicEffects(MagicEffects());
creatureStats.getActiveSpells().clear(ptr);
for (int i=0; i<27; ++i)
npcStats.getSkill (i).setBase (player->mNpdt.mSkills[i]);
@ -213,6 +213,7 @@ namespace MWMechanics
int attributes[ESM::Attribute::Length];
for (int i=0; i<ESM::Attribute::Length; ++i)
attributes[i] = npcStats.getAttribute(i).getBase();
npcStats.updateHealth();
std::vector<std::string> selectedSpells = autoCalcPlayerSpells(skills, attributes, race);
@ -221,6 +222,7 @@ namespace MWMechanics
// forced update and current value adjustments
mActors.updateActor (ptr, 0);
mActors.updateActor (ptr, 0);
for (int i=0; i<3; ++i)
{
@ -282,24 +284,6 @@ namespace MWMechanics
mObjects.dropObjects(cellStore);
}
void MechanicsManager::restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId)
{
auto& stats = actor.getClass().getCreatureStats (actor);
auto& corprusSpells = stats.getCorprusSpells();
auto corprusIt = corprusSpells.find(sourceId);
if (corprusIt != corprusSpells.end())
{
for (int i = 0; i < ESM::Attribute::Length; ++i)
{
MWMechanics::AttributeValue attr = stats.getAttribute(i);
attr.restore(corprusIt->second.mWorsenings[i]);
actor.getClass().getCreatureStats(actor).setAttribute(i, attr);
}
}
}
void MechanicsManager::update(float duration, bool paused)
{
// Note: we should do it here since game mechanics and world updates use these values

@ -230,8 +230,6 @@ namespace MWMechanics
GreetingState getGreetingState(const MWWorld::Ptr& ptr) const override;
bool isTurningToPlayer(const MWWorld::Ptr& ptr) const override;
void restoreStatsAfterCorprus(const MWWorld::Ptr& actor, const std::string& sourceId) override;
private:
bool canCommitCrimeAgainst(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker);
bool canReportCrime(const MWWorld::Ptr &actor, const MWWorld::Ptr &victim, std::set<MWWorld::Ptr> &playerFollowers);

@ -16,33 +16,30 @@
namespace MWMechanics
{
class GetAbsorptionProbability : public MWMechanics::EffectSourceVisitor
float getProbability(const MWMechanics::ActiveSpells& activeSpells)
{
public:
float mProbability{0.f};
GetAbsorptionProbability() = default;
void visit (MWMechanics::EffectKey key, int /*effectIndex*/,
const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/,
float magnitude, float /*remainingTime*/, float /*totalTime*/) override
float probability = 0.f;
for(const auto& params : activeSpells)
{
if (key.mId == ESM::MagicEffect::SpellAbsorption)
for(const auto& effect : params.getEffects())
{
if (mProbability == 0.f)
mProbability = magnitude / 100;
else
if(effect.mEffectId == ESM::MagicEffect::SpellAbsorption)
{
// If there are different sources of SpellAbsorption effect, multiply failing probability for all effects.
// Real absorption probability will be the (1 - total fail chance) in this case.
float failProbability = 1.f - mProbability;
failProbability *= 1.f - magnitude / 100;
mProbability = 1.f - failProbability;
if(probability == 0.f)
probability = effect.mMagnitude / 100;
else
{
// If there are different sources of SpellAbsorption effect, multiply failing probability for all effects.
// Real absorption probability will be the (1 - total fail chance) in this case.
float failProbability = 1.f - probability;
failProbability *= 1.f - effect.mMagnitude / 100;
probability = 1.f - failProbability;
}
}
}
}
};
return static_cast<int>(probability * 100);
}
bool absorbSpell (const std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target)
{
@ -53,13 +50,7 @@ namespace MWMechanics
if (stats.getMagicEffects().get(ESM::MagicEffect::SpellAbsorption).getMagnitude() <= 0.f)
return false;
GetAbsorptionProbability check;
stats.getActiveSpells().visitEffectSources(check);
stats.getSpells().visitEffectSources(check);
if (target.getClass().hasInventoryStore(target))
target.getClass().getInventoryStore(target).visitEffectSources(check);
int chance = check.mProbability * 100;
int chance = getProbability(stats.getActiveSpells());
if (Misc::Rng::roll0to99() >= chance)
return false;

@ -22,14 +22,38 @@
#include "actorutil.hpp"
#include "aifollow.hpp"
#include "creaturestats.hpp"
#include "linkedeffects.hpp"
#include "spellabsorption.hpp"
#include "spellresistance.hpp"
#include "spelleffects.hpp"
#include "spellutil.hpp"
#include "summoning.hpp"
#include "tickableeffects.hpp"
#include "weapontype.hpp"
namespace
{
bool reflectEffect(const ESM::ENAMstruct& effect, const ESM::MagicEffect* magicEffect,
const MWWorld::Ptr& caster, const MWWorld::Ptr& target, ESM::EffectList& reflectedEffects)
{
if (caster.isEmpty() || caster == target || !target.getClass().isActor())
return false;
bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful;
bool isUnreflectable = magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable;
if (!isHarmful || isUnreflectable)
return false;
float reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).getMagnitude();
if (Misc::Rng::roll0to99() >= reflect)
return false;
const ESM::Static* reflectStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find ("VFX_Reflect");
MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target);
if (animation && !reflectStatic->mModel.empty())
animation->addEffect("meshes\\" + reflectStatic->mModel, ESM::MagicEffect::Reflect, false, std::string());
reflectedEffects.mList.emplace_back(effect);
return true;
}
}
namespace MWMechanics
{
CastSpell::CastSpell(const MWWorld::Ptr &caster, const MWWorld::Ptr &target, const bool fromProjectile, const bool manualSpell)
@ -54,7 +78,7 @@ namespace MWMechanics
(mTarget.getRefData().getPosition().asVec3() + offset) -
(mCaster.getRefData().getPosition().asVec3());
MWBase::Environment::get().getWorld()->launchMagicBolt(mId, mCaster, fallbackDirection);
MWBase::Environment::get().getWorld()->launchMagicBolt(mId, mCaster, fallbackDirection, mSlot);
}
void CastSpell::inflict(const MWWorld::Ptr &target, const MWWorld::Ptr &caster,
@ -100,20 +124,13 @@ namespace MWMechanics
}
ESM::EffectList reflectedEffects;
std::vector<ActiveSpells::ActiveEffect> appliedLastingEffects;
// HACK: cache target's magic effects here, and add any applied effects to it. Use the cached effects for determining resistance.
// This is required for Weakness effects in a spell to apply to any subsequent effects in the spell.
// Otherwise, they'd only apply after the whole spell was added.
MagicEffects targetEffects;
if (targetIsActor)
targetEffects += target.getClass().getCreatureStats(target).getMagicEffects();
ActiveSpells::ActiveSpellParams params(*this, caster);
bool castByPlayer = (!caster.isEmpty() && caster == getPlayer());
ActiveSpells targetSpells;
const ActiveSpells* targetSpells = nullptr;
if (targetIsActor)
targetSpells = target.getClass().getCreatureStats(target).getActiveSpells();
targetSpells = &target.getClass().getCreatureStats(target).getActiveSpells();
bool canCastAnEffect = false; // For bound equipment.If this remains false
// throughout the iteration of this spell's
@ -134,7 +151,7 @@ namespace MWMechanics
effectIt->mEffectID);
// Re-casting a bound equipment effect has no effect if the spell is still active
if (magicEffect->mData.mFlags & ESM::MagicEffect::NonRecastable && targetSpells.isSpellActive(mId))
if (magicEffect->mData.mFlags & ESM::MagicEffect::NonRecastable && targetSpells && targetSpells->isSpellActive(mId))
{
if (effectIt == (effects.mList.end() - 1) && !canCastAnEffect && castByPlayer)
MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCannotRecast}");
@ -163,307 +180,70 @@ namespace MWMechanics
if (!reflected && reflectEffect(*effectIt, magicEffect, caster, target, reflectedEffects))
continue;
// Try resisting.
float magnitudeMult = getEffectMultiplier(effectIt->mEffectID, target, caster, spell, &targetEffects);
if (magnitudeMult == 0)
ActiveSpells::ActiveEffect effect;
effect.mEffectId = effectIt->mEffectID;
effect.mArg = MWMechanics::EffectKey(*effectIt).mArg;
effect.mMagnitude = 0.f;
effect.mMinMagnitude = effectIt->mMagnMin;
effect.mMaxMagnitude = effectIt->mMagnMax;
effect.mTimeLeft = 0.f;
effect.mEffectIndex = currentEffectIndex;
effect.mFlags = ESM::ActiveEffect::Flag_None;
// Avoid applying harmful effects to the player in god mode
if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState() && isHarmful)
{
// Fully resisted, show message
if (target == getPlayer())
MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicPCResisted}");
else if (castByPlayer)
MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}");
effect.mMinMagnitude = 0;
effect.mMaxMagnitude = 0;
}
else
{
float magnitude = effectIt->mMagnMin + Misc::Rng::rollDice(effectIt->mMagnMax - effectIt->mMagnMin + 1);
magnitude *= magnitudeMult;
if (!target.getClass().isActor())
{
// non-actor objects have no list of active magic effects, so have to apply instantly
if (!applyInstantEffect(target, caster, EffectKey(*effectIt), magnitude))
continue;
}
else // target.getClass().isActor() == true
{
ActiveSpells::ActiveEffect effect;
effect.mEffectId = effectIt->mEffectID;
effect.mArg = MWMechanics::EffectKey(*effectIt).mArg;
effect.mMagnitude = magnitude;
effect.mTimeLeft = 0.f;
effect.mEffectIndex = currentEffectIndex;
// Avoid applying absorb effects if the caster is the target
// We still need the spell to be added
if (caster == target
&& effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute
&& effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill)
{
effect.mMagnitude = 0;
}
bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration);
effect.mDuration = hasDuration ? static_cast<float>(effectIt->mDuration) : 1.f;
// Avoid applying harmful effects to the player in god mode
if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState() && isHarmful)
{
effect.mMagnitude = 0;
}
bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce;
if (!appliedOnce)
effect.mDuration = std::max(1.f, effect.mDuration);
bool effectAffectsHealth = isHarmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth;
if (castByPlayer && target != caster && !target.getClass().getCreatureStats(target).isDead() && effectAffectsHealth)
{
// If player is attempting to cast a harmful spell on or is healing a living target, show the target's HP bar.
MWBase::Environment::get().getWindowManager()->setEnemy(target);
}
bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration);
effect.mDuration = hasDuration ? static_cast<float>(effectIt->mDuration) : 1.f;
effect.mTimeLeft = effect.mDuration;
bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce;
if (!appliedOnce)
effect.mDuration = std::max(1.f, effect.mDuration);
// add to list of active effects, to apply in next frame
params.getEffects().emplace_back(effect);
if (effect.mDuration == 0)
{
// We still should add effect to list to allow GetSpellEffects to detect this spell
appliedLastingEffects.push_back(effect);
// duration 0 means apply full magnitude instantly
bool wasDead = target.getClass().getCreatureStats(target).isDead();
effectTick(target.getClass().getCreatureStats(target), target, EffectKey(*effectIt), effect.mMagnitude);
bool isDead = target.getClass().getCreatureStats(target).isDead();
if (!wasDead && isDead)
MWBase::Environment::get().getMechanicsManager()->actorKilled(target, caster);
}
else
{
effect.mTimeLeft = effect.mDuration;
targetEffects.add(MWMechanics::EffectKey(*effectIt), MWMechanics::EffectParam(effect.mMagnitude));
// add to list of active effects, to apply in next frame
appliedLastingEffects.push_back(effect);
// Unequip all items, if a spell with the ExtraSpell effect was casted
if (effectIt->mEffectID == ESM::MagicEffect::ExtraSpell && target.getClass().hasInventoryStore(target))
{
MWWorld::InventoryStore& store = target.getClass().getInventoryStore(target);
store.unequipAll(target);
}
// Command spells should have their effect, including taking the target out of combat, each time the spell successfully affects the target
if (((effectIt->mEffectID == ESM::MagicEffect::CommandHumanoid && target.getClass().isNpc())
|| (effectIt->mEffectID == ESM::MagicEffect::CommandCreature && target.getTypeName() == typeid(ESM::Creature).name()))
&& !caster.isEmpty() && caster.getClass().isActor() && target != getPlayer() && effect.mMagnitude >= target.getClass().getCreatureStats(target).getLevel())
{
MWMechanics::AiFollow package(caster, true);
target.getClass().getCreatureStats(target).getAiSequence().stack(package, target);
}
// For absorb effects, also apply the effect to the caster - but with a negative
// magnitude, since we're transferring stats from the target to the caster
if (effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute && effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill)
absorbStat(*effectIt, effect, caster, target, reflected, mSourceName);
}
}
// Re-casting a summon effect will remove the creature from previous castings of that effect.
if (isSummoningEffect(effectIt->mEffectID) && targetIsActor)
{
CreatureStats& targetStats = target.getClass().getCreatureStats(target);
ESM::SummonKey key(effectIt->mEffectID, mId, currentEffectIndex);
auto findCreature = targetStats.getSummonedCreatureMap().find(key);
if (findCreature != targetStats.getSummonedCreatureMap().end())
{
MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(target, findCreature->second);
targetStats.getSummonedCreatureMap().erase(findCreature);
}
}
if (target.getClass().isActor() || magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)
{
static const std::string schools[] = {
"alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration"
};
bool effectAffectsHealth = isHarmful || effectIt->mEffectID == ESM::MagicEffect::RestoreHealth;
if (castByPlayer && target != caster && targetIsActor && effectAffectsHealth)
{
// If player is attempting to cast a harmful spell on or is healing a living target, show the target's HP bar.
MWBase::Environment::get().getWindowManager()->setEnemy(target);
}
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
if(!magicEffect->mHitSound.empty())
sndMgr->playSound3D(target, magicEffect->mHitSound, 1.0f, 1.0f);
else
sndMgr->playSound3D(target, schools[magicEffect->mData.mSchool]+" hit", 1.0f, 1.0f);
// Add VFX
const ESM::Static* castStatic;
if (!magicEffect->mHit.empty())
castStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find (magicEffect->mHit);
else
castStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find ("VFX_DefaultHit");
bool loop = (magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0;
// Note: in case of non actor, a free effect should be fine as well
MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(target);
if (anim && !castStatic->mModel.empty())
anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, loop, "", magicEffect->mParticle);
}
if (targetIsActor || magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)
{
playEffects(target, *magicEffect);
}
}
if (!exploded)
MWBase::Environment::get().getWorld()->explodeSpell(mHitPosition, effects, caster, target, range, mId, mSourceName, mFromProjectile);
MWBase::Environment::get().getWorld()->explodeSpell(mHitPosition, effects, caster, target, range, mId, mSourceName, mFromProjectile, mSlot);
if (!target.isEmpty())
{
if (!reflectedEffects.mList.empty())
inflict(caster, target, reflectedEffects, range, true, exploded);
if (!appliedLastingEffects.empty())
{
int casterActorId = -1;
if (!caster.isEmpty() && caster.getClass().isActor())
casterActorId = caster.getClass().getCreatureStats(caster).getActorId();
target.getClass().getCreatureStats(target).getActiveSpells().addSpell(mId, mStack, appliedLastingEffects,
mSourceName, casterActorId);
}
}
}
bool CastSpell::applyInstantEffect(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, const MWMechanics::EffectKey& effect, float magnitude)
{
short effectId = effect.mId;
if (target.getClass().canLock(target))
{
if (effectId == ESM::MagicEffect::Lock)
{
const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore();
const ESM::MagicEffect *magiceffect = store.get<ESM::MagicEffect>().find(effectId);
MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target);
if (animation)
animation->addSpellCastGlow(magiceffect);
if (target.getCellRef().getLockLevel() < magnitude) //If the door is not already locked to a higher value, lock it to spell magnitude
{
if (caster == getPlayer())
MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicLockSuccess}");
target.getCellRef().lock(static_cast<int>(magnitude));
}
return true;
}
else if (effectId == ESM::MagicEffect::Open)
if (!params.getEffects().empty())
{
if (!caster.isEmpty())
{
MWBase::Environment::get().getMechanicsManager()->unlockAttempted(getPlayer(), target);
// Use the player instead of the caster for vanilla crime compatibility
}
const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore();
const ESM::MagicEffect *magiceffect = store.get<ESM::MagicEffect>().find(effectId);
MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target);
if (animation)
animation->addSpellCastGlow(magiceffect);
if (target.getCellRef().getLockLevel() <= magnitude)
{
if (target.getCellRef().getLockLevel() > 0)
{
MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock", 1.f, 1.f);
if (caster == getPlayer())
MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicOpenSuccess}");
}
target.getCellRef().unlock();
}
if(targetIsActor)
target.getClass().getCreatureStats(target).getActiveSpells().addSpell(params);
else
{
MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock Fail", 1.f, 1.f);
// Apply effects instantly. We can ignore effect deletion since the entire params object gets deleted afterwards anyway
for(auto& effect : params.getEffects())
applyMagicEffect(target, caster, params, effect, 0.f);
}
return true;
}
}
else if (target.getClass().isActor() && effectId == ESM::MagicEffect::Dispel)
{
target.getClass().getCreatureStats(target).getActiveSpells().purgeAll(magnitude, true);
return true;
}
else if(target.getClass().isActor() && effectId >= ESM::MagicEffect::CalmHumanoid && effectId <= ESM::MagicEffect::RallyCreature)
{
// Treat X Humanoid spells on creatures and X Creature spells on NPCs as instant effects and remove their VFX
bool affectsCreatures = (effectId - ESM::MagicEffect::CalmHumanoid) & 1;
if(affectsCreatures == target.getClass().isNpc())
{
MWBase::Environment::get().getWorld()->getAnimation(target)->removeEffect(effectId);
return true;
}
}
else if(target.getClass().isActor() && effectId == ESM::MagicEffect::TurnUndead)
{
// Diverge from vanilla by giving scripts a chance to detect Turn Undead on non-undead, but still remove the effect and VFX
if(target.getClass().isNpc() || target.get<ESM::Creature>()->mBase->mData.mType != ESM::Creature::Undead)
{
MWBase::Environment::get().getWorld()->getAnimation(target)->removeEffect(effectId);
return true;
}
}
else if (target.getClass().isActor() && target == getPlayer())
{
MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mCaster);
bool teleportingEnabled = MWBase::Environment::get().getWorld()->isTeleportingEnabled();
if (effectId == ESM::MagicEffect::DivineIntervention || effectId == ESM::MagicEffect::AlmsiviIntervention)
{
if (!teleportingEnabled)
{
if (caster == getPlayer())
MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}");
return true;
}
std::string marker = (effectId == ESM::MagicEffect::DivineIntervention) ? "divinemarker" : "templemarker";
MWBase::Environment::get().getWorld()->teleportToClosestMarker(target, marker);
anim->removeEffect(effectId);
const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>()
.search("VFX_Summon_end");
if (fx)
anim->addEffect("meshes\\" + fx->mModel, -1);
return true;
}
else if (effectId == ESM::MagicEffect::Mark)
{
if (teleportingEnabled)
{
MWBase::Environment::get().getWorld()->getPlayer().markPosition(
target.getCell(), target.getRefData().getPosition());
}
else if (caster == getPlayer())
{
MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}");
}
return true;
}
else if (effectId == ESM::MagicEffect::Recall)
{
if (!teleportingEnabled)
{
if (caster == getPlayer())
MWBase::Environment::get().getWindowManager()->messageBox("#{sTeleportDisabled}");
return true;
}
MWWorld::CellStore* markedCell = nullptr;
ESM::Position markedPosition;
MWBase::Environment::get().getWorld()->getPlayer().getMarkedPosition(markedCell, markedPosition);
if (markedCell)
{
MWWorld::ActionTeleport action(markedCell->isExterior() ? "" : markedCell->getCell()->mName,
markedPosition, false);
action.execute(target);
anim->removeEffect(effectId);
}
return true;
}
}
return false;
}
bool CastSpell::cast(const std::string &id)
{
const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore();
@ -479,7 +259,7 @@ namespace MWMechanics
throw std::runtime_error("ID type cannot be casted");
}
bool CastSpell::cast(const MWWorld::Ptr &item, bool launchProjectile)
bool CastSpell::cast(const MWWorld::Ptr &item, int slot, bool launchProjectile)
{
std::string enchantmentName = item.getClass().getEnchantment(item);
if (enchantmentName.empty())
@ -490,7 +270,7 @@ namespace MWMechanics
const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get<ESM::Enchantment>().find(enchantmentName);
mStack = false;
mSlot = slot;
bool godmode = mCaster == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState();
bool isProjectile = false;
@ -570,7 +350,7 @@ namespace MWMechanics
{
mSourceName = potion->mName;
mId = potion->mId;
mStack = true;
mType = ESM::ActiveSpells::Type_Consumable;
inflict(mCaster, mCaster, potion->mEffects, ESM::RT_Self);
@ -581,7 +361,6 @@ namespace MWMechanics
{
mSourceName = spell->mName;
mId = spell->mId;
mStack = false;
const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore();
@ -656,7 +435,7 @@ namespace MWMechanics
bool CastSpell::cast (const ESM::Ingredient* ingredient)
{
mId = ingredient->mId;
mStack = true;
mType = ESM::ActiveSpells::Type_Consumable;
mSourceName = ingredient->mName;
ESM::ENAMstruct effect;
@ -799,4 +578,36 @@ namespace MWMechanics
sndMgr->playSound3D(mCaster, schools[effect->mData.mSchool]+" cast", 1.0f, 1.0f);
}
}
void playEffects(const MWWorld::Ptr& target, const ESM::MagicEffect& magicEffect, bool playNonLooping)
{
if (playNonLooping)
{
static const std::string schools[] = {
"alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration"
};
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
if(!magicEffect.mHitSound.empty())
sndMgr->playSound3D(target, magicEffect.mHitSound, 1.0f, 1.0f);
else
sndMgr->playSound3D(target, schools[magicEffect.mData.mSchool]+" hit", 1.0f, 1.0f);
}
// Add VFX
const ESM::Static* castStatic;
if (!magicEffect.mHit.empty())
castStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find (magicEffect.mHit);
else
castStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find ("VFX_DefaultHit");
bool loop = (magicEffect.mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0;
MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(target);
if(anim && !castStatic->mModel.empty())
{
// Don't play particle VFX unless the effect is new or it should be looping.
if (playNonLooping || loop)
anim->addEffect("meshes\\" + castStatic->mModel, magicEffect.mIndex, loop, "", magicEffect.mParticle);
}
}
}

@ -1,6 +1,7 @@
#ifndef MWMECHANICS_SPELLCASTING_H
#define MWMECHANICS_SPELLCASTING_H
#include <components/esm/activespells.hpp>
#include <components/esm/effectlist.hpp>
#include "../mwworld/ptr.hpp"
@ -11,6 +12,7 @@ namespace ESM
struct Ingredient;
struct Potion;
struct EffectList;
struct MagicEffect;
}
namespace MWMechanics
@ -26,13 +28,14 @@ namespace MWMechanics
void playSpellCastingEffects(const std::vector<ESM::ENAMstruct>& effects);
public:
bool mStack{false};
std::string mId; // ID of spell, potion, item etc
std::string mSourceName; // Display name for spell, potion, etc
osg::Vec3f mHitPosition{0,0,0}; // Used for spawning area orb
bool mAlwaysSucceed{false}; // Always succeed spells casted by NPCs/creatures regardless of their chance (default: false)
bool mFromProjectile; // True if spell is cast by enchantment of some projectile (arrow, bolt or thrown weapon)
bool mManualSpell; // True if spell is casted from script and ignores some checks (mana level, success chance, etc.)
int mSlot{0};
ESM::ActiveSpells::EffectType mType{ESM::ActiveSpells::Type_Temporary};
public:
CastSpell(const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile=false, const bool manualSpell=false);
@ -41,7 +44,7 @@ namespace MWMechanics
/// @note mCaster must be an actor
/// @param launchProjectile If set to false, "on target" effects are directly applied instead of being launched as projectile originating from the caster.
bool cast (const MWWorld::Ptr& item, bool launchProjectile=true);
bool cast (const MWWorld::Ptr& item, int slot, bool launchProjectile=true);
/// @note mCaster must be an NPC
bool cast (const ESM::Ingredient* ingredient);
@ -60,11 +63,9 @@ namespace MWMechanics
/// @note \a caster can be any type of object, or even an empty object.
void inflict (const MWWorld::Ptr& target, const MWWorld::Ptr& caster,
const ESM::EffectList& effects, ESM::RangeType range, bool reflected=false, bool exploded=false);
/// @note \a caster can be any type of object, or even an empty object.
/// @return was the target suitable for the effect?
bool applyInstantEffect (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, const MWMechanics::EffectKey& effect, float magnitude);
};
void playEffects(const MWWorld::Ptr& target, const ESM::MagicEffect& magicEffect, bool playNonLooping = true);
}
#endif

File diff suppressed because it is too large Load Diff

@ -0,0 +1,20 @@
#ifndef GAME_MWMECHANICS_SPELLEFFECTS_H
#define GAME_MWMECHANICS_SPELLEFFECTS_H
#include "activespells.hpp"
#include "../mwworld/ptr.hpp"
// These functions should probably be split up into separate Lua functions for each magic effect when magic is dehardcoded.
// That way ESM::MGEF could point to two Lua scripts for each effect. Needs discussion.
namespace MWMechanics
{
// Applies a tick of a single effect. Returns true if the effect should be removed immediately
bool applyMagicEffect(const MWWorld::Ptr& target, const MWWorld::Ptr& caster, ActiveSpells::ActiveSpellParams& spellParams, ESM::ActiveEffect& effect, float dt);
// Undoes permanent effects created by ESM::MagicEffect::AppliedOnce
void onMagicEffectRemoved(const MWWorld::Ptr& target, ActiveSpells::ActiveSpellParams& spell, const ESM::ActiveEffect& effect);
}
#endif

@ -16,12 +16,6 @@ namespace ESM
namespace MWMechanics
{
struct SpellParams
{
std::map<int, float> mEffectRands; // <effect index, normalised random magnitude>
std::set<int> mPurgedEffects; // indices of purged effects
};
class Spells;
/// Multiple instances of the same actor share the same spell list in Morrowind.

@ -31,21 +31,20 @@ namespace
// if the effect filter is not specified, take in account only spells effects. Leave potions, enchanted items etc.
if (effectFilter == -1)
{
const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().search(it->first);
const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().search(it->getId());
if (!spell || spell->mData.mType != ESM::Spell::ST_Spell)
continue;
}
const MWMechanics::ActiveSpells::ActiveSpellParams& params = it->second;
for (std::vector<MWMechanics::ActiveSpells::ActiveEffect>::const_iterator effectIt = params.mEffects.begin();
effectIt != params.mEffects.end(); ++effectIt)
const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it;
for (const auto& effect : params.getEffects())
{
int effectId = effectIt->mEffectId;
int effectId = effect.mEffectId;
if (effectFilter != -1 && effectId != effectFilter)
continue;
const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find(effectId);
if (effectIt->mDuration <= 3) // Don't attempt to dispel if effect runs out shortly anyway
if (effect.mDuration <= 3) // Don't attempt to dispel if effect runs out shortly anyway
continue;
if (negative && magicEffect->mData.mFlags & ESM::MagicEffect::Harmful)
@ -64,15 +63,14 @@ namespace
const MWMechanics::ActiveSpells& activeSpells = actor.getClass().getCreatureStats(actor).getActiveSpells();
for (MWMechanics::ActiveSpells::TIterator it = activeSpells.begin(); it != activeSpells.end(); ++it)
{
if (it->first != spellId)
if (it->getId() != spellId)
continue;
const MWMechanics::ActiveSpells::ActiveSpellParams& params = it->second;
for (std::vector<MWMechanics::ActiveSpells::ActiveEffect>::const_iterator effectIt = params.mEffects.begin();
effectIt != params.mEffects.end(); ++effectIt)
const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it;
for (const auto& effect : params.getEffects())
{
if (effectIt->mDuration > duration)
duration = effectIt->mDuration;
if (effect.mDuration > duration)
duration = effect.mDuration;
}
}
return duration;

@ -20,72 +20,33 @@
namespace MWMechanics
{
Spells::Spells()
: mSpellsChanged(false)
{
}
Spells::Spells(const Spells& spells) : mSpellList(spells.mSpellList), mSpells(spells.mSpells),
mSelectedSpell(spells.mSelectedSpell), mUsedPowers(spells.mUsedPowers),
mSpellsChanged(spells.mSpellsChanged), mEffects(spells.mEffects), mSourcedEffects(spells.mSourcedEffects)
mSelectedSpell(spells.mSelectedSpell), mUsedPowers(spells.mUsedPowers)
{
if(mSpellList)
mSpellList->addListener(this);
}
Spells::Spells(Spells&& spells) : mSpellList(std::move(spells.mSpellList)), mSpells(std::move(spells.mSpells)),
mSelectedSpell(std::move(spells.mSelectedSpell)), mUsedPowers(std::move(spells.mUsedPowers)),
mSpellsChanged(std::move(spells.mSpellsChanged)), mEffects(std::move(spells.mEffects)),
mSourcedEffects(std::move(spells.mSourcedEffects))
mSelectedSpell(std::move(spells.mSelectedSpell)), mUsedPowers(std::move(spells.mUsedPowers))
{
if (mSpellList)
mSpellList->updateListener(&spells, this);
}
std::map<const ESM::Spell*, SpellParams>::const_iterator Spells::begin() const
std::vector<const ESM::Spell*>::const_iterator Spells::begin() const
{
return mSpells.begin();
}
std::map<const ESM::Spell*, SpellParams>::const_iterator Spells::end() const
std::vector<const ESM::Spell*>::const_iterator Spells::end() const
{
return mSpells.end();
}
void Spells::rebuildEffects() const
{
mEffects = MagicEffects();
mSourcedEffects.clear();
for (const auto& iter : mSpells)
{
const ESM::Spell *spell = iter.first;
if (spell->mData.mType==ESM::Spell::ST_Ability || spell->mData.mType==ESM::Spell::ST_Blight ||
spell->mData.mType==ESM::Spell::ST_Disease || spell->mData.mType==ESM::Spell::ST_Curse)
{
int i=0;
for (const auto& effect : spell->mEffects.mList)
{
if (iter.second.mPurgedEffects.find(i) != iter.second.mPurgedEffects.end())
{
++i;
continue; // effect was purged
}
float random = 1.f;
if (iter.second.mEffectRands.find(i) != iter.second.mEffectRands.end())
random = iter.second.mEffectRands.at(i);
float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * random;
mEffects.add (effect, magnitude);
mSourcedEffects[spell].add(MWMechanics::EffectKey(effect), magnitude);
++i;
}
}
}
}
bool Spells::hasSpell(const std::string &spell) const
{
return hasSpell(SpellList::getSpell(spell));
@ -93,7 +54,7 @@ namespace MWMechanics
bool Spells::hasSpell(const ESM::Spell *spell) const
{
return mSpells.find(spell) != mSpells.end();
return std::find(mSpells.begin(), mSpells.end(), spell) != mSpells.end();
}
void Spells::add (const ESM::Spell* spell)
@ -108,29 +69,8 @@ namespace MWMechanics
void Spells::addSpell(const ESM::Spell* spell)
{
if (mSpells.find (spell)==mSpells.end())
{
std::map<int, float> random;
// Determine the random magnitudes (unless this is a castable spell, in which case
// they will be determined when the spell is cast)
if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell)
{
for (unsigned int i=0; i<spell->mEffects.mList.size();++i)
{
if (spell->mEffects.mList[i].mMagnMin != spell->mEffects.mList[i].mMagnMax)
{
int delta = spell->mEffects.mList[i].mMagnMax - spell->mEffects.mList[i].mMagnMin;
random[i] = Misc::Rng::rollDice(delta + 1) / static_cast<float>(delta);
}
}
}
SpellParams params;
params.mEffectRands = random;
mSpells.emplace(spell, params);
mSpellsChanged = true;
}
if (!hasSpell(spell))
mSpells.emplace_back(spell);
}
void Spells::remove (const std::string& spellId)
@ -145,27 +85,14 @@ namespace MWMechanics
void Spells::removeSpell(const ESM::Spell* spell)
{
const auto it = mSpells.find(spell);
const auto it = std::find(mSpells.begin(), mSpells.end(), spell);
if(it != mSpells.end())
{
mSpells.erase(it);
mSpellsChanged = true;
}
}
MagicEffects Spells::getMagicEffects() const
{
if (mSpellsChanged) {
rebuildEffects();
mSpellsChanged = false;
}
return mEffects;
}
void Spells::removeAllSpells()
{
mSpells.clear();
mSpellsChanged = true;
}
void Spells::clear(bool modifyBase)
@ -185,26 +112,10 @@ namespace MWMechanics
return mSelectedSpell;
}
bool Spells::isSpellActive(const std::string &id) const
{
if (id.empty())
return false;
const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().search(id);
if (spell && hasSpell(spell))
{
auto type = spell->mData.mType;
return (type==ESM::Spell::ST_Ability || type==ESM::Spell::ST_Blight || type==ESM::Spell::ST_Disease || type==ESM::Spell::ST_Curse);
}
return false;
}
bool Spells::hasDisease(const ESM::Spell::SpellType type) const
{
for (const auto& iter : mSpells)
for (const auto spell : mSpells)
{
const ESM::Spell *spell = iter.first;
if (spell->mData.mType == type)
return true;
}
@ -227,12 +138,11 @@ namespace MWMechanics
std::vector<std::string> purged;
for (auto iter = mSpells.begin(); iter!=mSpells.end();)
{
const ESM::Spell *spell = iter->first;
const ESM::Spell *spell = *iter;
if (filter(spell))
{
mSpells.erase(iter++);
purged.push_back(spell->mId);
mSpellsChanged = true;
}
else
++iter;
@ -261,43 +171,6 @@ namespace MWMechanics
purge([](auto spell) { return spell->mData.mType == ESM::Spell::ST_Curse; });
}
void Spells::removeEffects(const std::string &id)
{
if (isSpellActive(id))
{
for (auto& spell : mSpells)
{
if (spell.first == SpellList::getSpell(id))
{
for (long unsigned int i = 0; i != spell.first->mEffects.mList.size(); i++)
{
spell.second.mPurgedEffects.insert(i);
}
}
}
mSpellsChanged = true;
}
}
void Spells::visitEffectSources(EffectSourceVisitor &visitor) const
{
if (mSpellsChanged) {
rebuildEffects();
mSpellsChanged = false;
}
for (const auto& it : mSourcedEffects)
{
const ESM::Spell * spell = it.first;
for (const auto& effectIt : it.second)
{
// FIXME: since Spells merges effects with the same ID, there is no sense to use multiple effects with same ID here
visitor.visit(effectIt.first, -1, spell->mName, spell->mId, -1, effectIt.second.getMagnitude());
}
}
}
bool Spells::hasCorprusEffect(const ESM::Spell *spell)
{
for (const auto& effectIt : spell->mEffects.mList)
@ -310,46 +183,6 @@ namespace MWMechanics
return false;
}
void Spells::purgeEffect(int effectId)
{
for (auto& spellIt : mSpells)
{
int i = 0;
for (auto& effectIt : spellIt.first->mEffects.mList)
{
if (effectIt.mEffectID == effectId)
{
spellIt.second.mPurgedEffects.insert(i);
mSpellsChanged = true;
}
++i;
}
}
}
void Spells::purgeEffect(int effectId, const std::string & sourceId)
{
// Effect source may be not a spell
const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().search(sourceId);
if (spell == nullptr)
return;
auto spellIt = mSpells.find(spell);
if (spellIt == mSpells.end())
return;
int index = 0;
for (auto& effectIt : spellIt->first->mEffects.mList)
{
if (effectIt.mEffectID == effectId)
{
spellIt->second.mPurgedEffects.insert(index);
mSpellsChanged = true;
}
++index;
}
}
bool Spells::canUsePower(const ESM::Spell* spell) const
{
const auto it = mUsedPowers.find(spell);
@ -365,17 +198,16 @@ namespace MWMechanics
{
const auto& baseSpells = mSpellList->getSpells();
for (ESM::SpellState::TContainer::const_iterator it = state.mSpells.begin(); it != state.mSpells.end(); ++it)
for (const std::string& id : state.mSpells)
{
// Discard spells that are no longer available due to changed content files
const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().search(it->first);
const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().search(id);
if (spell)
{
mSpells[spell].mEffectRands = it->second.mEffectRands;
mSpells[spell].mPurgedEffects = it->second.mPurgedEffects;
mSpells.emplace_back(spell);
if (it->first == state.mSelectedSpell)
mSelectedSpell = it->first;
if (id == state.mSelectedSpell)
mSelectedSpell = id;
}
}
// Add spells from the base record
@ -394,31 +226,6 @@ namespace MWMechanics
mUsedPowers[spell] = MWWorld::TimeStamp(it->second);
}
for (std::map<std::string, ESM::SpellState::CorprusStats>::const_iterator it = state.mCorprusSpells.begin(); it != state.mCorprusSpells.end(); ++it)
{
const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().search(it->first);
if (!spell)
continue;
CorprusStats stats;
int worsening = state.mCorprusSpells.at(it->first).mWorsenings;
for (int i=0; i<ESM::Attribute::Length; ++i)
stats.mWorsenings[i] = 0;
for (auto& effect : spell->mEffects.mList)
{
if (effect.mEffectID == ESM::MagicEffect::DrainAttribute)
stats.mWorsenings[effect.mAttribute] = worsening;
}
stats.mNextWorsening = MWWorld::TimeStamp(state.mCorprusSpells.at(it->first).mNextWorsening);
creatureStats->addCorprusSpell(it->first, stats);
}
mSpellsChanged = true;
// Permanent effects are used only to keep the custom magnitude of corprus spells effects (after cure too), and only in old saves. Convert data to the new approach.
for (std::map<std::string, std::vector<ESM::SpellState::PermanentSpellEffectInfo> >::const_iterator it =
state.mPermanentSpellEffects.begin(); it != state.mPermanentSpellEffects.end(); ++it)
@ -457,16 +264,13 @@ namespace MWMechanics
void Spells::writeState(ESM::SpellState &state) const
{
const auto& baseSpells = mSpellList->getSpells();
for (const auto& it : mSpells)
for (const auto spell : mSpells)
{
// Don't save spells and powers stored in the base record
if((it.first->mData.mType != ESM::Spell::ST_Spell && it.first->mData.mType != ESM::Spell::ST_Power) ||
std::find(baseSpells.begin(), baseSpells.end(), it.first->mId) == baseSpells.end())
if((spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power) ||
std::find(baseSpells.begin(), baseSpells.end(), spell->mId) == baseSpells.end())
{
ESM::SpellState::SpellParams params;
params.mEffectRands = it.second.mEffectRands;
params.mPurgedEffects = it.second.mPurgedEffects;
state.mSpells.emplace(it.first->mId, params);
state.mSpells.emplace_back(spell->mId);
}
}

@ -29,18 +29,13 @@ namespace MWMechanics
class Spells
{
std::shared_ptr<SpellList> mSpellList;
std::map<const ESM::Spell*, SpellParams> mSpells;
std::vector<const ESM::Spell*> mSpells;
// Note: this is the spell that's about to be cast, *not* the spell selected in the GUI (which may be different)
std::string mSelectedSpell;
std::map<const ESM::Spell*, MWWorld::TimeStamp> mUsedPowers;
mutable bool mSpellsChanged;
mutable MagicEffects mEffects;
mutable std::map<const ESM::Spell*, MagicEffects> mSourcedEffects;
void rebuildEffects() const;
bool hasDisease(const ESM::Spell::SpellType type) const;
using SpellFilter = bool (*)(const ESM::Spell*);
@ -52,8 +47,6 @@ namespace MWMechanics
friend class SpellList;
public:
using TIterator = std::map<const ESM::Spell*, SpellParams>::const_iterator;
Spells();
Spells(const Spells&);
@ -64,9 +57,6 @@ namespace MWMechanics
static bool hasCorprusEffect(const ESM::Spell *spell);
void purgeEffect(int effectId);
void purgeEffect(int effectId, const std::string & sourceId);
bool canUsePower (const ESM::Spell* spell) const;
void usePower (const ESM::Spell* spell);
@ -75,9 +65,9 @@ namespace MWMechanics
void purgeCorprusDisease();
void purgeCurses();
TIterator begin() const;
std::vector<const ESM::Spell*>::const_iterator begin() const;
TIterator end() const;
std::vector<const ESM::Spell*>::const_iterator end() const;
bool hasSpell(const std::string& spell) const;
bool hasSpell(const ESM::Spell* spell) const;
@ -92,9 +82,6 @@ namespace MWMechanics
///< If the spell to be removed is the selected spell, the selected spell will be changed to
/// no spell (empty string).
MagicEffects getMagicEffects() const;
///< Return sum of magic effects resulting from abilities, blights, deseases and curses.
void clear(bool modifyBase = false);
///< Remove all spells of al types.
@ -104,17 +91,10 @@ namespace MWMechanics
const std::string getSelectedSpell() const;
///< May return an empty string.
bool isSpellActive(const std::string& id) const;
///< Are we under the effects of the given spell ID?
bool hasCommonDisease() const;
bool hasBlightDisease() const;
void removeEffects(const std::string& id);
void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor) const;
void readState (const ESM::SpellState& state, CreatureStats* creatureStats);
void writeState (ESM::SpellState& state) const;

@ -60,90 +60,60 @@ namespace MWMechanics
return std::string();
}
UpdateSummonedCreatures::UpdateSummonedCreatures(const MWWorld::Ptr &actor)
: mActor(actor)
int summonCreature(int effectId, const MWWorld::Ptr& summoner)
{
}
void UpdateSummonedCreatures::visit(EffectKey key, int effectIndex, const std::string &sourceName, const std::string &sourceId, int casterActorId, float magnitude, float remainingTime, float totalTime)
{
if (isSummoningEffect(key.mId) && magnitude > 0)
std::string creatureID = getSummonedCreature(effectId);
int creatureActorId = -1;
if (!creatureID.empty())
{
mActiveEffects.insert(ESM::SummonKey(key.mId, sourceId, effectIndex));
}
}
try
{
auto world = MWBase::Environment::get().getWorld();
MWWorld::ManualRef ref(world->getStore(), creatureID, 1);
void UpdateSummonedCreatures::process(bool cleanup)
{
MWMechanics::CreatureStats& creatureStats = mActor.getClass().getCreatureStats(mActor);
std::map<ESM::SummonKey, int>& creatureMap = creatureStats.getSummonedCreatureMap();
MWMechanics::CreatureStats& summonedCreatureStats = ref.getPtr().getClass().getCreatureStats(ref.getPtr());
for (std::set<ESM::SummonKey>::iterator it = mActiveEffects.begin(); it != mActiveEffects.end(); ++it)
{
bool found = creatureMap.find(*it) != creatureMap.end();
if (!found)
{
std::string creatureID = getSummonedCreature(it->mEffectId);
if (!creatureID.empty())
// Make the summoned creature follow its master and help in fights
AiFollow package(summoner);
summonedCreatureStats.getAiSequence().stack(package, ref.getPtr());
creatureActorId = summonedCreatureStats.getActorId();
MWWorld::Ptr placed = world->safePlaceObject(ref.getPtr(), summoner, summoner.getCell(), 0, 120.f);
MWRender::Animation* anim = world->getAnimation(placed);
if (anim)
{
int creatureActorId = -1;
try
{
MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), creatureID, 1);
MWMechanics::CreatureStats& summonedCreatureStats = ref.getPtr().getClass().getCreatureStats(ref.getPtr());
// Make the summoned creature follow its master and help in fights
AiFollow package(mActor);
summonedCreatureStats.getAiSequence().stack(package, ref.getPtr());
creatureActorId = summonedCreatureStats.getActorId();
MWWorld::Ptr placed = MWBase::Environment::get().getWorld()->safePlaceObject(ref.getPtr(), mActor, mActor.getCell(), 0, 120.f);
MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(placed);
if (anim)
{
const ESM::Static* fx = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>()
.search("VFX_Summon_Start");
if (fx)
anim->addEffect("meshes\\" + fx->mModel, -1, false);
}
}
catch (std::exception& e)
{
Log(Debug::Error) << "Failed to spawn summoned creature: " << e.what();
// still insert into creatureMap so we don't try to spawn again every frame, that would spam the warning log
}
creatureMap.emplace(*it, creatureActorId);
const ESM::Static* fx = world->getStore().get<ESM::Static>().search("VFX_Summon_Start");
if (fx)
anim->addEffect("meshes\\" + fx->mModel, -1, false);
}
}
}
// Update summon effects
for (std::map<ESM::SummonKey, int>::iterator it = creatureMap.begin(); it != creatureMap.end(); )
{
bool found = mActiveEffects.find(it->first) != mActiveEffects.end();
if (!found)
catch (std::exception& e)
{
// Effect has ended
MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(mActor, it->second);
creatureMap.erase(it++);
continue;
Log(Debug::Error) << "Failed to spawn summoned creature: " << e.what();
// still insert into creatureMap so we don't try to spawn again every frame, that would spam the warning log
}
++it;
summoner.getClass().getCreatureStats(summoner).getSummonedCreatureMap().emplace(effectId, creatureActorId);
}
return creatureActorId;
}
void updateSummons(const MWWorld::Ptr& summoner, bool cleanup)
{
MWMechanics::CreatureStats& creatureStats = summoner.getClass().getCreatureStats(summoner);
auto& creatureMap = creatureStats.getSummonedCreatureMap();
std::vector<int> graveyard = creatureStats.getSummonedCreatureGraveyard();
creatureStats.getSummonedCreatureGraveyard().clear();
for (const int creature : graveyard)
MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(mActor, creature);
MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(summoner, creature);
if (!cleanup)
return;
for (std::map<ESM::SummonKey, int>::iterator it = creatureMap.begin(); it != creatureMap.end(); )
for (auto it = creatureMap.begin(); it != creatureMap.end(); )
{
if(it->second == -1)
{
@ -155,7 +125,7 @@ namespace MWMechanics
if (!ptr.isEmpty() && ptr.getClass().getCreatureStats(ptr).isDead() && ptr.getClass().getCreatureStats(ptr).isDeathAnimationFinished())
{
// Purge the magic effect so a new creature can be summoned if desired
purgeSummonEffect(mActor, *it);
purgeSummonEffect(summoner, *it);
creatureMap.erase(it++);
}
else
@ -163,13 +133,13 @@ namespace MWMechanics
}
}
void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair<const ESM::SummonKey, int>& summon)
void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair<int, int>& summon)
{
auto& creatureStats = summoner.getClass().getCreatureStats(summoner);
creatureStats.getActiveSpells().purgeEffect(summon.first.mEffectId, summon.first.mSourceId, summon.first.mEffectIndex);
creatureStats.getSpells().purgeEffect(summon.first.mEffectId, summon.first.mSourceId);
if (summoner.getClass().hasInventoryStore(summoner))
summoner.getClass().getInventoryStore(summoner).purgeEffect(summon.first.mEffectId, summon.first.mSourceId, false, summon.first.mEffectIndex);
creatureStats.getActiveSpells().purge([summon] (const auto& spell, const auto& effect)
{
return effect.mEffectId == summon.first && effect.mArg == summon.second;
}, summoner);
MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(summoner, summon.second);
}

@ -11,32 +11,15 @@
namespace MWMechanics
{
class CreatureStats;
bool isSummoningEffect(int effectId);
std::string getSummonedCreature(int effectId);
void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair<const ESM::SummonKey, int>& summon);
struct UpdateSummonedCreatures : public EffectSourceVisitor
{
UpdateSummonedCreatures(const MWWorld::Ptr& actor);
virtual ~UpdateSummonedCreatures() = default;
void visit (MWMechanics::EffectKey key, int effectIndex,
const std::string& sourceName, const std::string& sourceId, int casterActorId,
float magnitude, float remainingTime = -1, float totalTime = -1) override;
/// To call after all effect sources have been visited
void process(bool cleanup);
private:
MWWorld::Ptr mActor;
void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair<int, int>& summon);
std::set<ESM::SummonKey> mActiveEffects;
};
int summonCreature(int effectId, const MWWorld::Ptr& summoner);
void updateSummons(const MWWorld::Ptr& summoner, bool cleanup);
}
#endif

@ -1,216 +0,0 @@
#include "tickableeffects.hpp"
#include <components/settings/settings.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwbase/world.hpp"
#include "../mwworld/cellstore.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/containerstore.hpp"
#include "../mwworld/esmstore.hpp"
#include "../mwworld/inventorystore.hpp"
#include "actorutil.hpp"
#include "npcstats.hpp"
namespace MWMechanics
{
void adjustDynamicStat(CreatureStats& creatureStats, int index, float magnitude, bool allowDecreaseBelowZero = false)
{
DynamicStat<float> stat = creatureStats.getDynamic(index);
stat.setCurrent(stat.getCurrent() + magnitude, allowDecreaseBelowZero);
creatureStats.setDynamic(index, stat);
}
bool disintegrateSlot (const MWWorld::Ptr& ptr, int slot, float disintegrate)
{
if (!ptr.getClass().hasInventoryStore(ptr))
return false;
MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr);
MWWorld::ContainerStoreIterator item = inv.getSlot(slot);
if (item != inv.end() && (item.getType() == MWWorld::ContainerStore::Type_Armor || item.getType() == MWWorld::ContainerStore::Type_Weapon))
{
if (!item->getClass().hasItemHealth(*item))
return false;
int charge = item->getClass().getItemHealth(*item);
if (charge == 0)
return false;
// Store remainder of disintegrate amount (automatically subtracted if > 1)
item->getCellRef().applyChargeRemainderToBeSubtracted(disintegrate - std::floor(disintegrate));
charge = item->getClass().getItemHealth(*item);
charge -= std::min(static_cast<int>(disintegrate), charge);
item->getCellRef().setCharge(charge);
if (charge == 0)
{
// Will unequip the broken item and try to find a replacement
if (ptr != getPlayer())
inv.autoEquip(ptr);
else
inv.unequipItem(*item, ptr);
}
return true;
}
return false;
}
bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey &effectKey, float magnitude)
{
if (magnitude == 0.f)
return false;
bool receivedMagicDamage = false;
bool godmode = actor == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState();
switch (effectKey.mId)
{
case ESM::MagicEffect::DamageAttribute:
{
if (godmode)
break;
AttributeValue attr = creatureStats.getAttribute(effectKey.mArg);
attr.damage(magnitude);
creatureStats.setAttribute(effectKey.mArg, attr);
break;
}
case ESM::MagicEffect::RestoreAttribute:
{
AttributeValue attr = creatureStats.getAttribute(effectKey.mArg);
attr.restore(magnitude);
creatureStats.setAttribute(effectKey.mArg, attr);
break;
}
case ESM::MagicEffect::RestoreHealth:
case ESM::MagicEffect::RestoreMagicka:
case ESM::MagicEffect::RestoreFatigue:
adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::RestoreHealth, magnitude);
break;
case ESM::MagicEffect::DamageHealth:
if (godmode)
break;
receivedMagicDamage = true;
adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::DamageHealth, -magnitude);
break;
case ESM::MagicEffect::DamageMagicka:
case ESM::MagicEffect::DamageFatigue:
{
if (godmode)
break;
int index = effectKey.mId-ESM::MagicEffect::DamageHealth;
static const bool uncappedDamageFatigue = Settings::Manager::getBool("uncapped damage fatigue", "Game");
adjustDynamicStat(creatureStats, index, -magnitude, index == 2 && uncappedDamageFatigue);
break;
}
case ESM::MagicEffect::AbsorbHealth:
if (!godmode || magnitude <= 0)
{
if (magnitude > 0.f)
receivedMagicDamage = true;
adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude);
}
break;
case ESM::MagicEffect::AbsorbMagicka:
case ESM::MagicEffect::AbsorbFatigue:
if (!godmode || magnitude <= 0)
adjustDynamicStat(creatureStats, effectKey.mId-ESM::MagicEffect::AbsorbHealth, -magnitude);
break;
case ESM::MagicEffect::DisintegrateArmor:
{
if (godmode)
break;
static const std::array<int, 9> priorities
{
MWWorld::InventoryStore::Slot_CarriedLeft,
MWWorld::InventoryStore::Slot_Cuirass,
MWWorld::InventoryStore::Slot_LeftPauldron,
MWWorld::InventoryStore::Slot_RightPauldron,
MWWorld::InventoryStore::Slot_LeftGauntlet,
MWWorld::InventoryStore::Slot_RightGauntlet,
MWWorld::InventoryStore::Slot_Helmet,
MWWorld::InventoryStore::Slot_Greaves,
MWWorld::InventoryStore::Slot_Boots
};
for (const int priority : priorities)
{
if (disintegrateSlot(actor, priority, magnitude))
break;
}
break;
}
case ESM::MagicEffect::DisintegrateWeapon:
if (!godmode)
disintegrateSlot(actor, MWWorld::InventoryStore::Slot_CarriedRight, magnitude);
break;
case ESM::MagicEffect::SunDamage:
{
// isInCell shouldn't be needed, but updateActor called during game start
if (!actor.isInCell() || !actor.getCell()->isExterior() || godmode)
break;
float time = MWBase::Environment::get().getWorld()->getTimeStamp().getHour();
float timeDiff = std::min(7.f, std::max(0.f, std::abs(time - 13)));
float damageScale = 1.f - timeDiff / 7.f;
// When cloudy, the sun damage effect is halved
static float fMagicSunBlockedMult = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find(
"fMagicSunBlockedMult")->mValue.getFloat();
int weather = MWBase::Environment::get().getWorld()->getCurrentWeather();
if (weather > 1)
damageScale *= fMagicSunBlockedMult;
adjustDynamicStat(creatureStats, 0, -magnitude * damageScale);
if (magnitude * damageScale > 0.f)
receivedMagicDamage = true;
break;
}
case ESM::MagicEffect::FireDamage:
case ESM::MagicEffect::ShockDamage:
case ESM::MagicEffect::FrostDamage:
case ESM::MagicEffect::Poison:
{
if (godmode)
break;
adjustDynamicStat(creatureStats, 0, -magnitude);
receivedMagicDamage = true;
break;
}
case ESM::MagicEffect::DamageSkill:
case ESM::MagicEffect::RestoreSkill:
{
if (!actor.getClass().isNpc())
break;
if (godmode && effectKey.mId == ESM::MagicEffect::DamageSkill)
break;
NpcStats &npcStats = actor.getClass().getNpcStats(actor);
SkillValue& skill = npcStats.getSkill(effectKey.mArg);
if (effectKey.mId == ESM::MagicEffect::RestoreSkill)
skill.restore(magnitude);
else
skill.damage(magnitude);
break;
}
default:
return false;
}
if (receivedMagicDamage && actor == getPlayer())
MWBase::Environment::get().getWindowManager()->activateHitOverlay(false);
return true;
}
}

@ -1,20 +0,0 @@
#ifndef MWMECHANICS_TICKABLEEFFECTS_H
#define MWMECHANICS_TICKABLEEFFECTS_H
namespace MWWorld
{
class Ptr;
}
namespace MWMechanics
{
class CreatureStats;
struct EffectKey;
/// Apply a magic effect that is applied in tick intervals until its remaining time ends or it is removed
/// Note: this function works in loop, so magic effects should not be removed here to avoid iterator invalidation.
/// @return Was the effect a tickable effect with a magnitude?
bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey& effectKey, float magnitude);
}
#endif

@ -1101,34 +1101,6 @@ Resource::ResourceSystem* NpcAnimation::getResourceSystem()
return mResourceSystem;
}
void NpcAnimation::permanentEffectAdded(const ESM::MagicEffect *magicEffect, bool isNew)
{
// During first auto equip, we don't play any sounds.
// Basically we don't want sounds when the actor is first loaded,
// the items should appear as if they'd always been equipped.
if (isNew)
{
static const std::string schools[] = {
"alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration"
};
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
if(!magicEffect->mHitSound.empty())
sndMgr->playSound3D(mPtr, magicEffect->mHitSound, 1.0f, 1.0f);
else
sndMgr->playSound3D(mPtr, schools[magicEffect->mData.mSchool]+" hit", 1.0f, 1.0f);
}
if (!magicEffect->mHit.empty())
{
const ESM::Static* castStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find (magicEffect->mHit);
bool loop = (magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0;
// Don't play particle VFX unless the effect is new or it should be looping.
if (isNew || loop)
addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, loop, "", magicEffect->mParticle);
}
}
void NpcAnimation::enableHeadAnimation(bool enable)
{
mHeadAnimationTime->setEnabled(enable);

@ -31,7 +31,6 @@ class NpcAnimation : public ActorAnimation, public WeaponAnimation, public MWWor
{
public:
void equipmentChanged() override;
void permanentEffectAdded(const ESM::MagicEffect *magicEffect, bool isNew) override;
public:
typedef std::map<ESM::PartReferenceType,std::string> PartBoneMap;

@ -563,13 +563,7 @@ namespace MWScript
const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr);
MWMechanics::MagicEffects effects = stats.getSpells().getMagicEffects();
effects += stats.getActiveSpells().getMagicEffects();
if (ptr.getClass().hasInventoryStore(ptr) && !stats.isDeathAnimationFinished())
{
MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr);
effects += store.getMagicEffects();
}
const MWMechanics::MagicEffects& effects = stats.getMagicEffects();
for (const auto& activeEffect : effects)
{
@ -821,7 +815,7 @@ namespace MWScript
}
const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr);
runtime.push(stats.getActiveSpells().isSpellActive(id) || stats.getSpells().isSpellActive(id));
runtime.push(stats.getActiveSpells().isSpellActive(id));
}
};

@ -473,6 +473,8 @@ namespace MWScript
ESM::Spell::SpellType type = static_cast<ESM::Spell::SpellType>(spell->mData.mType);
if (type != ESM::Spell::ST_Spell && type != ESM::Spell::ST_Power)
{
// Add spell effect to *this actor's* queue immediately
creatureStats.getActiveSpells().addSpell(spell, ptr);
// Apply looping particles immediately for constant effects
MWBase::Environment::get().getWorld()->applyLoopingParticles(ptr);
}
@ -492,17 +494,6 @@ namespace MWScript
runtime.pop();
MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr);
// The spell may have an instant effect which must be handled before the spell's removal.
for (const auto& effect : creatureStats.getSpells().getMagicEffects())
{
if (effect.second.getMagnitude() <= 0)
continue;
MWMechanics::CastSpell cast(ptr, ptr);
if (cast.applyInstantEffect(ptr, ptr, effect.first, effect.second.getMagnitude()))
creatureStats.getSpells().purgeEffect(effect.first.mId);
}
MWBase::Environment::get().getMechanicsManager()->restoreStatsAfterCorprus(ptr, id);
creatureStats.getSpells().remove (id);
MWBase::WindowManager *wm = MWBase::Environment::get().getWindowManager();
@ -527,8 +518,7 @@ namespace MWScript
std::string spellid = runtime.getStringLiteral (runtime[0].mInteger);
runtime.pop();
ptr.getClass().getCreatureStats (ptr).getActiveSpells().removeEffects(spellid);
ptr.getClass().getCreatureStats (ptr).getSpells().removeEffects(spellid);
ptr.getClass().getCreatureStats (ptr).getActiveSpells().removeEffects(ptr, spellid);
}
};
@ -544,7 +534,7 @@ namespace MWScript
Interpreter::Type_Integer effectId = runtime[0].mInteger;
runtime.pop();
ptr.getClass().getCreatureStats (ptr).getActiveSpells().purgeEffect(effectId);
ptr.getClass().getCreatureStats (ptr).getActiveSpells().purgeEffect(ptr, effectId);
}
};

@ -26,6 +26,7 @@
#include "../mwmechanics/recharge.hpp"
#include "ptr.hpp"
#include "esmloader.hpp"
#include "esmstore.hpp"
#include "class.hpp"
#include "containerstore.hpp"
@ -176,6 +177,13 @@ namespace
if (state.mVersion < 15)
fixRestocking(record, state);
if (state.mVersion < 17)
{
if constexpr (std::is_same_v<T, ESM::Creature>)
MWWorld::convertMagicEffects(state.mCreatureStats, state.mInventory);
else if constexpr (std::is_same_v<T, ESM::NPC>)
MWWorld::convertMagicEffects(state.mCreatureStats, state.mInventory, &state.mNpcStats);
}
if (state.mRef.mRefNum.hasContentFile())
{

@ -2,6 +2,26 @@
#include "esmstore.hpp"
#include <components/esm/esmreader.hpp>
#include <components/esm/npcstate.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
#include "../mwmechanics/magiceffects.hpp"
namespace
{
template<class T>
void getEnchantedItem(const std::string& id, std::string& enchantment, std::string& itemName)
{
const T* item = MWBase::Environment::get().getWorld()->getStore().get<T>().search(id);
if(item)
{
enchantment = item->mEnchant;
itemName = item->mName;
}
}
}
namespace MWWorld
{
@ -28,4 +48,187 @@ void EsmLoader::load(const boost::filesystem::path& filepath, int& index)
mStore.load(mEsm[index], &mListener);
}
void convertMagicEffects(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory, ESM::NpcStats* npcStats)
{
const auto& store = MWBase::Environment::get().getWorld()->getStore();
// Convert corprus to format 10
for (const auto& [id, oldStats] : creatureStats.mSpells.mCorprusSpells)
{
const ESM::Spell* spell = store.get<ESM::Spell>().search(id);
if (!spell)
continue;
ESM::CreatureStats::CorprusStats stats;
stats.mNextWorsening = oldStats.mNextWorsening;
for (int i=0; i<ESM::Attribute::Length; ++i)
stats.mWorsenings[i] = 0;
for (auto& effect : spell->mEffects.mList)
{
if (effect.mEffectID == ESM::MagicEffect::DrainAttribute)
stats.mWorsenings[effect.mAttribute] = oldStats.mWorsenings;
}
creatureStats.mCorprusSpells[id] = stats;
}
// Convert to format 17
for(const auto& [id, oldParams] : creatureStats.mSpells.mSpellParams)
{
const ESM::Spell* spell = store.get<ESM::Spell>().search(id);
if (!spell || spell->mData.mType == ESM::Spell::ST_Spell || spell->mData.mType == ESM::Spell::ST_Power)
continue;
ESM::ActiveSpells::ActiveSpellParams params;
params.mId = id;
params.mDisplayName = spell->mName;
params.mItem.unset();
params.mCasterActorId = creatureStats.mActorId;
if(spell->mData.mType == ESM::Spell::ST_Ability)
params.mType = ESM::ActiveSpells::Type_Ability;
else
params.mType = ESM::ActiveSpells::Type_Permanent;
params.mWorsenings = -1;
int effectIndex = 0;
for(const auto& enam : spell->mEffects.mList)
{
if(oldParams.mPurgedEffects.find(effectIndex) == oldParams.mPurgedEffects.end())
{
ESM::ActiveEffect effect;
effect.mEffectId = enam.mEffectID;
effect.mArg = MWMechanics::EffectKey(enam).mArg;
effect.mDuration = -1;
effect.mTimeLeft = -1;
effect.mEffectIndex = effectIndex;
auto rand = oldParams.mEffectRands.find(effectIndex);
if(rand != oldParams.mEffectRands.end())
{
float magnitude = (enam.mMagnMax - enam.mMagnMin) * rand->second + enam.mMagnMin;
effect.mMagnitude = magnitude;
effect.mMinMagnitude = magnitude;
effect.mMaxMagnitude = magnitude;
// Prevent recalculation of resistances
effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances;
}
else
{
effect.mMagnitude = 0.f;
effect.mMinMagnitude = enam.mMagnMin;
effect.mMaxMagnitude = enam.mMagnMax;
effect.mFlags = ESM::ActiveEffect::Flag_None;
}
params.mEffects.emplace_back(effect);
}
effectIndex++;
}
creatureStats.mActiveSpells.mSpells.emplace_back(params);
}
std::multimap<std::string, int> equippedItems;
for(std::size_t i = 0; i < inventory.mItems.size(); ++i)
{
const ESM::ObjectState& item = inventory.mItems[i];
auto slot = inventory.mEquipmentSlots.find(i);
if(slot != inventory.mEquipmentSlots.end())
equippedItems.emplace(item.mRef.mRefID, slot->second);
}
for(const auto& [id, oldMagnitudes] : inventory.mPermanentMagicEffectMagnitudes)
{
std::string eId;
std::string name;
switch(store.find(id))
{
case ESM::REC_ARMO:
getEnchantedItem<ESM::Armor>(id, eId, name);
break;
case ESM::REC_CLOT:
getEnchantedItem<ESM::Clothing>(id, eId, name);
break;
case ESM::REC_WEAP:
getEnchantedItem<ESM::Weapon>(id, eId, name);
break;
}
if(eId.empty())
continue;
const ESM::Enchantment* enchantment = store.get<ESM::Enchantment>().search(eId);
if(!enchantment)
continue;
ESM::ActiveSpells::ActiveSpellParams params;
params.mId = id;
params.mDisplayName = name;
params.mCasterActorId = creatureStats.mActorId;
params.mType = ESM::ActiveSpells::Type_Enchantment;
params.mWorsenings = -1;
for(std::size_t effectIndex = 0; effectIndex < oldMagnitudes.size() && effectIndex < enchantment->mEffects.mList.size(); ++effectIndex)
{
const auto& enam = enchantment->mEffects.mList[effectIndex];
auto [random, multiplier] = oldMagnitudes[effectIndex];
float magnitude = (enam.mMagnMax - enam.mMagnMin) * random + enam.mMagnMin;
magnitude *= multiplier;
if(magnitude <= 0)
continue;
ESM::ActiveEffect effect;
effect.mEffectId = enam.mEffectID;
effect.mMagnitude = magnitude;
effect.mMinMagnitude = magnitude;
effect.mMaxMagnitude = magnitude;
effect.mArg = MWMechanics::EffectKey(enam).mArg;
effect.mDuration = -1;
effect.mTimeLeft = -1;
effect.mEffectIndex = static_cast<int>(effectIndex);
// Prevent recalculation of resistances
effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances;
params.mEffects.emplace_back(effect);
}
auto [begin, end] = equippedItems.equal_range(id);
for(auto it = begin; it != end; ++it)
{
params.mItem = { static_cast<unsigned int>(it->second), 0 };
creatureStats.mActiveSpells.mSpells.emplace_back(params);
}
}
for(const auto& spell : creatureStats.mCorprusSpells)
{
auto it = std::find_if(creatureStats.mActiveSpells.mSpells.begin(), creatureStats.mActiveSpells.mSpells.end(), [&] (const auto& params) { return params.mId == spell.first; });
if(it != creatureStats.mActiveSpells.mSpells.end())
{
it->mNextWorsening = spell.second.mNextWorsening;
int worsenings = 0;
for(int i = 0; i < ESM::Attribute::Length; ++i)
worsenings = std::max(spell.second.mWorsenings[i], worsenings);
it->mWorsenings = worsenings;
}
}
for(const auto& [key, actorId] : creatureStats.mSummonedCreatureMap)
{
if(actorId == -1)
continue;
for(auto& params : creatureStats.mActiveSpells.mSpells)
{
if(params.mId == key.mSourceId)
{
bool found = false;
for(auto& effect : params.mEffects)
{
if(effect.mEffectId == key.mEffectId && effect.mEffectIndex == key.mEffectIndex)
{
effect.mArg = actorId;
found = true;
break;
}
}
if(found)
break;
}
}
}
// Reset modifiers that were previously recalculated each frame
for(std::size_t i = 0; i < ESM::Attribute::Length; ++i)
creatureStats.mAttributes[i].mMod = 0.f;
for(std::size_t i = 0; i < 3; ++i)
creatureStats.mDynamic[i].mMod = 0.f;
for(std::size_t i = 0; i < 4; ++i)
creatureStats.mAiSettings[i].mMod = 0.f;
if(npcStats)
{
for(std::size_t i = 0; i < ESM::Skill::Length; ++i)
npcStats->mSkills[i].mMod = 0.f;
}
}
} /* namespace MWWorld */

@ -13,6 +13,9 @@ namespace ToUTF8
namespace ESM
{
class ESMReader;
struct CreatureStats;
struct InventoryState;
struct NpcStats;
}
namespace MWWorld
@ -33,6 +36,8 @@ struct EsmLoader : public ContentLoader
ToUTF8::Utf8Encoder* mEncoder;
};
void convertMagicEffects(ESM::CreatureStats& creatureStats, ESM::InventoryState& inventory, ESM::NpcStats* npcStats = nullptr);
} /* namespace MWWorld */
#endif // ESMLOADER_HPP

@ -108,11 +108,9 @@ MWWorld::InventoryStore::InventoryStore()
MWWorld::InventoryStore::InventoryStore (const InventoryStore& store)
: ContainerStore (store)
, mMagicEffects(store.mMagicEffects)
, mInventoryListener(store.mInventoryListener)
, mUpdatesEnabled(store.mUpdatesEnabled)
, mFirstAutoEquip(store.mFirstAutoEquip)
, mPermanentMagicEffectMagnitudes(store.mPermanentMagicEffectMagnitudes)
, mSelectedEnchantItem(end())
{
copySlots (store);
@ -125,9 +123,7 @@ MWWorld::InventoryStore& MWWorld::InventoryStore::operator= (const InventoryStor
mListener = store.mListener;
mInventoryListener = store.mInventoryListener;
mMagicEffects = store.mMagicEffects;
mFirstAutoEquip = store.mFirstAutoEquip;
mPermanentMagicEffectMagnitudes = store.mPermanentMagicEffectMagnitudes;
mRechargingItemsUpToDate = false;
ContainerStore::operator= (store);
mSlots.clear();
@ -186,8 +182,6 @@ void MWWorld::InventoryStore::equip (int slot, const ContainerStoreIterator& ite
flagAsModified();
fireEquipmentChangedEvent(actor);
updateMagicEffects(actor);
}
void MWWorld::InventoryStore::unequipAll(const MWWorld::Ptr& actor)
@ -199,7 +193,6 @@ void MWWorld::InventoryStore::unequipAll(const MWWorld::Ptr& actor)
mUpdatesEnabled = true;
fireEquipmentChangedEvent(actor);
updateMagicEffects(actor);
}
MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getSlot (int slot)
@ -554,7 +547,6 @@ void MWWorld::InventoryStore::autoEquip (const MWWorld::Ptr& actor)
{
mSlots.swap (slots_);
fireEquipmentChangedEvent(actor);
updateMagicEffects(actor);
flagAsModified();
}
}
@ -567,129 +559,6 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getPreferredShield(cons
return slots[Slot_CarriedLeft];
}
const MWMechanics::MagicEffects& MWWorld::InventoryStore::getMagicEffects() const
{
return mMagicEffects;
}
void MWWorld::InventoryStore::updateMagicEffects(const Ptr& actor)
{
// To avoid excessive updates during auto-equip
if (!mUpdatesEnabled)
return;
// Delay update until the listener is set up
if (!mInventoryListener)
return;
mMagicEffects = MWMechanics::MagicEffects();
const auto& stats = actor.getClass().getCreatureStats(actor);
if (stats.isDead() && stats.isDeathAnimationFinished())
return;
for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter)
{
if (*iter==end())
continue;
std::string enchantmentId = (*iter)->getClass().getEnchantment (**iter);
if (!enchantmentId.empty())
{
const ESM::Enchantment& enchantment =
*MWBase::Environment::get().getWorld()->getStore().get<ESM::Enchantment>().find (enchantmentId);
if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect)
continue;
std::vector<EffectParams> params;
bool existed = (mPermanentMagicEffectMagnitudes.find((**iter).getCellRef().getRefId()) != mPermanentMagicEffectMagnitudes.end());
if (!existed)
{
params.resize(enchantment.mEffects.mList.size());
int i=0;
for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList)
{
int delta = effect.mMagnMax - effect.mMagnMin;
// Roll some dice, one for each effect
if (delta)
params[i].mRandom = Misc::Rng::rollDice(delta + 1) / static_cast<float>(delta);
// Try resisting each effect
params[i].mMultiplier = MWMechanics::getEffectMultiplier(effect.mEffectID, actor, actor);
++i;
}
// Note that using the RefID as a key here is not entirely correct.
// Consider equipping the same item twice (e.g. a ring)
// However, permanent enchantments with a random magnitude are kind of an exploit anyway,
// so it doesn't really matter if both items will get the same magnitude. *Extreme* edge case.
mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()] = params;
}
else
params = mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()];
int i=0;
for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList)
{
const ESM::MagicEffect *magicEffect =
MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find (
effect.mEffectID);
// Fully resisted or can't be applied to target?
if (params[i].mMultiplier == 0 || !MWMechanics::checkEffectTarget(effect.mEffectID, actor, actor, actor == MWMechanics::getPlayer()))
{
i++;
continue;
}
float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * params[i].mRandom;
magnitude *= params[i].mMultiplier;
if (!existed)
{
// During first auto equip, we don't play any sounds.
// Basically we don't want sounds when the actor is first loaded,
// the items should appear as if they'd always been equipped.
mInventoryListener->permanentEffectAdded(magicEffect, !mFirstAutoEquip);
}
if (magnitude)
mMagicEffects.add (effect, magnitude);
i++;
}
}
}
// Now drop expired effects
for (TEffectMagnitudes::iterator it = mPermanentMagicEffectMagnitudes.begin();
it != mPermanentMagicEffectMagnitudes.end();)
{
bool found = false;
for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter)
{
if (*iter == end())
continue;
if ((**iter).getCellRef().getRefId() == it->first)
{
found = true;
}
}
if (!found)
mPermanentMagicEffectMagnitudes.erase(it++);
else
++it;
}
// Magic effects are normally not updated when paused, but we need this to make resistances work immediately after equipping
MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(actor);
mFirstAutoEquip = false;
}
bool MWWorld::InventoryStore::stacks(const ConstPtr& ptr1, const ConstPtr& ptr2) const
{
bool canStack = MWWorld::ContainerStore::stacks(ptr1, ptr2);
@ -800,7 +669,6 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipSlot(int slot, c
if (applyUpdates)
{
fireEquipmentChangedEvent(actor);
updateMagicEffects(actor);
}
return retval;
@ -848,7 +716,7 @@ MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipItemQuantity(con
return unstack(item, actor, item.getRefData().getCount() - count);
}
MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener()
MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener() const
{
return mInventoryListener;
}
@ -856,7 +724,6 @@ MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener()
void MWWorld::InventoryStore::setInvListener(InventoryStoreListener *listener, const Ptr& actor)
{
mInventoryListener = listener;
updateMagicEffects(actor);
}
void MWWorld::InventoryStore::fireEquipmentChangedEvent(const Ptr& actor)
@ -875,105 +742,6 @@ void MWWorld::InventoryStore::fireEquipmentChangedEvent(const Ptr& actor)
*/
}
void MWWorld::InventoryStore::visitEffectSources(MWMechanics::EffectSourceVisitor &visitor)
{
for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter)
{
if (*iter==end())
continue;
std::string enchantmentId = (*iter)->getClass().getEnchantment (**iter);
if (enchantmentId.empty())
continue;
const ESM::Enchantment& enchantment =
*MWBase::Environment::get().getWorld()->getStore().get<ESM::Enchantment>().find (enchantmentId);
if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect)
continue;
if (mPermanentMagicEffectMagnitudes.find((**iter).getCellRef().getRefId()) == mPermanentMagicEffectMagnitudes.end())
continue;
int i=0;
for (const ESM::ENAMstruct& effect : enchantment.mEffects.mList)
{
i++;
// Don't get spell icon display information for enchantments that weren't actually applied
if (mMagicEffects.get(MWMechanics::EffectKey(effect)).getMagnitude() == 0)
continue;
const EffectParams& params = mPermanentMagicEffectMagnitudes[(**iter).getCellRef().getRefId()][i-1];
float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * params.mRandom;
magnitude *= params.mMultiplier;
if (magnitude > 0)
visitor.visit(MWMechanics::EffectKey(effect), i-1, (**iter).getClass().getName(**iter), (**iter).getCellRef().getRefId(), -1, magnitude);
}
}
}
void MWWorld::InventoryStore::purgeEffect(short effectId, bool wholeSpell)
{
for (TSlots::const_iterator it = mSlots.begin(); it != mSlots.end(); ++it)
{
if (*it != end())
purgeEffect(effectId, (*it)->getCellRef().getRefId(), wholeSpell);
}
}
void MWWorld::InventoryStore::purgeEffect(short effectId, const std::string &sourceId, bool wholeSpell, int effectIndex)
{
TEffectMagnitudes::iterator effectMagnitudeIt = mPermanentMagicEffectMagnitudes.find(sourceId);
if (effectMagnitudeIt == mPermanentMagicEffectMagnitudes.end())
return;
for (TSlots::const_iterator iter (mSlots.begin()); iter!=mSlots.end(); ++iter)
{
if (*iter==end())
continue;
if ((*iter)->getCellRef().getRefId() != sourceId)
continue;
std::string enchantmentId = (*iter)->getClass().getEnchantment (**iter);
if (!enchantmentId.empty())
{
const ESM::Enchantment& enchantment =
*MWBase::Environment::get().getWorld()->getStore().get<ESM::Enchantment>().find (enchantmentId);
if (enchantment.mData.mType != ESM::Enchantment::ConstantEffect)
continue;
std::vector<EffectParams>& params = effectMagnitudeIt->second;
int i=0;
for (std::vector<ESM::ENAMstruct>::const_iterator effectIt (enchantment.mEffects.mList.begin());
effectIt!=enchantment.mEffects.mList.end(); ++effectIt, ++i)
{
if (effectIt->mEffectID != effectId)
continue;
if (effectIndex >= 0 && effectIndex != i)
continue;
if (wholeSpell)
{
mPermanentMagicEffectMagnitudes.erase(sourceId);
return;
}
float magnitude = effectIt->mMagnMin + (effectIt->mMagnMax - effectIt->mMagnMin) * params[i].mRandom;
magnitude *= params[i].mMultiplier;
if (magnitude)
mMagicEffects.add (*effectIt, -magnitude);
params[i].mMultiplier = 0;
}
}
}
}
void MWWorld::InventoryStore::clear()
{
mSlots.clear();
@ -991,38 +759,9 @@ bool MWWorld::InventoryStore::isEquipped(const MWWorld::ConstPtr &item)
return false;
}
void MWWorld::InventoryStore::writeState(ESM::InventoryState &state) const
{
MWWorld::ContainerStore::writeState(state);
for (TEffectMagnitudes::const_iterator it = mPermanentMagicEffectMagnitudes.begin(); it != mPermanentMagicEffectMagnitudes.end(); ++it)
{
std::vector<std::pair<float, float> > params;
for (std::vector<EffectParams>::const_iterator pIt = it->second.begin(); pIt != it->second.end(); ++pIt)
{
params.emplace_back(pIt->mRandom, pIt->mMultiplier);
}
state.mPermanentMagicEffectMagnitudes[it->first] = params;
}
}
void MWWorld::InventoryStore::readState(const ESM::InventoryState &state)
bool MWWorld::InventoryStore::isFirstEquip()
{
MWWorld::ContainerStore::readState(state);
for (ESM::InventoryState::TEffectMagnitudes::const_iterator it = state.mPermanentMagicEffectMagnitudes.begin();
it != state.mPermanentMagicEffectMagnitudes.end(); ++it)
{
std::vector<EffectParams> params;
for (std::vector<std::pair<float, float> >::const_iterator pIt = it->second.begin(); pIt != it->second.end(); ++pIt)
{
EffectParams p;
p.mRandom = pIt->first;
p.mMultiplier = pIt->second;
params.push_back(p);
}
mPermanentMagicEffectMagnitudes[it->first] = params;
}
bool first = mFirstAutoEquip;
mFirstAutoEquip = false;
return first;
}

@ -25,15 +25,6 @@ namespace MWWorld
*/
virtual void equipmentChanged () {}
/**
* @param effect
* @param isNew Is this effect new (e.g. the item for it was just now manually equipped)
* or was it loaded from a savegame / initial game state? \n
* If it isn't new, non-looping VFX should not be played.
* @param playSound Play effect sound?
*/
virtual void permanentEffectAdded (const ESM::MagicEffect *magicEffect, bool isNew) {}
virtual ~InventoryStoreListener() = default;
};
@ -68,8 +59,6 @@ namespace MWWorld
private:
MWMechanics::MagicEffects mMagicEffects;
InventoryStoreListener* mInventoryListener;
// Enables updates of magic effects and actor model whenever items are equipped or unequipped.
@ -78,19 +67,6 @@ namespace MWWorld
bool mFirstAutoEquip;
// Vanilla allows permanent effects with a random magnitude, so it needs to be stored here.
// We also need this to only play sounds and particle effects when the item is equipped, rather than on every update.
struct EffectParams
{
// Modifier to scale between min and max magnitude
float mRandom;
// Multiplier for when an effect was fully or partially resisted
float mMultiplier;
};
typedef std::map<std::string, std::vector<EffectParams> > TEffectMagnitudes;
TEffectMagnitudes mPermanentMagicEffectMagnitudes;
typedef std::vector<ContainerStoreIterator> TSlots;
TSlots mSlots;
@ -106,8 +82,6 @@ namespace MWWorld
void initSlots (TSlots& slots_);
void updateMagicEffects(const Ptr& actor);
void fireEquipmentChangedEvent(const Ptr& actor);
void storeEquipmentState (const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const override;
@ -161,9 +135,6 @@ namespace MWWorld
void autoEquip (const MWWorld::Ptr& actor);
///< Auto equip items according to stats and item value.
const MWMechanics::MagicEffects& getMagicEffects() const;
///< Return magic effects from worn items.
bool stacks (const ConstPtr& ptr1, const ConstPtr& ptr2) const override;
///< @return true if the two specified objects can stack with each other
@ -198,22 +169,12 @@ namespace MWWorld
void setInvListener (InventoryStoreListener* listener, const Ptr& actor);
///< Set a listener for various events, see \a InventoryStoreListener
InventoryStoreListener* getInvListener();
void visitEffectSources (MWMechanics::EffectSourceVisitor& visitor);
void purgeEffect (short effectId, bool wholeSpell = false);
///< Remove a magic effect
void purgeEffect (short effectId, const std::string& sourceId, bool wholeSpell = false, int effectIndex=-1);
///< Remove a magic effect
InventoryStoreListener* getInvListener() const;
void clear() override;
///< Empty container.
void writeState (ESM::InventoryState& state) const override;
void readState (const ESM::InventoryState& state) override;
bool isFirstEquip();
};
}

@ -22,9 +22,10 @@
#include "../mwmechanics/npcstats.hpp"
#include "../mwmechanics/spellutil.hpp"
#include "cellstore.hpp"
#include "class.hpp"
#include "esmloader.hpp"
#include "ptr.hpp"
#include "cellstore.hpp"
namespace MWWorld
{
@ -381,6 +382,14 @@ namespace MWWorld
// this is the one object we can not silently drop.
throw std::runtime_error ("invalid player state record (object state)");
}
if (reader.getFormat() < 17)
{
convertMagicEffects(player.mObject.mCreatureStats, player.mObject.mInventory, &player.mObject.mNpcStats);
for(std::size_t i = 0; i < ESM::Attribute::Length; ++i)
player.mSaveAttributes[i].mMod = 0.f;
for(std::size_t i = 0; i < ESM::Skill::Length; ++i)
player.mSaveSkills[i].mMod = 0.f;
}
if (!player.mObject.mEnabled)
{

@ -256,7 +256,7 @@ namespace MWWorld
state.mEffectAnimationTime->addTime(duration);
}
void ProjectileManager::launchMagicBolt(const std::string &spellId, const Ptr &caster, const osg::Vec3f& fallbackDirection)
void ProjectileManager::launchMagicBolt(const std::string &spellId, const Ptr &caster, const osg::Vec3f& fallbackDirection, int slot)
{
osg::Vec3f pos = caster.getRefData().getPosition().asVec3();
if (caster.getClass().isActor())
@ -278,6 +278,7 @@ namespace MWWorld
MagicBoltState state;
state.mSpellId = spellId;
state.mCasterHandle = caster;
state.mSlot = slot;
if (caster.getClass().isActor())
state.mActorId = caster.getClass().getCreatureStats(caster).getActorId();
else
@ -545,10 +546,10 @@ namespace MWWorld
cast.mHitPosition = pos;
cast.mId = magicBoltState.mSpellId;
cast.mSourceName = magicBoltState.mSourceName;
cast.mStack = false;
cast.mSlot = magicBoltState.mSlot;
cast.inflict(target, caster, magicBoltState.mEffects, ESM::RT_Target, false, true);
MWBase::Environment::get().getWorld()->explodeSpell(pos, magicBoltState.mEffects, caster, target, ESM::RT_Target, magicBoltState.mSpellId, magicBoltState.mSourceName);
MWBase::Environment::get().getWorld()->explodeSpell(pos, magicBoltState.mEffects, caster, target, ESM::RT_Target, magicBoltState.mSpellId, magicBoltState.mSourceName, false, magicBoltState.mSlot);
magicBoltState.mToDelete = true;
}
@ -628,7 +629,7 @@ namespace MWWorld
state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition()));
state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude()));
state.mActorId = it->mActorId;
state.mSlot = it->mSlot;
state.mSpellId = it->mSpellId;
state.mSpeed = it->mSpeed;
@ -684,6 +685,7 @@ namespace MWWorld
state.mSpellId = esm.mSpellId;
state.mActorId = esm.mActorId;
state.mToDelete = false;
state.mSlot = esm.mSlot;
std::string texture;
try

@ -49,7 +49,7 @@ namespace MWWorld
MWRender::RenderingManager* rendering, MWPhysics::PhysicsSystem* physics);
/// If caster is an actor, the actor's facing orientation is used. Otherwise fallbackDirection is used.
void launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection);
void launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, int slot);
void launchProjectile (const MWWorld::Ptr& actor, const MWWorld::ConstPtr& projectile,
const osg::Vec3f& pos, const osg::Quat& orient, const MWWorld::Ptr& bow, float speed, float attackStrength);
@ -107,6 +107,7 @@ namespace MWWorld
ESM::EffectList mEffects;
float mSpeed;
int mSlot;
std::vector<MWBase::Sound*> mSounds;
std::set<std::string> mSoundIds;

@ -1862,7 +1862,7 @@ namespace MWWorld
mRendering->getCamera()->setSneakOffset(0.f);
int blind = 0;
auto& magicEffects = player.getClass().getCreatureStats(player).getMagicEffects();
const auto& magicEffects = player.getClass().getCreatureStats(player).getMagicEffects();
if (!mGodMode)
blind = static_cast<int>(magicEffects.get(ESM::MagicEffect::Blind).getMagnitude());
MWBase::Environment::get().getWindowManager()->setBlindness(std::max(0, std::min(100, blind)));
@ -3109,7 +3109,20 @@ namespace MWWorld
{
MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor);
if (inv.getSelectedEnchantItem() != inv.end())
cast.cast(*inv.getSelectedEnchantItem());
{
const auto& itemPtr = *inv.getSelectedEnchantItem();
auto [slots, _] = itemPtr.getClass().getEquipmentSlots(itemPtr);
int slot = 0;
for(std::size_t i = 0; i < slots.size(); ++i)
{
if(inv.getSlot(slots[i]) == inv.getSelectedEnchantItem())
{
slot = slots[i];
break;
}
}
cast.cast(itemPtr, slot);
}
}
}
@ -3144,9 +3157,9 @@ namespace MWWorld
mProjectileManager->launchProjectile(actor, projectile, worldPos, orient, bow, speed, attackStrength);
}
void World::launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection)
void World::launchMagicBolt (const std::string &spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, int slot)
{
mProjectileManager->launchMagicBolt(spellId, caster, fallbackDirection);
mProjectileManager->launchMagicBolt(spellId, caster, fallbackDirection, slot);
}
void World::updateProjectilesCasters()
@ -3154,46 +3167,24 @@ namespace MWWorld
mProjectileManager->updateCasters();
}
class ApplyLoopingParticlesVisitor : public MWMechanics::EffectSourceVisitor
{
private:
MWWorld::Ptr mActor;
public:
ApplyLoopingParticlesVisitor(const MWWorld::Ptr& actor)
: mActor(actor)
{
}
void visit (MWMechanics::EffectKey key, int /*effectIndex*/,
const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/,
float /*magnitude*/, float /*remainingTime*/ = -1, float /*totalTime*/ = -1) override
{
const ESMStore& store = MWBase::Environment::get().getWorld()->getStore();
const auto magicEffect = store.get<ESM::MagicEffect>().find(key.mId);
if ((magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx) == 0)
return;
const ESM::Static* castStatic;
if (!magicEffect->mHit.empty())
castStatic = store.get<ESM::Static>().find (magicEffect->mHit);
else
castStatic = store.get<ESM::Static>().find ("VFX_DefaultHit");
MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mActor);
if (anim && !castStatic->mModel.empty())
anim->addEffect("meshes\\" + castStatic->mModel, magicEffect->mIndex, /*loop*/true, "", magicEffect->mParticle);
}
};
void World::applyLoopingParticles(const MWWorld::Ptr& ptr)
{
const MWWorld::Class &cls = ptr.getClass();
if (cls.isActor())
{
ApplyLoopingParticlesVisitor visitor(ptr);
cls.getCreatureStats(ptr).getActiveSpells().visitEffectSources(visitor);
cls.getCreatureStats(ptr).getSpells().visitEffectSources(visitor);
if (cls.hasInventoryStore(ptr))
cls.getInventoryStore(ptr).visitEffectSources(visitor);
std::set<int> playing;
for(const auto& params : cls.getCreatureStats(ptr).getActiveSpells())
{
for(const auto& effect : params.getEffects())
{
if(playing.insert(effect.mEffectId).second)
{
const auto magicEffect = getStore().get<ESM::MagicEffect>().find(effect.mEffectId);
if(magicEffect->mData.mFlags & ESM::MagicEffect::ContinuousVfx)
MWMechanics::playEffects(ptr, *magicEffect, false);
}
}
}
}
}
@ -3204,10 +3195,7 @@ namespace MWWorld
void World::breakInvisibility(const Ptr &actor)
{
actor.getClass().getCreatureStats(actor).getSpells().purgeEffect(ESM::MagicEffect::Invisibility);
actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(ESM::MagicEffect::Invisibility);
if (actor.getClass().hasInventoryStore(actor))
actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Invisibility);
actor.getClass().getCreatureStats(actor).getActiveSpells().purgeEffect(actor, ESM::MagicEffect::Invisibility);
// Normally updated once per frame, but here it is kinda important to do it right away.
MWBase::Environment::get().getMechanicsManager()->updateMagicEffects(actor);
@ -3700,7 +3688,7 @@ namespace MWWorld
}
void World::explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const Ptr& caster, const Ptr& ignore, ESM::RangeType rangeType,
const std::string& id, const std::string& sourceName, const bool fromProjectile)
const std::string& id, const std::string& sourceName, const bool fromProjectile, int slot)
{
std::map<MWWorld::Ptr, std::vector<ESM::ENAMstruct> > toApply;
for (const ESM::ENAMstruct& effectInfo : effects.mList)
@ -3778,7 +3766,7 @@ namespace MWWorld
cast.mHitPosition = origin;
cast.mId = id;
cast.mSourceName = sourceName;
cast.mStack = false;
cast.mSlot = slot;
ESM::EffectList effectsToApply;
effectsToApply.mList = applyPair.second;
cast.inflict(applyPair.first, caster, effectsToApply, rangeType, false, true);

@ -636,7 +636,7 @@ namespace MWWorld
*/
void castSpell (const MWWorld::Ptr& actor, bool manualSpell=false) override;
void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) override;
void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, int slot) override;
void launchProjectile (MWWorld::Ptr& actor, MWWorld::Ptr& projectile,
const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr& bow, float speed, float attackStrength) override;
void updateProjectilesCasters() override;
@ -681,7 +681,7 @@ namespace MWWorld
void explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const MWWorld::Ptr& ignore,
ESM::RangeType rangeType, const std::string& id, const std::string& sourceName,
const bool fromProjectile=false) override;
const bool fromProjectile=false, int slot = 0) override;
void activate (const MWWorld::Ptr& object, const MWWorld::Ptr& actor) override;

@ -3,44 +3,67 @@
#include "esmreader.hpp"
#include "esmwriter.hpp"
namespace ESM
namespace
{
void ActiveSpells::save(ESMWriter &esm) const
void save(ESM::ESMWriter& esm, const std::vector<ESM::ActiveSpells::ActiveSpellParams>& spells, const std::string& tag)
{
for (TContainer::const_iterator it = mSpells.begin(); it != mSpells.end(); ++it)
for (const auto& params : spells)
{
esm.writeHNString ("ID__", it->first);
const ActiveSpellParams& params = it->second;
esm.writeHNString (tag, params.mId);
esm.writeHNT ("CAST", params.mCasterActorId);
esm.writeHNString ("DISP", params.mDisplayName);
esm.writeHNT ("TYPE", params.mType);
if(params.mItem.isSet())
params.mItem.save(esm, true, "ITEM");
if(params.mWorsenings >= 0)
{
esm.writeHNT ("WORS", params.mWorsenings);
esm.writeHNT ("TIME", params.mNextWorsening);
}
for (std::vector<ActiveEffect>::const_iterator effectIt = params.mEffects.begin(); effectIt != params.mEffects.end(); ++effectIt)
for (auto effect : params.mEffects)
{
esm.writeHNT ("MGEF", effectIt->mEffectId);
if (effectIt->mArg != -1)
esm.writeHNT ("ARG_", effectIt->mArg);
esm.writeHNT ("MAGN", effectIt->mMagnitude);
esm.writeHNT ("DURA", effectIt->mDuration);
esm.writeHNT ("EIND", effectIt->mEffectIndex);
esm.writeHNT ("LEFT", effectIt->mTimeLeft);
esm.writeHNT ("MGEF", effect.mEffectId);
if (effect.mArg != -1)
esm.writeHNT ("ARG_", effect.mArg);
esm.writeHNT ("MAGN", effect.mMagnitude);
esm.writeHNT ("MAGN", effect.mMinMagnitude);
esm.writeHNT ("MAGN", effect.mMaxMagnitude);
esm.writeHNT ("DURA", effect.mDuration);
esm.writeHNT ("EIND", effect.mEffectIndex);
esm.writeHNT ("LEFT", effect.mTimeLeft);
esm.writeHNT ("FLAG", effect.mFlags);
}
}
}
void ActiveSpells::load(ESMReader &esm)
void load(ESM::ESMReader& esm, std::vector<ESM::ActiveSpells::ActiveSpellParams>& spells, const char* tag)
{
int format = esm.getFormat();
while (esm.isNextSub("ID__"))
while (esm.isNextSub(tag))
{
std::string spellId = esm.getHString();
ActiveSpellParams params;
ESM::ActiveSpells::ActiveSpellParams params;
params.mId = esm.getHString();
esm.getHNT (params.mCasterActorId, "CAST");
params.mDisplayName = esm.getHNString ("DISP");
params.mItem.unset();
if (format < 17)
params.mType = ESM::ActiveSpells::Type_Temporary;
else
{
esm.getHNT (params.mType, "TYPE");
if(esm.peekNextSub("ITEM"))
params.mItem.load(esm, true, "ITEM");
}
if(esm.isNextSub("WORS"))
{
esm.getHT(params.mWorsenings);
esm.getHNT(params.mNextWorsening, "TIME");
}
else
params.mWorsenings = -1;
// spell casting timestamp, no longer used
if (esm.isNextSub("TIME"))
@ -48,11 +71,21 @@ namespace ESM
while (esm.isNextSub("MGEF"))
{
ActiveEffect effect;
ESM::ActiveEffect effect;
esm.getHT(effect.mEffectId);
effect.mArg = -1;
esm.getHNOT(effect.mArg, "ARG_");
esm.getHNT (effect.mMagnitude, "MAGN");
if (format < 17)
{
effect.mMinMagnitude = effect.mMagnitude;
effect.mMaxMagnitude = effect.mMagnitude;
}
else
{
esm.getHNT (effect.mMinMagnitude, "MAGN");
esm.getHNT (effect.mMaxMagnitude, "MAGN");
}
esm.getHNT (effect.mDuration, "DURA");
effect.mEffectIndex = -1;
esm.getHNOT (effect.mEffectIndex, "EIND");
@ -60,10 +93,30 @@ namespace ESM
effect.mTimeLeft = effect.mDuration;
else
esm.getHNT (effect.mTimeLeft, "LEFT");
if (format < 17)
effect.mFlags = ESM::ActiveEffect::Flag_None;
else
esm.getHNT (effect.mFlags, "FLAG");
params.mEffects.push_back(effect);
}
mSpells.insert(std::make_pair(spellId, params));
spells.emplace_back(params);
}
}
}
namespace ESM
{
void ActiveSpells::save(ESMWriter &esm) const
{
::save(esm, mSpells, "ID__");
::save(esm, mQueue, "QID_");
}
void ActiveSpells::load(ESMReader &esm)
{
::load(esm, mSpells, "ID__");
::load(esm, mQueue, "QID_");
}
}

@ -1,11 +1,12 @@
#ifndef OPENMW_ESM_ACTIVESPELLS_H
#define OPENMW_ESM_ACTIVESPELLS_H
#include "effectlist.hpp"
#include "cellref.hpp"
#include "defs.hpp"
#include "effectlist.hpp"
#include <string>
#include <map>
#include <vector>
namespace ESM
{
@ -14,29 +15,53 @@ namespace ESM
// Parameters of an effect concerning lasting effects.
// Note we are not using ENAMstruct since the magnitude may be modified by magic resistance, etc.
// It could also be a negative magnitude, in case of inversing an effect, e.g. Absorb spell causes damage on target, but heals the caster.
struct ActiveEffect
{
enum Flags
{
Flag_None = 0,
Flag_Applied = 1 << 0,
Flag_Remove = 1 << 1,
Flag_Ignore_Resistances = 1 << 2
};
int mEffectId;
float mMagnitude;
float mMinMagnitude;
float mMaxMagnitude;
int mArg; // skill or attribute
float mDuration;
float mTimeLeft;
int mEffectIndex;
int mFlags;
};
// format 0, saved games only
struct ActiveSpells
{
enum EffectType
{
Type_Temporary,
Type_Ability,
Type_Enchantment,
Type_Permanent,
Type_Consumable
};
struct ActiveSpellParams
{
std::string mId;
std::vector<ActiveEffect> mEffects;
std::string mDisplayName;
int mCasterActorId;
RefNum mItem;
EffectType mType;
int mWorsenings;
TimeStamp mNextWorsening;
};
typedef std::multimap<std::string, ActiveSpellParams > TContainer;
TContainer mSpells;
std::vector<ActiveSpellParams> mSpells;
std::vector<ActiveSpellParams> mQueue;
void load (ESMReader &esm);
void save (ESMWriter &esm) const;

@ -110,16 +110,31 @@ void ESM::CreatureStats::load (ESMReader &esm)
mAiSequence.load(esm);
mMagicEffects.load(esm);
while (esm.isNextSub("SUMM"))
if (esm.getFormat() < 17)
{
int magicEffect;
esm.getHT(magicEffect);
std::string source = esm.getHNOString("SOUR");
int effectIndex = -1;
esm.getHNOT (effectIndex, "EIND");
int actorId;
esm.getHNT (actorId, "ACID");
mSummonedCreatureMap[SummonKey(magicEffect, source, effectIndex)] = actorId;
while (esm.isNextSub("SUMM"))
{
int magicEffect;
esm.getHT(magicEffect);
std::string source = esm.getHNOString("SOUR");
int effectIndex = -1;
esm.getHNOT (effectIndex, "EIND");
int actorId;
esm.getHNT (actorId, "ACID");
mSummonedCreatureMap[SummonKey(magicEffect, source, effectIndex)] = actorId;
mSummonedCreatures.emplace(magicEffect, actorId);
}
}
else
{
while (esm.isNextSub("SUMM"))
{
int magicEffect;
esm.getHT(magicEffect);
int actorId;
esm.getHNT (actorId, "ACID");
mSummonedCreatures.emplace(magicEffect, actorId);
}
}
while (esm.isNextSub("GRAV"))
@ -214,14 +229,10 @@ void ESM::CreatureStats::save (ESMWriter &esm) const
mAiSequence.save(esm);
mMagicEffects.save(esm);
for (const auto& summon : mSummonedCreatureMap)
for (const auto& [effectId, actorId] : mSummonedCreatures)
{
esm.writeHNT ("SUMM", summon.first.mEffectId);
esm.writeHNString ("SOUR", summon.first.mSourceId);
int effectIndex = summon.first.mEffectIndex;
if (effectIndex != -1)
esm.writeHNT ("EIND", effectIndex);
esm.writeHNT ("ACID", summon.second);
esm.writeHNT ("SUMM", effectId);
esm.writeHNT ("ACID", actorId);
}
for (int key : mSummonGraveyard)
@ -235,15 +246,6 @@ void ESM::CreatureStats::save (ESMWriter &esm) const
for (int i=0; i<4; ++i)
mAiSettings[i].save(esm);
}
for (const auto& corprusSpell : mCorprusSpells)
{
esm.writeHNString("CORP", corprusSpell.first);
const CorprusStats & stats = corprusSpell.second;
esm.writeHNT("WORS", stats.mWorsenings);
esm.writeHNT("TIME", stats.mNextWorsening);
}
}
void ESM::CreatureStats::blank()

@ -40,6 +40,7 @@ namespace ESM
StatState<int> mAiSettings[4];
std::map<SummonKey, int> mSummonedCreatureMap;
std::multimap<int, int> mSummonedCreatures;
std::vector<int> mSummonGraveyard;
ESM::TimeStamp mTradeTime;

@ -8,10 +8,11 @@ namespace ESM
void MagicEffects::save(ESMWriter &esm) const
{
for (std::map<int, int>::const_iterator it = mEffects.begin(); it != mEffects.end(); ++it)
for (const auto& [key, params] : mEffects)
{
esm.writeHNT("EFID", it->first);
esm.writeHNT("BASE", it->second);
esm.writeHNT("EFID", key);
esm.writeHNT("BASE", params.first);
esm.writeHNT("MODI", params.second);
}
}
@ -19,10 +20,15 @@ void MagicEffects::load(ESMReader &esm)
{
while (esm.isNextSub("EFID"))
{
int id, base;
int id;
std::pair<int, float> params;
esm.getHT(id);
esm.getHNT(base, "BASE");
mEffects.insert(std::make_pair(id, base));
esm.getHNT(params.first, "BASE");
if(esm.getFormat() < 17)
params.second = 0.f;
else
esm.getHNT(params.second, "MODI");
mEffects.emplace(id, params);
}
}

@ -12,8 +12,8 @@ namespace ESM
// format 0, saved games only
struct MagicEffects
{
// <Effect Id, Base value>
std::map<int, int> mEffects;
// <Effect Id, Base value, Modifier>
std::map<int, std::pair<int, float>> mEffects;
void load (ESMReader &esm);
void save (ESMWriter &esm) const;

@ -28,6 +28,7 @@ namespace ESM
esm.writeHNString ("SPEL", mSpellId);
esm.writeHNT ("SPED", mSpeed);
esm.writeHNT ("SLOT", mSlot);
}
void MagicBoltState::load(ESMReader &esm)
@ -39,6 +40,10 @@ namespace ESM
esm.skipHSub();
ESM::EffectList().load(esm); // for backwards compatibility
esm.getHNT (mSpeed, "SPED");
if(esm.getFormat() < 17)
mSlot = 0;
else
esm.getHNT(mSlot, "SLOT");
if (esm.isNextSub("STCK")) // for backwards compatibility
esm.skipHSub();
if (esm.isNextSub("SOUN")) // for backwards compatibility

@ -32,6 +32,7 @@ namespace ESM
{
std::string mSpellId;
float mSpeed;
int mSlot;
void load (ESMReader &esm);
void save (ESMWriter &esm) const;

@ -4,7 +4,7 @@
#include "esmwriter.hpp"
unsigned int ESM::SavedGame::sRecordId = ESM::REC_SAVE;
int ESM::SavedGame::sCurrentFormat = 16;
int ESM::SavedGame::sCurrentFormat = 17;
void ESM::SavedGame::load (ESMReader &esm)
{

@ -8,29 +8,38 @@ namespace ESM
void SpellState::load(ESMReader &esm)
{
while (esm.isNextSub("SPEL"))
if(esm.getFormat() < 17)
{
std::string id = esm.getHString();
SpellParams state;
while (esm.isNextSub("INDX"))
while (esm.isNextSub("SPEL"))
{
int index;
esm.getHT(index);
std::string id = esm.getHString();
float magnitude;
esm.getHNT(magnitude, "RAND");
SpellParams state;
while (esm.isNextSub("INDX"))
{
int index;
esm.getHT(index);
state.mEffectRands[index] = magnitude;
}
float magnitude;
esm.getHNT(magnitude, "RAND");
while (esm.isNextSub("PURG")) {
int index;
esm.getHT(index);
state.mPurgedEffects.insert(index);
}
state.mEffectRands[index] = magnitude;
}
mSpells[id] = state;
while (esm.isNextSub("PURG")) {
int index;
esm.getHT(index);
state.mPurgedEffects.insert(index);
}
mSpellParams[id] = state;
mSpells.emplace_back(id);
}
}
else
{
while (esm.isNextSub("SPEL"))
mSpells.emplace_back(esm.getHString());
}
// Obsolete
@ -88,30 +97,8 @@ namespace ESM
void SpellState::save(ESMWriter &esm) const
{
for (TContainer::const_iterator it = mSpells.begin(); it != mSpells.end(); ++it)
{
esm.writeHNString("SPEL", it->first);
const std::map<int, float>& random = it->second.mEffectRands;
for (std::map<int, float>::const_iterator rIt = random.begin(); rIt != random.end(); ++rIt)
{
esm.writeHNT("INDX", rIt->first);
esm.writeHNT("RAND", rIt->second);
}
const std::set<int>& purges = it->second.mPurgedEffects;
for (std::set<int>::const_iterator pIt = purges.begin(); pIt != purges.end(); ++pIt)
esm.writeHNT("PURG", *pIt);
}
for (std::map<std::string, CorprusStats>::const_iterator it = mCorprusSpells.begin(); it != mCorprusSpells.end(); ++it)
{
esm.writeHNString("CORP", it->first);
const CorprusStats & stats = it->second;
esm.writeHNT("WORS", stats.mWorsenings);
esm.writeHNT("TIME", stats.mNextWorsening);
}
for (const std::string& spell : mSpells)
esm.writeHNString("SPEL", spell);
for (std::map<std::string, TimeStamp>::const_iterator it = mUsedPowers.begin(); it != mUsedPowers.end(); ++it)
{

@ -31,13 +31,13 @@ namespace ESM
struct SpellParams
{
std::map<int, float> mEffectRands;
std::set<int> mPurgedEffects;
std::map<int, float> mEffectRands; // <effect index, normalised random magnitude>
std::set<int> mPurgedEffects; // indices of purged effects
};
typedef std::map<std::string, SpellParams> TContainer;
TContainer mSpells;
std::vector<std::string> mSpells;
// FIXME: obsolete, used only for old saves
std::map<std::string, SpellParams> mSpellParams;
std::map<std::string, std::vector<PermanentSpellEffectInfo> > mPermanentSpellEffects;
std::map<std::string, CorprusStats> mCorprusSpells;

Loading…
Cancel
Save