#include "spells.hpp" #include #include #include #include #include /* Start of tes3mp addition Include additional headers for multiplayer purposes */ #include "../mwmp/Main.hpp" #include "../mwmp/LocalPlayer.hpp" /* End of tes3mp addition */ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "actorutil.hpp" #include "creaturestats.hpp" #include "magiceffects.hpp" #include "stat.hpp" 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) { 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)) { if (mSpellList) mSpellList->updateListener(&spells, this); } std::map::const_iterator Spells::begin() const { return mSpells.begin(); } std::map::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)); } bool Spells::hasSpell(const ESM::Spell *spell) const { return mSpells.find(spell) != mSpells.end(); } void Spells::add (const ESM::Spell* spell) { mSpellList->add(spell); } void Spells::add (const std::string& spellId) { add(SpellList::getSpell(spellId)); } void Spells::addSpell(const ESM::Spell* spell) { if (mSpells.find (spell)==mSpells.end()) { std::map 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; imEffects.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(delta); } } } SpellParams params; params.mEffectRands = random; mSpells.emplace(spell, params); mSpellsChanged = true; } } void Spells::remove (const std::string& spellId) { const auto spell = SpellList::getSpell(spellId); removeSpell(spell); mSpellList->remove(spell); if (spellId==mSelectedSpell) mSelectedSpell.clear(); } void Spells::removeSpell(const ESM::Spell* spell) { const auto it = mSpells.find(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) { removeAllSpells(); if(modifyBase) mSpellList->clear(); } void Spells::setSelectedSpell (const std::string& spellId) { mSelectedSpell = spellId; } const std::string Spells::getSelectedSpell() const { 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().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) { const ESM::Spell *spell = iter.first; if (spell->mData.mType == type) return true; } return false; } bool Spells::hasCommonDisease() const { return hasDisease(ESM::Spell::ST_Disease); } bool Spells::hasBlightDisease() const { return hasDisease(ESM::Spell::ST_Blight); } void Spells::purge(const SpellFilter& filter) { std::vector purged; for (auto iter = mSpells.begin(); iter!=mSpells.end();) { const ESM::Spell *spell = iter->first; if (filter(spell)) { /* Start of tes3mp addition Send an ID_PLAYER_SPELLBOOK packet every time a spell is purged here */ mwmp::Main::get().getLocalPlayer()->sendSpellChange(spell->mId, mwmp::SpellbookChanges::REMOVE); /* End of tes3mp addition */ mSpells.erase(iter++); purged.push_back(spell->mId); mSpellsChanged = true; } else ++iter; } if(!purged.empty()) mSpellList->removeAll(purged); } void Spells::purgeCommonDisease() { purge([](auto spell) { return spell->mData.mType == ESM::Spell::ST_Disease; }); } void Spells::purgeBlightDisease() { purge([](auto spell) { return spell->mData.mType == ESM::Spell::ST_Blight && !hasCorprusEffect(spell); }); } void Spells::purgeCorprusDisease() { purge(&hasCorprusEffect); } void Spells::purgeCurses() { 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) { if (effectIt.mEffectID == ESM::MagicEffect::Corprus) { return true; } } 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().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); return it == mUsedPowers.end() || it->second + 24 <= MWBase::Environment::get().getWorld()->getTimeStamp(); } void Spells::usePower(const ESM::Spell* spell) { mUsedPowers[spell] = MWBase::Environment::get().getWorld()->getTimeStamp(); /* Start of tes3mp addition Send an ID_PLAYER_COOLDOWN packet every time a cooldown is recorded here */ mwmp::Main::get().getLocalPlayer()->sendCooldownChange(spell->mId, MWBase::Environment::get().getWorld()->getTimeStamp().getDay(), MWBase::Environment::get().getWorld()->getTimeStamp().getHour()); /* End of tes3mp addition */ } /* Start of tes3mp addition Make it possible to set timestamps for power cooldowns, necessary for ID_PLAYER_COOLDOWNS packets */ void Spells::setPowerUseTimestamp(const ESM::Spell* spell, int startDay, float startHour) { ESM::TimeStamp timestamp; timestamp.mDay = startDay; timestamp.mHour = startHour; mUsedPowers[spell] = MWWorld::TimeStamp(timestamp); } /* End of tes3mp addition */ void Spells::readState(const ESM::SpellState &state, CreatureStats* creatureStats) { const auto& baseSpells = mSpellList->getSpells(); for (ESM::SpellState::TContainer::const_iterator it = state.mSpells.begin(); it != state.mSpells.end(); ++it) { // Discard spells that are no longer available due to changed content files const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); if (spell) { mSpells[spell].mEffectRands = it->second.mEffectRands; mSpells[spell].mPurgedEffects = it->second.mPurgedEffects; if (it->first == state.mSelectedSpell) mSelectedSpell = it->first; } } // Add spells from the base record for(const std::string& id : baseSpells) { const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(id); if(spell) addSpell(spell); } for (std::map::const_iterator it = state.mUsedPowers.begin(); it != state.mUsedPowers.end(); ++it) { const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); if (!spell) continue; mUsedPowers[spell] = MWWorld::TimeStamp(it->second); } for (std::map::const_iterator it = state.mCorprusSpells.begin(); it != state.mCorprusSpells.end(); ++it) { const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); if (!spell) continue; CorprusStats stats; int worsening = state.mCorprusSpells.at(it->first).mWorsenings; for (int i=0; imEffects.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 >::const_iterator it = state.mPermanentSpellEffects.begin(); it != state.mPermanentSpellEffects.end(); ++it) { const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().search(it->first); if (!spell) continue; // Import data only for player, other actors should not suffer from corprus worsening. MWWorld::Ptr player = getPlayer(); if (creatureStats->getActorId() != player.getClass().getCreatureStats(player).getActorId()) return; // Note: if target actor has the Restore attirbute effects, stats will be restored. for (std::vector::const_iterator effectIt = it->second.begin(); effectIt != it->second.end(); ++effectIt) { // Applied corprus effects are already in loaded stats modifiers if (effectIt->mId == ESM::MagicEffect::FortifyAttribute) { AttributeValue attr = creatureStats->getAttribute(effectIt->mArg); attr.setModifier(attr.getModifier() - effectIt->mMagnitude); attr.damage(-effectIt->mMagnitude); creatureStats->setAttribute(effectIt->mArg, attr); } else if (effectIt->mId == ESM::MagicEffect::DrainAttribute) { AttributeValue attr = creatureStats->getAttribute(effectIt->mArg); attr.setModifier(attr.getModifier() + effectIt->mMagnitude); attr.damage(effectIt->mMagnitude); creatureStats->setAttribute(effectIt->mArg, attr); } } } } void Spells::writeState(ESM::SpellState &state) const { const auto& baseSpells = mSpellList->getSpells(); for (const auto& it : 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()) { ESM::SpellState::SpellParams params; params.mEffectRands = it.second.mEffectRands; params.mPurgedEffects = it.second.mPurgedEffects; state.mSpells.emplace(it.first->mId, params); } } state.mSelectedSpell = mSelectedSpell; for (const auto& it : mUsedPowers) state.mUsedPowers[it.first->mId] = it.second.toEsm(); } bool Spells::setSpells(const std::string& actorId) { bool result; std::tie(mSpellList, result) = MWBase::Environment::get().getWorld()->getStore().getSpellList(actorId); mSpellList->addListener(this); addAllToInstance(mSpellList->getSpells()); return result; } void Spells::addAllToInstance(const std::vector& spells) { for(const std::string& id : spells) { const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().search(id); if(spell) addSpell(spell); else Log(Debug::Warning) << "Warning: ignoring nonexistent spell '" << id << "'"; } } Spells::~Spells() { if(mSpellList) mSpellList->removeListener(this); } }