From 320d8ef01441c2a9f3e7cc2a93644f5206fcac28 Mon Sep 17 00:00:00 2001 From: Mads Buvik Sandvei Date: Mon, 25 Mar 2024 13:50:23 +0000 Subject: [PATCH] Spellcast related Lua API + spellcasting/activespell refactor --- apps/esmtool/record.cpp | 27 +- apps/opencs/model/tools/enchantmentcheck.cpp | 20 +- .../model/world/nestedcoladapterimp.hpp | 39 +-- apps/openmw/mwbase/mechanicsmanager.hpp | 2 +- apps/openmw/mwbase/world.hpp | 2 +- apps/openmw/mwclass/door.cpp | 2 +- apps/openmw/mwgui/enchantingdialog.cpp | 2 +- apps/openmw/mwgui/hud.cpp | 2 +- apps/openmw/mwgui/quickkeysmenu.cpp | 3 +- apps/openmw/mwgui/spellcreationdialog.cpp | 13 +- apps/openmw/mwgui/spellmodel.cpp | 6 +- apps/openmw/mwgui/tooltips.cpp | 18 +- apps/openmw/mwgui/widgets.cpp | 34 +- apps/openmw/mwlua/magicbindings.cpp | 306 +++++++++++++++--- apps/openmw/mwlua/types/ingredient.cpp | 19 +- apps/openmw/mwlua/types/potion.cpp | 7 +- apps/openmw/mwmechanics/activespells.cpp | 139 +++++--- apps/openmw/mwmechanics/activespells.hpp | 25 +- apps/openmw/mwmechanics/actors.cpp | 4 +- apps/openmw/mwmechanics/actors.hpp | 2 +- apps/openmw/mwmechanics/aicast.cpp | 8 +- apps/openmw/mwmechanics/aicast.hpp | 4 +- apps/openmw/mwmechanics/aicombat.cpp | 2 +- apps/openmw/mwmechanics/aicombataction.cpp | 12 +- apps/openmw/mwmechanics/alchemy.cpp | 12 +- apps/openmw/mwmechanics/autocalcspell.cpp | 20 +- apps/openmw/mwmechanics/character.cpp | 30 +- apps/openmw/mwmechanics/character.hpp | 4 +- apps/openmw/mwmechanics/enchanting.cpp | 22 +- .../mwmechanics/mechanicsmanagerimp.cpp | 10 +- .../mwmechanics/mechanicsmanagerimp.hpp | 2 +- apps/openmw/mwmechanics/spellcasting.cpp | 168 ++++------ apps/openmw/mwmechanics/spellcasting.hpp | 10 +- apps/openmw/mwmechanics/spelleffects.cpp | 47 ++- apps/openmw/mwmechanics/spellpriority.cpp | 24 +- apps/openmw/mwmechanics/spells.cpp | 2 +- apps/openmw/mwmechanics/spellutil.cpp | 77 ++++- apps/openmw/mwmechanics/spellutil.hpp | 6 + apps/openmw/mwrender/animation.cpp | 7 +- apps/openmw/mwrender/animation.hpp | 2 +- apps/openmw/mwscript/statsextensions.cpp | 2 +- apps/openmw/mwworld/class.cpp | 2 +- apps/openmw/mwworld/esmstore.cpp | 27 +- apps/openmw/mwworld/esmstore.hpp | 5 +- apps/openmw/mwworld/magiceffects.cpp | 51 ++- apps/openmw/mwworld/projectilemanager.cpp | 15 +- apps/openmw/mwworld/worldimp.cpp | 18 +- apps/openmw_test_suite/esm3/testsaveload.cpp | 37 +-- components/esm3/activespells.cpp | 39 ++- components/esm3/activespells.hpp | 44 ++- components/esm3/effectlist.cpp | 27 +- components/esm3/effectlist.hpp | 13 +- components/esm3/formatversion.hpp | 3 +- files/lua_api/openmw/core.lua | 27 +- files/lua_api/openmw/types.lua | 50 ++- 55 files changed, 974 insertions(+), 527 deletions(-) diff --git a/apps/esmtool/record.cpp b/apps/esmtool/record.cpp index 912ad0d683..fd51560f80 100644 --- a/apps/esmtool/record.cpp +++ b/apps/esmtool/record.cpp @@ -180,22 +180,23 @@ namespace void printEffectList(const ESM::EffectList& effects) { int i = 0; - for (const ESM::ENAMstruct& effect : effects.mList) + for (const ESM::IndexedENAMstruct& effect : effects.mList) { - std::cout << " Effect[" << i << "]: " << magicEffectLabel(effect.mEffectID) << " (" << effect.mEffectID - << ")" << std::endl; - if (effect.mSkill != -1) - std::cout << " Skill: " << skillLabel(effect.mSkill) << " (" << (int)effect.mSkill << ")" + std::cout << " Effect[" << i << "]: " << magicEffectLabel(effect.mData.mEffectID) << " (" + << effect.mData.mEffectID << ")" << std::endl; + if (effect.mData.mSkill != -1) + std::cout << " Skill: " << skillLabel(effect.mData.mSkill) << " (" << (int)effect.mData.mSkill << ")" << std::endl; - if (effect.mAttribute != -1) - std::cout << " Attribute: " << attributeLabel(effect.mAttribute) << " (" << (int)effect.mAttribute - << ")" << std::endl; - std::cout << " Range: " << rangeTypeLabel(effect.mRange) << " (" << effect.mRange << ")" << std::endl; + if (effect.mData.mAttribute != -1) + std::cout << " Attribute: " << attributeLabel(effect.mData.mAttribute) << " (" + << (int)effect.mData.mAttribute << ")" << std::endl; + std::cout << " Range: " << rangeTypeLabel(effect.mData.mRange) << " (" << effect.mData.mRange << ")" + << std::endl; // Area is always zero if range type is "Self" - if (effect.mRange != ESM::RT_Self) - std::cout << " Area: " << effect.mArea << std::endl; - std::cout << " Duration: " << effect.mDuration << std::endl; - std::cout << " Magnitude: " << effect.mMagnMin << "-" << effect.mMagnMax << std::endl; + if (effect.mData.mRange != ESM::RT_Self) + std::cout << " Area: " << effect.mData.mArea << std::endl; + std::cout << " Duration: " << effect.mData.mDuration << std::endl; + std::cout << " Magnitude: " << effect.mData.mMagnMin << "-" << effect.mData.mMagnMax << std::endl; i++; } } diff --git a/apps/opencs/model/tools/enchantmentcheck.cpp b/apps/opencs/model/tools/enchantmentcheck.cpp index d6cb22b738..48cee579be 100644 --- a/apps/opencs/model/tools/enchantmentcheck.cpp +++ b/apps/opencs/model/tools/enchantmentcheck.cpp @@ -60,38 +60,38 @@ void CSMTools::EnchantmentCheckStage::perform(int stage, CSMDoc::Messages& messa } else { - std::vector::const_iterator effect = enchantment.mEffects.mList.begin(); + std::vector::const_iterator effect = enchantment.mEffects.mList.begin(); for (size_t i = 1; i <= enchantment.mEffects.mList.size(); i++) { const std::string number = std::to_string(i); // At the time of writing this effects, attributes and skills are hardcoded - if (effect->mEffectID < 0 || effect->mEffectID > 142) + if (effect->mData.mEffectID < 0 || effect->mData.mEffectID > 142) { messages.add(id, "Effect #" + number + " is invalid", "", CSMDoc::Message::Severity_Error); ++effect; continue; } - if (effect->mSkill < -1 || effect->mSkill > 26) + if (effect->mData.mSkill < -1 || effect->mData.mSkill > 26) messages.add( id, "Effect #" + number + " affected skill is invalid", "", CSMDoc::Message::Severity_Error); - if (effect->mAttribute < -1 || effect->mAttribute > 7) + if (effect->mData.mAttribute < -1 || effect->mData.mAttribute > 7) messages.add( id, "Effect #" + number + " affected attribute is invalid", "", CSMDoc::Message::Severity_Error); - if (effect->mRange < 0 || effect->mRange > 2) + if (effect->mData.mRange < 0 || effect->mData.mRange > 2) messages.add(id, "Effect #" + number + " range is invalid", "", CSMDoc::Message::Severity_Error); - if (effect->mArea < 0) + if (effect->mData.mArea < 0) messages.add(id, "Effect #" + number + " area is negative", "", CSMDoc::Message::Severity_Error); - if (effect->mDuration < 0) + if (effect->mData.mDuration < 0) messages.add(id, "Effect #" + number + " duration is negative", "", CSMDoc::Message::Severity_Error); - if (effect->mMagnMin < 0) + if (effect->mData.mMagnMin < 0) messages.add( id, "Effect #" + number + " minimum magnitude is negative", "", CSMDoc::Message::Severity_Error); - if (effect->mMagnMax < 0) + if (effect->mData.mMagnMax < 0) messages.add( id, "Effect #" + number + " maximum magnitude is negative", "", CSMDoc::Message::Severity_Error); - if (effect->mMagnMin > effect->mMagnMax) + if (effect->mData.mMagnMin > effect->mData.mMagnMax) messages.add(id, "Effect #" + number + " minimum magnitude is higher than maximum magnitude", "", CSMDoc::Message::Severity_Error); ++effect; diff --git a/apps/opencs/model/world/nestedcoladapterimp.hpp b/apps/opencs/model/world/nestedcoladapterimp.hpp index 46928973fe..e18cda9611 100644 --- a/apps/opencs/model/world/nestedcoladapterimp.hpp +++ b/apps/opencs/model/world/nestedcoladapterimp.hpp @@ -255,20 +255,22 @@ namespace CSMWorld { ESXRecordT magic = record.get(); - std::vector& effectsList = magic.mEffects.mList; + std::vector& effectsList = magic.mEffects.mList; // blank row - ESM::ENAMstruct effect; - effect.mEffectID = 0; - effect.mSkill = -1; - effect.mAttribute = -1; - effect.mRange = 0; - effect.mArea = 0; - effect.mDuration = 0; - effect.mMagnMin = 0; - effect.mMagnMax = 0; + ESM::IndexedENAMstruct effect; + effect.mIndex = position; + effect.mData.mEffectID = 0; + effect.mData.mSkill = -1; + effect.mData.mAttribute = -1; + effect.mData.mRange = 0; + effect.mData.mArea = 0; + effect.mData.mDuration = 0; + effect.mData.mMagnMin = 0; + effect.mData.mMagnMax = 0; effectsList.insert(effectsList.begin() + position, effect); + magic.mEffects.updateIndexes(); record.setModified(magic); } @@ -277,12 +279,13 @@ namespace CSMWorld { ESXRecordT magic = record.get(); - std::vector& effectsList = magic.mEffects.mList; + std::vector& effectsList = magic.mEffects.mList; if (rowToRemove < 0 || rowToRemove >= static_cast(effectsList.size())) throw std::runtime_error("index out of range"); effectsList.erase(effectsList.begin() + rowToRemove); + magic.mEffects.updateIndexes(); record.setModified(magic); } @@ -292,7 +295,7 @@ namespace CSMWorld ESXRecordT magic = record.get(); magic.mEffects.mList - = static_cast>&>(nestedTable).mNestedTable; + = static_cast>&>(nestedTable).mNestedTable; record.setModified(magic); } @@ -300,19 +303,19 @@ namespace CSMWorld NestedTableWrapperBase* table(const Record& record) const override { // deleted by dtor of NestedTableStoring - return new NestedTableWrapper>(record.get().mEffects.mList); + return new NestedTableWrapper>(record.get().mEffects.mList); } QVariant getData(const Record& record, int subRowIndex, int subColIndex) const override { ESXRecordT magic = record.get(); - std::vector& effectsList = magic.mEffects.mList; + std::vector& effectsList = magic.mEffects.mList; if (subRowIndex < 0 || subRowIndex >= static_cast(effectsList.size())) throw std::runtime_error("index out of range"); - ESM::ENAMstruct effect = effectsList[subRowIndex]; + ESM::ENAMstruct effect = effectsList[subRowIndex].mData; switch (subColIndex) { case 0: @@ -374,12 +377,12 @@ namespace CSMWorld { ESXRecordT magic = record.get(); - std::vector& effectsList = magic.mEffects.mList; + std::vector& effectsList = magic.mEffects.mList; if (subRowIndex < 0 || subRowIndex >= static_cast(effectsList.size())) throw std::runtime_error("index out of range"); - ESM::ENAMstruct effect = effectsList[subRowIndex]; + ESM::ENAMstruct effect = effectsList[subRowIndex].mData; switch (subColIndex) { case 0: @@ -438,7 +441,7 @@ namespace CSMWorld throw std::runtime_error("Magic Effects subcolumn index out of range"); } - magic.mEffects.mList[subRowIndex] = effect; + magic.mEffects.mList[subRowIndex].mData = effect; record.setModified(magic); } diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index c8e353acc9..37586ed33a 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -265,7 +265,7 @@ namespace MWBase virtual bool isReadyToBlock(const MWWorld::Ptr& ptr) const = 0; virtual bool isAttackingOrSpell(const MWWorld::Ptr& ptr) const = 0; - virtual void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool manualSpell) = 0; + virtual void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell) = 0; virtual void processChangedSettings(const std::set>& settings) = 0; diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index fe8b5cc13a..85e83b7f8a 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -463,7 +463,7 @@ namespace MWBase */ virtual MWWorld::SpellCastState startSpellCast(const MWWorld::Ptr& actor) = 0; - virtual void castSpell(const MWWorld::Ptr& actor, bool manualSpell = false) = 0; + virtual void castSpell(const MWWorld::Ptr& actor, bool scriptedSpell = false) = 0; virtual void launchMagicBolt(const ESM::RefId& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, ESM::RefNum item) diff --git a/apps/openmw/mwclass/door.cpp b/apps/openmw/mwclass/door.cpp index 7509fbf71f..c5cdc60cec 100644 --- a/apps/openmw/mwclass/door.cpp +++ b/apps/openmw/mwclass/door.cpp @@ -136,7 +136,7 @@ namespace MWClass const ESM::MagicEffect* effect = store.get().find(ESM::MagicEffect::Telekinesis); animation->addSpellCastGlow( - effect, 1); // 1 second glow to match the time taken for a door opening or closing + effect->getColor(), 1); // 1 second glow to match the time taken for a door opening or closing } } diff --git a/apps/openmw/mwgui/enchantingdialog.cpp b/apps/openmw/mwgui/enchantingdialog.cpp index 8264dd60b6..af4a3e8ce3 100644 --- a/apps/openmw/mwgui/enchantingdialog.cpp +++ b/apps/openmw/mwgui/enchantingdialog.cpp @@ -273,7 +273,7 @@ namespace MWGui void EnchantingDialog::notifyEffectsChanged() { - mEffectList.mList = mEffects; + mEffectList.populate(mEffects); mEnchanting.setEffect(mEffectList); updateLabels(); } diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp index 1c8aad5447..0ee341c5c2 100644 --- a/apps/openmw/mwgui/hud.cpp +++ b/apps/openmw/mwgui/hud.cpp @@ -427,7 +427,7 @@ namespace MWGui { // use the icon of the first effect const ESM::MagicEffect* effect = MWBase::Environment::get().getESMStore()->get().find( - spell->mEffects.mList.front().mEffectID); + spell->mEffects.mList.front().mData.mEffectID); std::string icon = effect->mIcon; std::replace(icon.begin(), icon.end(), '/', '\\'); size_t slashPos = icon.rfind('\\'); diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index 204fa00492..df7236242f 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -299,7 +299,8 @@ namespace MWGui mSelected->button->setUserString("Spell", spellId.serialize()); // use the icon of the first effect - const ESM::MagicEffect* effect = esmStore.get().find(spell->mEffects.mList.front().mEffectID); + const ESM::MagicEffect* effect + = esmStore.get().find(spell->mEffects.mList.front().mData.mEffectID); std::string path = effect->mIcon; std::replace(path.begin(), path.end(), '/', '\\'); diff --git a/apps/openmw/mwgui/spellcreationdialog.cpp b/apps/openmw/mwgui/spellcreationdialog.cpp index d668db1dec..d8302df87c 100644 --- a/apps/openmw/mwgui/spellcreationdialog.cpp +++ b/apps/openmw/mwgui/spellcreationdialog.cpp @@ -470,9 +470,7 @@ namespace MWGui y *= 1.5; } - ESM::EffectList effectList; - effectList.mList = mEffects; - mSpell.mEffects = std::move(effectList); + mSpell.mEffects.populate(mEffects); mSpell.mData.mCost = int(y); mSpell.mData.mType = ESM::Spell::ST_Spell; mSpell.mData.mFlags = 0; @@ -528,10 +526,11 @@ namespace MWGui if (spell->mData.mType != ESM::Spell::ST_Spell) continue; - for (const ESM::ENAMstruct& effectInfo : spell->mEffects.mList) + for (const ESM::IndexedENAMstruct& effectInfo : spell->mEffects.mList) { + int16_t effectId = effectInfo.mData.mEffectID; const ESM::MagicEffect* effect - = MWBase::Environment::get().getESMStore()->get().find(effectInfo.mEffectID); + = MWBase::Environment::get().getESMStore()->get().find(effectId); // skip effects that do not allow spellmaking/enchanting int requiredFlags @@ -539,8 +538,8 @@ namespace MWGui if (!(effect->mData.mFlags & requiredFlags)) continue; - if (std::find(knownEffects.begin(), knownEffects.end(), effectInfo.mEffectID) == knownEffects.end()) - knownEffects.push_back(effectInfo.mEffectID); + if (std::find(knownEffects.begin(), knownEffects.end(), effectId) == knownEffects.end()) + knownEffects.push_back(effectId); } } diff --git a/apps/openmw/mwgui/spellmodel.cpp b/apps/openmw/mwgui/spellmodel.cpp index 385464da24..3d70c391c9 100644 --- a/apps/openmw/mwgui/spellmodel.cpp +++ b/apps/openmw/mwgui/spellmodel.cpp @@ -48,14 +48,14 @@ namespace MWGui for (const auto& effect : effects.mList) { - short effectId = effect.mEffectID; + short effectId = effect.mData.mEffectID; if (effectId != -1) { const ESM::MagicEffect* magicEffect = store.get().find(effectId); const ESM::Attribute* attribute - = store.get().search(ESM::Attribute::indexToRefId(effect.mAttribute)); - const ESM::Skill* skill = store.get().search(ESM::Skill::indexToRefId(effect.mSkill)); + = store.get().search(ESM::Attribute::indexToRefId(effect.mData.mAttribute)); + const ESM::Skill* skill = store.get().search(ESM::Skill::indexToRefId(effect.mData.mSkill)); std::string fullEffectName = MWMechanics::getMagicEffectString(*magicEffect, attribute, skill); std::string convert = Utf8Stream::lowerCaseUtf8(fullEffectName); diff --git a/apps/openmw/mwgui/tooltips.cpp b/apps/openmw/mwgui/tooltips.cpp index 0e0c56c194..960a4a5a21 100644 --- a/apps/openmw/mwgui/tooltips.cpp +++ b/apps/openmw/mwgui/tooltips.cpp @@ -222,17 +222,17 @@ namespace MWGui = store->get().find(ESM::RefId::deserialize(focus->getUserString("Spell"))); info.caption = spell->mName; Widgets::SpellEffectList effects; - for (const ESM::ENAMstruct& spellEffect : spell->mEffects.mList) + for (const ESM::IndexedENAMstruct& spellEffect : spell->mEffects.mList) { Widgets::SpellEffectParams params; - params.mEffectID = spellEffect.mEffectID; - params.mSkill = ESM::Skill::indexToRefId(spellEffect.mSkill); - params.mAttribute = ESM::Attribute::indexToRefId(spellEffect.mAttribute); - params.mDuration = spellEffect.mDuration; - params.mMagnMin = spellEffect.mMagnMin; - params.mMagnMax = spellEffect.mMagnMax; - params.mRange = spellEffect.mRange; - params.mArea = spellEffect.mArea; + params.mEffectID = spellEffect.mData.mEffectID; + params.mSkill = ESM::Skill::indexToRefId(spellEffect.mData.mSkill); + params.mAttribute = ESM::Attribute::indexToRefId(spellEffect.mData.mAttribute); + params.mDuration = spellEffect.mData.mDuration; + params.mMagnMin = spellEffect.mData.mMagnMin; + params.mMagnMax = spellEffect.mData.mMagnMax; + params.mRange = spellEffect.mData.mRange; + params.mArea = spellEffect.mData.mArea; params.mIsConstant = (spell->mData.mType == ESM::Spell::ST_Ability); params.mNoTarget = false; effects.push_back(params); diff --git a/apps/openmw/mwgui/widgets.cpp b/apps/openmw/mwgui/widgets.cpp index fea6d490c5..6cc5bdfdf5 100644 --- a/apps/openmw/mwgui/widgets.cpp +++ b/apps/openmw/mwgui/widgets.cpp @@ -195,18 +195,18 @@ namespace MWGui::Widgets const ESM::Spell* spell = store.get().search(mId); MYGUI_ASSERT(spell, "spell with id '" << mId << "' not found"); - for (const ESM::ENAMstruct& effectInfo : spell->mEffects.mList) + for (const ESM::IndexedENAMstruct& effectInfo : spell->mEffects.mList) { MWSpellEffectPtr effect = creator->createWidget("MW_EffectImage", coord, MyGUI::Align::Default); SpellEffectParams params; - params.mEffectID = effectInfo.mEffectID; - params.mSkill = ESM::Skill::indexToRefId(effectInfo.mSkill); - params.mAttribute = ESM::Attribute::indexToRefId(effectInfo.mAttribute); - params.mDuration = effectInfo.mDuration; - params.mMagnMin = effectInfo.mMagnMin; - params.mMagnMax = effectInfo.mMagnMax; - params.mRange = effectInfo.mRange; + params.mEffectID = effectInfo.mData.mEffectID; + params.mSkill = ESM::Skill::indexToRefId(effectInfo.mData.mSkill); + params.mAttribute = ESM::Attribute::indexToRefId(effectInfo.mData.mAttribute); + params.mDuration = effectInfo.mData.mDuration; + params.mMagnMin = effectInfo.mData.mMagnMin; + params.mMagnMax = effectInfo.mData.mMagnMax; + params.mRange = effectInfo.mData.mRange; params.mIsConstant = (flags & MWEffectList::EF_Constant) != 0; params.mNoTarget = (flags & MWEffectList::EF_NoTarget); params.mNoMagnitude = (flags & MWEffectList::EF_NoMagnitude); @@ -308,17 +308,17 @@ namespace MWGui::Widgets SpellEffectList MWEffectList::effectListFromESM(const ESM::EffectList* effects) { SpellEffectList result; - for (const ESM::ENAMstruct& effectInfo : effects->mList) + for (const ESM::IndexedENAMstruct& effectInfo : effects->mList) { SpellEffectParams params; - params.mEffectID = effectInfo.mEffectID; - params.mSkill = ESM::Skill::indexToRefId(effectInfo.mSkill); - params.mAttribute = ESM::Attribute::indexToRefId(effectInfo.mAttribute); - params.mDuration = effectInfo.mDuration; - params.mMagnMin = effectInfo.mMagnMin; - params.mMagnMax = effectInfo.mMagnMax; - params.mRange = effectInfo.mRange; - params.mArea = effectInfo.mArea; + params.mEffectID = effectInfo.mData.mEffectID; + params.mSkill = ESM::Skill::indexToRefId(effectInfo.mData.mSkill); + params.mAttribute = ESM::Attribute::indexToRefId(effectInfo.mData.mAttribute); + params.mDuration = effectInfo.mData.mDuration; + params.mMagnMin = effectInfo.mData.mMagnMin; + params.mMagnMax = effectInfo.mData.mMagnMax; + params.mRange = effectInfo.mData.mRange; + params.mArea = effectInfo.mData.mArea; result.push_back(params); } return result; diff --git a/apps/openmw/mwlua/magicbindings.cpp b/apps/openmw/mwlua/magicbindings.cpp index 1e3cb2ab69..0e2d5217ae 100644 --- a/apps/openmw/mwlua/magicbindings.cpp +++ b/apps/openmw/mwlua/magicbindings.cpp @@ -13,12 +13,14 @@ #include #include #include +#include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/activespells.hpp" +#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/magiceffects.hpp" #include "../mwmechanics/spellutil.hpp" @@ -144,7 +146,7 @@ namespace sol { }; template <> - struct is_automagical : std::false_type + struct is_automagical : std::false_type { }; template <> @@ -192,6 +194,26 @@ namespace MWLua return ESM::RefId::deserializeText(LuaUtil::cast(recordOrId)); } + static const ESM::Spell* toSpell(const sol::object& spellOrId) + { + if (spellOrId.is()) + return spellOrId.as(); + else + { + auto& store = MWBase::Environment::get().getWorld()->getStore(); + auto refId = ESM::RefId::deserializeText(LuaUtil::cast(spellOrId)); + return store.get().find(refId); + } + } + + static sol::table effectParamsListToTable(sol::state_view& lua, const std::vector& effects) + { + sol::table res(lua, sol::create); + for (size_t i = 0; i < effects.size(); ++i) + res[i + 1] = effects[i]; // ESM::IndexedENAMstruct (effect params) + return res; + } + sol::table initCoreMagicBindings(const Context& context) { sol::state_view& lua = context.mLua->sol(); @@ -314,12 +336,12 @@ namespace MWLua spellT["name"] = sol::readonly_property([](const ESM::Spell& rec) -> std::string_view { return rec.mName; }); spellT["type"] = sol::readonly_property([](const ESM::Spell& rec) -> int { return rec.mData.mType; }); spellT["cost"] = sol::readonly_property([](const ESM::Spell& rec) -> int { return rec.mData.mCost; }); - spellT["effects"] = sol::readonly_property([&lua](const ESM::Spell& rec) -> sol::table { - sol::table res(lua, sol::create); - for (size_t i = 0; i < rec.mEffects.mList.size(); ++i) - res[i + 1] = rec.mEffects.mList[i]; // ESM::ENAMstruct (effect params) - return res; - }); + spellT["alwaysSucceedFlag"] = sol::readonly_property( + [](const ESM::Spell& rec) -> bool { return !!(rec.mData.mFlags & ESM::Spell::F_Always); }); + spellT["autocalcFlag"] = sol::readonly_property( + [](const ESM::Spell& rec) -> bool { return !!(rec.mData.mFlags & ESM::Spell::F_Autocalc); }); + spellT["effects"] = sol::readonly_property( + [&lua](const ESM::Spell& rec) -> sol::table { return effectParamsListToTable(lua, rec.mEffects.mList); }); // Enchantment record auto enchantT = lua.new_usertype("ESM3_Enchantment"); @@ -334,46 +356,49 @@ namespace MWLua enchantT["charge"] = sol::readonly_property([](const ESM::Enchantment& rec) -> int { return rec.mData.mCharge; }); enchantT["effects"] = sol::readonly_property([&lua](const ESM::Enchantment& rec) -> sol::table { - sol::table res(lua, sol::create); - for (size_t i = 0; i < rec.mEffects.mList.size(); ++i) - res[i + 1] = rec.mEffects.mList[i]; // ESM::ENAMstruct (effect params) - return res; + return effectParamsListToTable(lua, rec.mEffects.mList); }); // Effect params - auto effectParamsT = lua.new_usertype("ESM3_EffectParams"); - effectParamsT[sol::meta_function::to_string] = [magicEffectStore](const ESM::ENAMstruct& params) { - const ESM::MagicEffect* const rec = magicEffectStore->find(params.mEffectID); + auto effectParamsT = lua.new_usertype("ESM3_EffectParams"); + effectParamsT[sol::meta_function::to_string] = [magicEffectStore](const ESM::IndexedENAMstruct& params) { + const ESM::MagicEffect* const rec = magicEffectStore->find(params.mData.mEffectID); return "ESM3_EffectParams[" + ESM::MagicEffect::indexToGmstString(rec->mIndex) + "]"; }; - effectParamsT["effect"] - = sol::readonly_property([magicEffectStore](const ESM::ENAMstruct& params) -> const ESM::MagicEffect* { - return magicEffectStore->find(params.mEffectID); - }); + effectParamsT["effect"] = sol::readonly_property( + [magicEffectStore](const ESM::IndexedENAMstruct& params) -> const ESM::MagicEffect* { + return magicEffectStore->find(params.mData.mEffectID); + }); + effectParamsT["id"] = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> std::string { + auto name = ESM::MagicEffect::indexToName(params.mData.mEffectID); + return Misc::StringUtils::lowerCase(name); + }); effectParamsT["affectedSkill"] - = sol::readonly_property([](const ESM::ENAMstruct& params) -> sol::optional { - ESM::RefId id = ESM::Skill::indexToRefId(params.mSkill); + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> sol::optional { + ESM::RefId id = ESM::Skill::indexToRefId(params.mData.mSkill); if (!id.empty()) return id.serializeText(); return sol::nullopt; }); effectParamsT["affectedAttribute"] - = sol::readonly_property([](const ESM::ENAMstruct& params) -> sol::optional { - ESM::RefId id = ESM::Attribute::indexToRefId(params.mAttribute); + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> sol::optional { + ESM::RefId id = ESM::Attribute::indexToRefId(params.mData.mAttribute); if (!id.empty()) return id.serializeText(); return sol::nullopt; }); effectParamsT["range"] - = sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mRange; }); + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mRange; }); effectParamsT["area"] - = sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mArea; }); + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mArea; }); effectParamsT["magnitudeMin"] - = sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mMagnMin; }); + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mMagnMin; }); effectParamsT["magnitudeMax"] - = sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mMagnMax; }); - effectParamsT["duration"] - = sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mDuration; }); + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mMagnMax; }); + effectParamsT["duration"] = sol::readonly_property( + [](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mDuration; }); + effectParamsT["index"] + = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mIndex; }); // MagicEffect record auto magicEffectT = context.mLua->sol().new_usertype("ESM3_MagicEffect"); @@ -394,12 +419,22 @@ namespace MWLua magicEffectT["continuousVfx"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> bool { return (rec.mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; }); - magicEffectT["castingStatic"] = sol::readonly_property( + magicEffectT["areaSound"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mAreaSound.serializeText(); }); + magicEffectT["boltSound"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mBoltSound.serializeText(); }); + magicEffectT["castSound"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mCastSound.serializeText(); }); + magicEffectT["hitSound"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mHitSound.serializeText(); }); + magicEffectT["areaStatic"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mArea.serializeText(); }); + magicEffectT["boltStatic"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mBolt.serializeText(); }); + magicEffectT["castStatic"] = sol::readonly_property( [](const ESM::MagicEffect& rec) -> std::string { return rec.mCasting.serializeText(); }); magicEffectT["hitStatic"] = sol::readonly_property( [](const ESM::MagicEffect& rec) -> std::string { return rec.mHit.serializeText(); }); - magicEffectT["areaStatic"] = sol::readonly_property( - [](const ESM::MagicEffect& rec) -> std::string { return rec.mArea.serializeText(); }); magicEffectT["name"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> std::string_view { return MWBase::Environment::get() .getWorld() @@ -415,8 +450,20 @@ namespace MWLua magicEffectT["color"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> Misc::Color { return Misc::Color(rec.mData.mRed / 255.f, rec.mData.mGreen / 255.f, rec.mData.mBlue / 255.f, 1.f); }); + magicEffectT["hasDuration"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> bool { return !(rec.mData.mFlags & ESM::MagicEffect::NoDuration); }); + magicEffectT["hasMagnitude"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> bool { return !(rec.mData.mFlags & ESM::MagicEffect::NoMagnitude); }); + // TODO: Not self-explanatory. Needs either a better name or documentation. The description in + // loadmgef.hpp is uninformative. + magicEffectT["isAppliedOnce"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> bool { return rec.mData.mFlags & ESM::MagicEffect::AppliedOnce; }); magicEffectT["harmful"] = sol::readonly_property( [](const ESM::MagicEffect& rec) -> bool { return rec.mData.mFlags & ESM::MagicEffect::Harmful; }); + magicEffectT["casterLinked"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> bool { return rec.mData.mFlags & ESM::MagicEffect::CasterLinked; }); + magicEffectT["nonRecastable"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> bool { return rec.mData.mFlags & ESM::MagicEffect::NonRecastable; }); // TODO: Should we expose it? What happens if a spell has several effects with different projectileSpeed? // magicEffectT["projectileSpeed"] @@ -430,6 +477,8 @@ namespace MWLua auto name = ESM::MagicEffect::indexToName(effect.mEffectId); return Misc::StringUtils::lowerCase(name); }); + activeSpellEffectT["index"] + = sol::readonly_property([](const ESM::ActiveEffect& effect) -> int { return effect.mEffectIndex; }); activeSpellEffectT["name"] = sol::readonly_property([](const ESM::ActiveEffect& effect) -> std::string { return MWMechanics::EffectKey(effect.mEffectId, effect.getSkillOrAttribute()).toString(); }); @@ -493,12 +542,13 @@ namespace MWLua auto activeSpellT = context.mLua->sol().new_usertype("ActiveSpellParams"); activeSpellT[sol::meta_function::to_string] = [](const ActiveSpell& activeSpell) { - return "ActiveSpellParams[" + activeSpell.mParams.getId().serializeText() + "]"; + return "ActiveSpellParams[" + activeSpell.mParams.getSourceSpellId().serializeText() + "]"; }; activeSpellT["name"] = sol::readonly_property( [](const ActiveSpell& activeSpell) -> std::string_view { return activeSpell.mParams.getDisplayName(); }); - activeSpellT["id"] = sol::readonly_property( - [](const ActiveSpell& activeSpell) -> std::string { return activeSpell.mParams.getId().serializeText(); }); + activeSpellT["id"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> std::string { + return activeSpell.mParams.getSourceSpellId().serializeText(); + }); activeSpellT["item"] = sol::readonly_property([&lua](const ActiveSpell& activeSpell) -> sol::object { auto item = activeSpell.mParams.getItem(); if (!item.isSet()) @@ -535,6 +585,21 @@ namespace MWLua } return res; }); + activeSpellT["fromEquipment"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> bool { + return activeSpell.mParams.hasFlag(ESM::ActiveSpells::Flag_Equipment); + }); + activeSpellT["temporary"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> bool { + return activeSpell.mParams.hasFlag(ESM::ActiveSpells::Flag_Temporary); + }); + activeSpellT["affectsBaseValues"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> bool { + return activeSpell.mParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues); + }); + activeSpellT["stackable"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> bool { + return activeSpell.mParams.hasFlag(ESM::ActiveSpells::Flag_Stackable); + }); + activeSpellT["activeSpellId"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> std::string { + return activeSpell.mParams.getActiveSpellId().serializeText(); + }); auto activeEffectT = context.mLua->sol().new_usertype("ActiveEffect"); @@ -573,6 +638,78 @@ namespace MWLua return LuaUtil::makeReadOnly(magicApi); } + static std::pair> getNameAndMagicEffects( + const MWWorld::Ptr& actor, ESM::RefId id, const sol::table& effects, bool quiet) + { + std::vector effectIndexes; + + for (const auto& entry : effects) + { + if (entry.second.is()) + effectIndexes.push_back(entry.second.as()); + else if (entry.second.is()) + throw std::runtime_error("Error: Adding effects as enam structs is not implemented, use indexes."); + else + throw std::runtime_error("Unexpected entry in 'effects' table while trying to add to active effects"); + } + + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); + + auto getEffectsFromIndexes = [&](const ESM::EffectList& effects) { + std::vector enams; + for (auto index : effectIndexes) + enams.push_back(effects.mList.at(index)); + return enams; + }; + + auto getNameAndEffects = [&](auto* record) { + return std::pair>( + record->mName, getEffectsFromIndexes(record->mEffects)); + }; + auto getNameAndEffectsEnch = [&](auto* record) { + auto* enchantment = esmStore.get().find(record->mEnchant); + return std::pair>( + record->mName, getEffectsFromIndexes(enchantment->mEffects)); + }; + switch (esmStore.find(id)) + { + case ESM::REC_ALCH: + return getNameAndEffects(esmStore.get().find(id)); + case ESM::REC_INGR: + { + // Ingredients are a special case as their effect list is calculated on consumption. + const ESM::Ingredient* ingredient = esmStore.get().find(id); + std::vector enams; + quiet = quiet || actor != MWMechanics::getPlayer(); + for (uint32_t i = 0; i < effectIndexes.size(); i++) + { + if (auto effect = MWMechanics::rollIngredientEffect(actor, ingredient, effectIndexes[i])) + enams.push_back(effect->mList[0]); + } + if (enams.empty() && !quiet) + { + // "X has no effect on you" + std::string message = esmStore.get().find("sNotifyMessage50")->mValue.getString(); + message = Misc::StringUtils::format(message, ingredient->mName); + MWBase::Environment::get().getWindowManager()->messageBox(message); + } + return { ingredient->mName, std::move(enams) }; + } + case ESM::REC_ARMO: + return getNameAndEffectsEnch(esmStore.get().find(id)); + case ESM::REC_BOOK: + return getNameAndEffectsEnch(esmStore.get().find(id)); + case ESM::REC_CLOT: + return getNameAndEffectsEnch(esmStore.get().find(id)); + case ESM::REC_WEAP: + return getNameAndEffectsEnch(esmStore.get().find(id)); + default: + // esmStore.find doesn't find REC_SPELs + case ESM::REC_SPEL: + return getNameAndEffects(esmStore.get().find(id)); + } + } + void addActorMagicBindings(sol::table& actor, const Context& context) { const MWWorld::Store* spellStore @@ -731,6 +868,16 @@ namespace MWLua }); }; + // types.Actor.spells(o):canUsePower() + spellsT["canUsePower"] = [](const ActorSpells& spells, const sol::object& spellOrId) -> bool { + if (spells.mActor.isLObject()) + throw std::runtime_error("Local scripts can modify only spells of the actor they are attached to."); + auto* spell = toSpell(spellOrId); + if (auto* store = spells.getStore()) + return store->canUsePower(spell); + return false; + }; + // pairs(types.Actor.activeSpells(o)) activeSpellsT["__pairs"] = [](sol::this_state ts, ActorActiveSpells& self) { sol::state_view lua(ts); @@ -738,7 +885,7 @@ namespace MWLua return sol::as_function([lua, self]() mutable -> std::pair { if (!self.isEnd()) { - auto id = sol::make_object(lua, self.mIterator->getId().serializeText()); + auto id = sol::make_object(lua, self.mIterator->getSourceSpellId().serializeText()); auto params = sol::make_object(lua, ActiveSpell{ self.mActor, *self.mIterator }); self.advance(); return { id, params }; @@ -762,14 +909,97 @@ namespace MWLua }; // types.Actor.activeSpells(o):remove(id) - activeSpellsT["remove"] = [](const ActorActiveSpells& spells, const sol::object& spellOrId) { + activeSpellsT["remove"] = [context](const ActorActiveSpells& spells, std::string_view idStr) { + if (spells.isLObject()) + throw std::runtime_error("Local scripts can modify effect only on the actor they are attached to."); + + context.mLuaManager->addAction([spells = spells, id = ESM::RefId::deserializeText(idStr)]() { + if (auto* store = spells.getStore()) + { + auto it = store->getActiveSpellById(id); + if (it != store->end()) + { + if (it->hasFlag(ESM::ActiveSpells::Flag_Temporary)) + store->removeEffectsByActiveSpellId(spells.mActor.ptr(), id); + else + throw std::runtime_error("Can only remove temporary effects."); + } + } + }); + }; + + // types.Actor.activeSpells(o):add(id, spellid, effects, options) + activeSpellsT["add"] = [](const ActorActiveSpells& spells, const sol::table& options) { if (spells.isLObject()) throw std::runtime_error("Local scripts can modify effect only on the actor they are attached to."); - auto id = toSpellId(spellOrId); if (auto* store = spells.getStore()) { - store->removeEffects(spells.mActor.ptr(), id); + ESM::RefId id = ESM::RefId::deserializeText(options.get("id")); + sol::optional item = options.get>("item"); + ESM::RefNum itemId; + if (item) + itemId = item->id(); + sol::optional caster = options.get>("caster"); + bool stackable = options.get_or("stackable", false); + bool ignoreReflect = options.get_or("ignoreReflect", false); + bool ignoreSpellAbsorption = options.get_or("ignoreSpellAbsorption", false); + bool ignoreResistances = options.get_or("ignoreResistances", false); + sol::table effects = options.get("effects"); + bool quiet = options.get_or("quiet", false); + if (effects.empty()) + throw std::runtime_error("Error: Parameter 'effects': cannot be an empty list/table"); + const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); + auto [name, enams] = getNameAndMagicEffects(spells.mActor.ptr(), id, effects, quiet); + name = options.get_or("name", name); + + MWWorld::Ptr casterPtr; + if (caster) + casterPtr = caster->ptrOrEmpty(); + + bool affectsHealth = false; + MWMechanics::ActiveSpells::ActiveSpellParams params(casterPtr, id, name, itemId); + params.setFlag(ESM::ActiveSpells::Flag_Lua); + params.setFlag(ESM::ActiveSpells::Flag_Temporary); + if (stackable) + params.setFlag(ESM::ActiveSpells::Flag_Stackable); + + for (auto enam : enams) + { + const ESM::MagicEffect* mgef = esmStore.get().find(enam.mData.mEffectID); + MWMechanics::ActiveSpells::ActiveEffect effect; + effect.mEffectId = enam.mData.mEffectID; + effect.mArg = MWMechanics::EffectKey(enam.mData).mArg; + effect.mMagnitude = 0.f; + effect.mMinMagnitude = enam.mData.mMagnMin; + effect.mMaxMagnitude = enam.mData.mMagnMax; + effect.mEffectIndex = enam.mIndex; + effect.mFlags = ESM::ActiveEffect::Flag_None; + if (ignoreReflect) + effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Reflect; + if (ignoreSpellAbsorption) + effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; + if (ignoreResistances) + effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Resistances; + + bool hasDuration = !(mgef->mData.mFlags & ESM::MagicEffect::NoDuration); + effect.mDuration = hasDuration ? static_cast(enam.mData.mDuration) : 1.f; + + bool appliedOnce = mgef->mData.mFlags & ESM::MagicEffect::AppliedOnce; + if (!appliedOnce) + effect.mDuration = std::max(1.f, effect.mDuration); + effect.mTimeLeft = effect.mDuration; + params.getEffects().emplace_back(effect); + + affectsHealth = affectsHealth || mgef->mData.mFlags & ESM::MagicEffect::Harmful + || effect.mEffectId == ESM::MagicEffect::RestoreHealth; + } + store->addSpell(params); + if (affectsHealth && casterPtr == MWMechanics::getPlayer()) + // If player is attempting to cast a harmful spell on or is healing a living target, show the + // target's HP bar. + // TODO: This should be moved to Lua once the HUD has been dehardcoded + MWBase::Environment::get().getWindowManager()->setEnemy(spells.mActor.ptr()); } }; diff --git a/apps/openmw/mwlua/types/ingredient.cpp b/apps/openmw/mwlua/types/ingredient.cpp index abfd2329ce..f7c3a8a050 100644 --- a/apps/openmw/mwlua/types/ingredient.cpp +++ b/apps/openmw/mwlua/types/ingredient.cpp @@ -47,15 +47,16 @@ namespace MWLua { if (rec.mData.mEffectID[i] < 0) continue; - ESM::ENAMstruct effect; - effect.mEffectID = rec.mData.mEffectID[i]; - effect.mSkill = rec.mData.mSkills[i]; - effect.mAttribute = rec.mData.mAttributes[i]; - effect.mRange = ESM::RT_Self; - effect.mArea = 0; - effect.mDuration = 0; - effect.mMagnMin = 0; - effect.mMagnMax = 0; + ESM::IndexedENAMstruct effect; + effect.mData.mEffectID = rec.mData.mEffectID[i]; + effect.mData.mSkill = rec.mData.mSkills[i]; + effect.mData.mAttribute = rec.mData.mAttributes[i]; + effect.mData.mRange = ESM::RT_Self; + effect.mData.mArea = 0; + effect.mData.mDuration = 0; + effect.mData.mMagnMin = 0; + effect.mData.mMagnMax = 0; + effect.mIndex = i; res[i + 1] = effect; } return res; diff --git a/apps/openmw/mwlua/types/potion.cpp b/apps/openmw/mwlua/types/potion.cpp index 50aca6d9e7..d686bdb1f7 100644 --- a/apps/openmw/mwlua/types/potion.cpp +++ b/apps/openmw/mwlua/types/potion.cpp @@ -46,7 +46,10 @@ namespace size_t numEffects = effectsTable.size(); potion.mEffects.mList.resize(numEffects); for (size_t i = 0; i < numEffects; ++i) - potion.mEffects.mList[i] = LuaUtil::cast(effectsTable[i + 1]); + { + potion.mEffects.mList[i] = LuaUtil::cast(effectsTable[i + 1]); + } + potion.mEffects.updateIndexes(); } return potion; } @@ -83,7 +86,7 @@ namespace MWLua record["effects"] = sol::readonly_property([context](const ESM::Potion& rec) -> sol::table { sol::table res(context.mLua->sol(), sol::create); for (size_t i = 0; i < rec.mEffects.mList.size(); ++i) - res[i + 1] = rec.mEffects.mList[i]; // ESM::ENAMstruct (effect params) + res[i + 1] = rec.mEffects.mList[i]; // ESM::IndexedENAMstruct (effect params) return res; }); } diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index 937e7a6658..2fb7df61c1 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -8,6 +8,7 @@ #include +#include #include #include #include @@ -49,16 +50,15 @@ namespace void addEffects( std::vector& effects, const ESM::EffectList& list, bool ignoreResistances = false) { - int currentEffectIndex = 0; for (const auto& enam : list.mList) { ESM::ActiveEffect effect; - effect.mEffectId = enam.mEffectID; - effect.mArg = MWMechanics::EffectKey(enam).mArg; + effect.mEffectId = enam.mData.mEffectID; + effect.mArg = MWMechanics::EffectKey(enam.mData).mArg; effect.mMagnitude = 0.f; - effect.mMinMagnitude = enam.mMagnMin; - effect.mMaxMagnitude = enam.mMagnMax; - effect.mEffectIndex = currentEffectIndex++; + effect.mMinMagnitude = enam.mData.mMagnMin; + effect.mMaxMagnitude = enam.mData.mMagnMax; + effect.mEffectIndex = enam.mIndex; effect.mFlags = ESM::ActiveEffect::Flag_None; if (ignoreResistances) effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Resistances; @@ -82,12 +82,13 @@ namespace MWMechanics mActiveSpells.mIterating = false; } - ActiveSpells::ActiveSpellParams::ActiveSpellParams(const CastSpell& cast, const MWWorld::Ptr& caster) - : mId(cast.mId) - , mDisplayName(cast.mSourceName) + ActiveSpells::ActiveSpellParams::ActiveSpellParams( + const MWWorld::Ptr& caster, const ESM::RefId& id, std::string_view sourceName, ESM::RefNum item) + : mSourceSpellId(id) + , mDisplayName(sourceName) , mCasterActorId(-1) - , mItem(cast.mItem) - , mType(cast.mType) + , mItem(item) + , mFlags() , mWorsenings(-1) { if (!caster.isEmpty() && caster.getClass().isActor()) @@ -96,48 +97,52 @@ namespace MWMechanics ActiveSpells::ActiveSpellParams::ActiveSpellParams( const ESM::Spell* spell, const MWWorld::Ptr& actor, bool ignoreResistances) - : mId(spell->mId) + : mSourceSpellId(spell->mId) , mDisplayName(spell->mName) , mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()) - , mType(spell->mData.mType == ESM::Spell::ST_Ability ? ESM::ActiveSpells::Type_Ability - : ESM::ActiveSpells::Type_Permanent) + , mFlags() , mWorsenings(-1) { assert(spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power); + setFlag(ESM::ActiveSpells::Flag_SpellStore); + if (spell->mData.mType == ESM::Spell::ST_Ability) + setFlag(ESM::ActiveSpells::Flag_AffectsBaseValues); addEffects(mEffects, spell->mEffects, ignoreResistances); } ActiveSpells::ActiveSpellParams::ActiveSpellParams( const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, const MWWorld::Ptr& actor) - : mId(item.getCellRef().getRefId()) + : mSourceSpellId(item.getCellRef().getRefId()) , mDisplayName(item.getClass().getName(item)) , mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()) , mItem(item.getCellRef().getRefNum()) - , mType(ESM::ActiveSpells::Type_Enchantment) + , mFlags() , mWorsenings(-1) { assert(enchantment->mData.mType == ESM::Enchantment::ConstantEffect); addEffects(mEffects, enchantment->mEffects); + setFlag(ESM::ActiveSpells::Flag_Equipment); } ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ESM::ActiveSpells::ActiveSpellParams& params) - : mId(params.mId) + : mActiveSpellId(params.mActiveSpellId) + , mSourceSpellId(params.mSourceSpellId) , mEffects(params.mEffects) , mDisplayName(params.mDisplayName) , mCasterActorId(params.mCasterActorId) , mItem(params.mItem) - , mType(params.mType) + , mFlags(params.mFlags) , mWorsenings(params.mWorsenings) , mNextWorsening({ params.mNextWorsening }) { } ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ActiveSpellParams& params, const MWWorld::Ptr& actor) - : mId(params.mId) + : mSourceSpellId(params.mSourceSpellId) , mDisplayName(params.mDisplayName) , mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()) , mItem(params.mItem) - , mType(params.mType) + , mFlags(params.mFlags) , mWorsenings(-1) { } @@ -145,17 +150,23 @@ namespace MWMechanics ESM::ActiveSpells::ActiveSpellParams ActiveSpells::ActiveSpellParams::toEsm() const { ESM::ActiveSpells::ActiveSpellParams params; - params.mId = mId; + params.mActiveSpellId = mActiveSpellId; + params.mSourceSpellId = mSourceSpellId; params.mEffects = mEffects; params.mDisplayName = mDisplayName; params.mCasterActorId = mCasterActorId; params.mItem = mItem; - params.mType = mType; + params.mFlags = mFlags; params.mWorsenings = mWorsenings; params.mNextWorsening = mNextWorsening.toEsm(); return params; } + void ActiveSpells::ActiveSpellParams::setFlag(ESM::ActiveSpells::Flags flag) + { + mFlags = static_cast(mFlags | flag); + } + void ActiveSpells::ActiveSpellParams::worsen() { ++mWorsenings; @@ -178,21 +189,31 @@ namespace MWMechanics { // Enchantment id is not stored directly. Instead the enchanted item is stored. const auto& store = MWBase::Environment::get().getESMStore(); - switch (store->find(mId)) + switch (store->find(mSourceSpellId)) { case ESM::REC_ARMO: - return store->get().find(mId)->mEnchant; + return store->get().find(mSourceSpellId)->mEnchant; case ESM::REC_BOOK: - return store->get().find(mId)->mEnchant; + return store->get().find(mSourceSpellId)->mEnchant; case ESM::REC_CLOT: - return store->get().find(mId)->mEnchant; + return store->get().find(mSourceSpellId)->mEnchant; case ESM::REC_WEAP: - return store->get().find(mId)->mEnchant; + return store->get().find(mSourceSpellId)->mEnchant; default: return {}; } } + const ESM::Spell* ActiveSpells::ActiveSpellParams::getSpell() const + { + return MWBase::Environment::get().getESMStore()->get().search(getSourceSpellId()); + } + + bool ActiveSpells::ActiveSpellParams::hasFlag(ESM::ActiveSpells::Flags flags) const + { + return static_cast(mFlags & flags) == flags; + } + void ActiveSpells::update(const MWWorld::Ptr& ptr, float duration) { if (mIterating) @@ -203,8 +224,7 @@ namespace MWMechanics // 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) + if (!spellIt->hasFlag(ESM::ActiveSpells::Flag_Temporary)) { ++spellIt; continue; @@ -244,7 +264,10 @@ namespace MWMechanics { if (spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power && !isSpellActive(spell->mId)) + { mSpells.emplace_back(ActiveSpellParams{ spell, ptr }); + mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); + } } bool updateSpellWindow = false; @@ -270,8 +293,8 @@ namespace MWMechanics if (std::find_if(mSpells.begin(), mSpells.end(), [&](const ActiveSpellParams& params) { return params.mItem == slot->getCellRef().getRefNum() - && params.mType == ESM::ActiveSpells::Type_Enchantment - && params.mId == slot->getCellRef().getRefId(); + && params.hasFlag(ESM::ActiveSpells::Flag_Equipment) + && params.mSourceSpellId == slot->getCellRef().getRefId(); }) != mSpells.end()) continue; @@ -279,8 +302,8 @@ namespace MWMechanics // invisibility manually purgeEffect(ptr, ESM::MagicEffect::Invisibility); applyPurges(ptr); - const ActiveSpellParams& params - = mSpells.emplace_back(ActiveSpellParams{ *slot, enchantment, ptr }); + ActiveSpellParams& params = mSpells.emplace_back(ActiveSpellParams{ *slot, enchantment, ptr }); + params.setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); for (const auto& effect : params.mEffects) MWMechanics::playEffects( ptr, *world->getStore().get().find(effect.mEffectId), playNonLooping); @@ -350,12 +373,11 @@ namespace MWMechanics continue; bool remove = false; - if (spellIt->mType == ESM::ActiveSpells::Type_Ability - || spellIt->mType == ESM::ActiveSpells::Type_Permanent) + if (spellIt->hasFlag(ESM::ActiveSpells::Flag_SpellStore)) { try { - remove = !spells.hasSpell(spellIt->mId); + remove = !spells.hasSpell(spellIt->mSourceSpellId); } catch (const std::runtime_error& e) { @@ -363,9 +385,9 @@ namespace MWMechanics Log(Debug::Error) << "Removing active effect: " << e.what(); } } - else if (spellIt->mType == ESM::ActiveSpells::Type_Enchantment) + else if (spellIt->hasFlag(ESM::ActiveSpells::Flag_Equipment)) { - // Remove constant effect enchantments that have been unequipped + // Remove effects tied to equipment that has been unequipped const auto& store = ptr.getClass().getInventoryStore(ptr); remove = true; for (int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++) @@ -411,11 +433,11 @@ namespace MWMechanics void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell) { - if (spell.mType != ESM::ActiveSpells::Type_Consumable) + if (!spell.hasFlag(ESM::ActiveSpells::Flag_Stackable)) { auto found = std::find_if(mSpells.begin(), mSpells.end(), [&](const auto& existing) { - return spell.mId == existing.mId && spell.mCasterActorId == existing.mCasterActorId - && spell.mItem == existing.mItem; + return spell.mSourceSpellId == existing.mSourceSpellId + && spell.mCasterActorId == existing.mCasterActorId && spell.mItem == existing.mItem; }); if (found != mSpells.end()) { @@ -428,6 +450,7 @@ namespace MWMechanics } } mSpells.emplace_back(spell); + mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); } ActiveSpells::ActiveSpells() @@ -445,10 +468,19 @@ namespace MWMechanics return mSpells.end(); } + ActiveSpells::TIterator ActiveSpells::getActiveSpellById(const ESM::RefId& id) + { + for (TIterator it = begin(); it != end(); it++) + if (it->getActiveSpellId() == id) + return it; + return end(); + } + bool ActiveSpells::isSpellActive(const ESM::RefId& id) const { - return std::find_if(mSpells.begin(), mSpells.end(), [&](const auto& spell) { return spell.mId == id; }) - != mSpells.end(); + return std::find_if(mSpells.begin(), mSpells.end(), [&](const auto& spell) { + return spell.mSourceSpellId == id; + }) != mSpells.end(); } bool ActiveSpells::isEnchantmentActive(const ESM::RefId& id) const @@ -557,9 +589,14 @@ namespace MWMechanics return removedCurrentSpell; } - void ActiveSpells::removeEffects(const MWWorld::Ptr& ptr, const ESM::RefId& id) + void ActiveSpells::removeEffectsBySourceSpellId(const MWWorld::Ptr& ptr, const ESM::RefId& id) + { + purge([=](const ActiveSpellParams& params) { return params.mSourceSpellId == id; }, ptr); + } + + void ActiveSpells::removeEffectsByActiveSpellId(const MWWorld::Ptr& ptr, const ESM::RefId& id) { - purge([=](const ActiveSpellParams& params) { return params.mId == id; }, ptr); + purge([=](const ActiveSpellParams& params) { return params.mActiveSpellId == id; }, ptr); } void ActiveSpells::purgeEffect(const MWWorld::Ptr& ptr, int effectId, ESM::RefId effectArg) @@ -604,19 +641,19 @@ namespace MWMechanics void ActiveSpells::readState(const ESM::ActiveSpells& state) { for (const ESM::ActiveSpells::ActiveSpellParams& spell : state.mSpells) + { mSpells.emplace_back(ActiveSpellParams{ spell }); + // Generate ID for older saves that didn't have any. + if (mSpells.back().getActiveSpellId().empty()) + mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); + } for (const ESM::ActiveSpells::ActiveSpellParams& spell : state.mQueue) mQueue.emplace_back(ActiveSpellParams{ spell }); } void ActiveSpells::unloadActor(const MWWorld::Ptr& ptr) { - purge( - [](const auto& spell) { - return spell.getType() == ESM::ActiveSpells::Type_Consumable - || spell.getType() == ESM::ActiveSpells::Type_Temporary; - }, - ptr); + purge([](const auto& spell) { return spell.hasFlag(ESM::ActiveSpells::Flag_Temporary); }, ptr); mQueue.clear(); } } diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index a505b8990a..e4fa60ddb6 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -33,12 +33,13 @@ namespace MWMechanics using ActiveEffect = ESM::ActiveEffect; class ActiveSpellParams { - ESM::RefId mId; + ESM::RefId mActiveSpellId; + ESM::RefId mSourceSpellId; std::vector mEffects; std::string mDisplayName; int mCasterActorId; ESM::RefNum mItem; - ESM::ActiveSpells::EffectType mType; + ESM::ActiveSpells::Flags mFlags; int mWorsenings; MWWorld::TimeStamp mNextWorsening; MWWorld::Ptr mSource; @@ -57,15 +58,17 @@ namespace MWMechanics friend class ActiveSpells; public: - ActiveSpellParams(const CastSpell& cast, const MWWorld::Ptr& caster); + ActiveSpellParams( + const MWWorld::Ptr& caster, const ESM::RefId& id, std::string_view sourceName, ESM::RefNum item); + + ESM::RefId getActiveSpellId() const { return mActiveSpellId; } + void setActiveSpellId(ESM::RefId id) { mActiveSpellId = id; } - const ESM::RefId& getId() const { return mId; } + const ESM::RefId& getSourceSpellId() const { return mSourceSpellId; } const std::vector& getEffects() const { return mEffects; } std::vector& getEffects() { return mEffects; } - ESM::ActiveSpells::EffectType getType() const { return mType; } - int getCasterActorId() const { return mCasterActorId; } int getWorsenings() const { return mWorsenings; } @@ -75,6 +78,10 @@ namespace MWMechanics ESM::RefNum getItem() const { return mItem; } ESM::RefId getEnchantment() const; + const ESM::Spell* getSpell() const; + bool hasFlag(ESM::ActiveSpells::Flags flags) const; + void setFlag(ESM::ActiveSpells::Flags flags); + // Increments worsenings count and sets the next timestamp void worsen(); @@ -93,6 +100,8 @@ namespace MWMechanics TIterator end() const; + TIterator getActiveSpellById(const ESM::RefId& id); + void update(const MWWorld::Ptr& ptr, float duration); private: @@ -132,7 +141,9 @@ namespace MWMechanics void addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor); /// Removes the active effects from this spell/potion/.. with \a id - void removeEffects(const MWWorld::Ptr& ptr, const ESM::RefId& id); + void removeEffectsBySourceSpellId(const MWWorld::Ptr& ptr, const ESM::RefId& id); + /// Removes the active effects of a specific active spell + void removeEffectsByActiveSpellId(const MWWorld::Ptr& ptr, const ESM::RefId& id); /// Remove all active effects with this effect id void purgeEffect(const MWWorld::Ptr& ptr, int effectId, ESM::RefId effectArg = {}); diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 506bf80bc4..addf62df34 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -1228,11 +1228,11 @@ namespace MWMechanics } } - void Actors::castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool manualSpell) const + void Actors::castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell) const { const auto iter = mIndex.find(ptr.mRef); if (iter != mIndex.end()) - iter->second->getCharacterController().castSpell(spellId, manualSpell); + iter->second->getCharacterController().castSpell(spellId, scriptedSpell); } bool Actors::isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) const diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 14c55c4e45..2821df43e6 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -67,7 +67,7 @@ namespace MWMechanics void resurrect(const MWWorld::Ptr& ptr) const; - void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool manualSpell = false) const; + void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell = false) const; void updateActor(const MWWorld::Ptr& old, const MWWorld::Ptr& ptr) const; ///< Updates an actor with a new Ptr diff --git a/apps/openmw/mwmechanics/aicast.cpp b/apps/openmw/mwmechanics/aicast.cpp index 249ca97326..6384d70c06 100644 --- a/apps/openmw/mwmechanics/aicast.cpp +++ b/apps/openmw/mwmechanics/aicast.cpp @@ -25,11 +25,11 @@ namespace MWMechanics } } -MWMechanics::AiCast::AiCast(const ESM::RefId& targetId, const ESM::RefId& spellId, bool manualSpell) +MWMechanics::AiCast::AiCast(const ESM::RefId& targetId, const ESM::RefId& spellId, bool scriptedSpell) : mTargetId(targetId) , mSpellId(spellId) , mCasting(false) - , mManual(manualSpell) + , mScripted(scriptedSpell) , mDistance(getInitialDistance(spellId)) { } @@ -49,7 +49,7 @@ bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::Charac if (target.isEmpty()) return true; - if (!mManual + if (!mScripted && !pathTo(actor, target.getRefData().getPosition().asVec3(), duration, characterController.getSupportedMovementDirections(), mDistance)) { @@ -85,7 +85,7 @@ bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::Charac if (!mCasting) { - MWBase::Environment::get().getMechanicsManager()->castSpell(actor, mSpellId, mManual); + MWBase::Environment::get().getMechanicsManager()->castSpell(actor, mSpellId, mScripted); mCasting = true; return false; } diff --git a/apps/openmw/mwmechanics/aicast.hpp b/apps/openmw/mwmechanics/aicast.hpp index 435458cc0f..649c5a4d34 100644 --- a/apps/openmw/mwmechanics/aicast.hpp +++ b/apps/openmw/mwmechanics/aicast.hpp @@ -15,7 +15,7 @@ namespace MWMechanics class AiCast final : public TypedAiPackage { public: - AiCast(const ESM::RefId& targetId, const ESM::RefId& spellId, bool manualSpell = false); + AiCast(const ESM::RefId& targetId, const ESM::RefId& spellId, bool scriptedSpell = false); bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; @@ -37,7 +37,7 @@ namespace MWMechanics const ESM::RefId mTargetId; const ESM::RefId mSpellId; bool mCasting; - const bool mManual; + const bool mScripted; const float mDistance; }; } diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 0b3c2b8bd2..2399961a3a 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -275,7 +275,7 @@ namespace MWMechanics if (!spellId.empty()) { const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().find(spellId); - if (spell->mEffects.mList.empty() || spell->mEffects.mList[0].mRange != ESM::RT_Target) + if (spell->mEffects.mList.empty() || spell->mEffects.mList[0].mData.mRange != ESM::RT_Target) canShout = false; } storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat, canShout); diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp index 563bd8b8cd..91d2a9bbb8 100644 --- a/apps/openmw/mwmechanics/aicombataction.cpp +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -355,14 +355,14 @@ namespace MWMechanics { const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().find(selectedSpellId); - for (std::vector::const_iterator effectIt = spell->mEffects.mList.begin(); + for (std::vector::const_iterator effectIt = spell->mEffects.mList.begin(); effectIt != spell->mEffects.mList.end(); ++effectIt) { - if (effectIt->mRange == ESM::RT_Target) + if (effectIt->mData.mRange == ESM::RT_Target) { const ESM::MagicEffect* effect = MWBase::Environment::get().getESMStore()->get().find( - effectIt->mEffectID); + effectIt->mData.mEffectID); dist = effect->mData.mSpeed; break; } @@ -375,14 +375,14 @@ namespace MWMechanics { const ESM::Enchantment* ench = MWBase::Environment::get().getESMStore()->get().find(enchId); - for (std::vector::const_iterator effectIt = ench->mEffects.mList.begin(); + for (std::vector::const_iterator effectIt = ench->mEffects.mList.begin(); effectIt != ench->mEffects.mList.end(); ++effectIt) { - if (effectIt->mRange == ESM::RT_Target) + if (effectIt->mData.mRange == ESM::RT_Target) { const ESM::MagicEffect* effect = MWBase::Environment::get().getESMStore()->get().find( - effectIt->mEffectID); + effectIt->mData.mEffectID); dist = effect->mData.mSpeed; break; } diff --git a/apps/openmw/mwmechanics/alchemy.cpp b/apps/openmw/mwmechanics/alchemy.cpp index 3adb399483..4be48296a9 100644 --- a/apps/openmw/mwmechanics/alchemy.cpp +++ b/apps/openmw/mwmechanics/alchemy.cpp @@ -262,13 +262,13 @@ const ESM::Potion* MWMechanics::Alchemy::getRecord(const ESM::Potion& toFind) co for (size_t i = 0; i < iter->mEffects.mList.size(); ++i) { - const ESM::ENAMstruct& first = iter->mEffects.mList[i]; + const ESM::IndexedENAMstruct& first = iter->mEffects.mList[i]; const ESM::ENAMstruct& second = mEffects[i]; - if (first.mEffectID != second.mEffectID || first.mArea != second.mArea || first.mRange != second.mRange - || first.mSkill != second.mSkill || first.mAttribute != second.mAttribute - || first.mMagnMin != second.mMagnMin || first.mMagnMax != second.mMagnMax - || first.mDuration != second.mDuration) + if (first.mData.mEffectID != second.mEffectID || first.mData.mArea != second.mArea + || first.mData.mRange != second.mRange || first.mData.mSkill != second.mSkill + || first.mData.mAttribute != second.mAttribute || first.mData.mMagnMin != second.mMagnMin + || first.mData.mMagnMax != second.mMagnMax || first.mData.mDuration != second.mDuration) { mismatch = true; break; @@ -324,7 +324,7 @@ void MWMechanics::Alchemy::addPotion(const std::string& name) newRecord.mModel = "m\\misc_potion_" + std::string(meshes[index]) + "_01.nif"; newRecord.mIcon = "m\\tx_potion_" + std::string(meshes[index]) + "_01.dds"; - newRecord.mEffects.mList = mEffects; + newRecord.mEffects.populate(mEffects); const ESM::Potion* record = getRecord(newRecord); if (!record) diff --git a/apps/openmw/mwmechanics/autocalcspell.cpp b/apps/openmw/mwmechanics/autocalcspell.cpp index a2f6d479f1..5bab25fbe5 100644 --- a/apps/openmw/mwmechanics/autocalcspell.cpp +++ b/apps/openmw/mwmechanics/autocalcspell.cpp @@ -221,7 +221,7 @@ namespace MWMechanics for (const auto& spellEffect : spell->mEffects.mList) { const ESM::MagicEffect* magicEffect - = MWBase::Environment::get().getESMStore()->get().find(spellEffect.mEffectID); + = MWBase::Environment::get().getESMStore()->get().find(spellEffect.mData.mEffectID); static const int iAutoSpellAttSkillMin = MWBase::Environment::get() .getESMStore() ->get() @@ -230,7 +230,7 @@ namespace MWMechanics if ((magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill)) { - ESM::RefId skill = ESM::Skill::indexToRefId(spellEffect.mSkill); + ESM::RefId skill = ESM::Skill::indexToRefId(spellEffect.mData.mSkill); auto found = actorSkills.find(skill); if (found == actorSkills.end() || found->second.getBase() < iAutoSpellAttSkillMin) return false; @@ -238,7 +238,7 @@ namespace MWMechanics if ((magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute)) { - ESM::RefId attribute = ESM::Attribute::indexToRefId(spellEffect.mAttribute); + ESM::RefId attribute = ESM::Attribute::indexToRefId(spellEffect.mData.mAttribute); auto found = actorAttributes.find(attribute); if (found == actorAttributes.end() || found->second.getBase() < iAutoSpellAttSkillMin) return false; @@ -253,22 +253,22 @@ namespace MWMechanics { // Morrowind for some reason uses a formula slightly different from magicka cost calculation float minChance = std::numeric_limits::max(); - for (const ESM::ENAMstruct& effect : spell->mEffects.mList) + for (const ESM::IndexedENAMstruct& effect : spell->mEffects.mList) { const ESM::MagicEffect* magicEffect - = MWBase::Environment::get().getESMStore()->get().find(effect.mEffectID); + = MWBase::Environment::get().getESMStore()->get().find(effect.mData.mEffectID); int minMagn = 1; int maxMagn = 1; if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) { - minMagn = effect.mMagnMin; - maxMagn = effect.mMagnMax; + minMagn = effect.mData.mMagnMin; + maxMagn = effect.mData.mMagnMax; } int duration = 0; if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) - duration = effect.mDuration; + duration = effect.mData.mDuration; if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce)) duration = std::max(1, duration); @@ -281,10 +281,10 @@ namespace MWMechanics float x = 0.5 * (std::max(1, minMagn) + std::max(1, maxMagn)); x *= 0.1 * magicEffect->mData.mBaseCost; x *= 1 + duration; - x += 0.05 * std::max(1, effect.mArea) * magicEffect->mData.mBaseCost; + x += 0.05 * std::max(1, effect.mData.mArea) * magicEffect->mData.mBaseCost; x *= fEffectCostMult; - if (effect.mRange == ESM::RT_Target) + if (effect.mData.mRange == ESM::RT_Target) x *= 1.5f; float s = 0.f; diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index c0f6111a79..57e61e74aa 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -1155,8 +1155,8 @@ namespace MWMechanics else if (groupname == "spellcast" && action == mAttackType + " release") { if (mCanCast) - MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell); - mCastingManualSpell = false; + MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingScriptedSpell); + mCastingScriptedSpell = false; mCanCast = false; } else if (groupname == "containeropen" && action == "loot") @@ -1526,9 +1526,9 @@ namespace MWMechanics bool isMagicItem = false; // Play hand VFX and allow castSpell use (assuming an animation is going to be played) if - // spellcasting is successful. Manual spellcasting bypasses restrictions. + // spellcasting is successful. Scripted spellcasting bypasses restrictions. MWWorld::SpellCastState spellCastResult = MWWorld::SpellCastState::Success; - if (!mCastingManualSpell) + if (!mCastingScriptedSpell) spellCastResult = world->startSpellCast(mPtr); mCanCast = spellCastResult == MWWorld::SpellCastState::Success; @@ -1558,9 +1558,9 @@ namespace MWMechanics else if (!spellid.empty() && spellCastResult != MWWorld::SpellCastState::PowerAlreadyUsed) { world->breakInvisibility(mPtr); - MWMechanics::CastSpell cast(mPtr, {}, false, mCastingManualSpell); + MWMechanics::CastSpell cast(mPtr, {}, false, mCastingScriptedSpell); - const std::vector* effects{ nullptr }; + const std::vector* effects{ nullptr }; const MWWorld::ESMStore& store = world->getStore(); if (isMagicItem) { @@ -1579,7 +1579,7 @@ namespace MWMechanics if (mCanCast) { const ESM::MagicEffect* effect = store.get().find( - effects->back().mEffectID); // use last effect of list for color of VFX_Hands + effects->back().mData.mEffectID); // use last effect of list for color of VFX_Hands const ESM::Static* castStatic = world->getStore().get().find(ESM::RefId::stringRefId("VFX_Hands")); @@ -1593,7 +1593,7 @@ namespace MWMechanics "", false, "Bip01 R Hand", effect->mParticle); } // first effect used for casting animation - const ESM::ENAMstruct& firstEffect = effects->front(); + const ESM::ENAMstruct& firstEffect = effects->front().mData; std::string startKey; std::string stopKey; @@ -1602,9 +1602,9 @@ namespace MWMechanics startKey = "start"; stopKey = "stop"; if (mCanCast) - world->castSpell( - mPtr, mCastingManualSpell); // No "release" text key to use, so cast immediately - mCastingManualSpell = false; + world->castSpell(mPtr, + mCastingScriptedSpell); // No "release" text key to use, so cast immediately + mCastingScriptedSpell = false; mCanCast = false; } else @@ -2735,7 +2735,7 @@ namespace MWMechanics // Make sure we canceled the current attack or spellcasting, // because we disabled attack animations anyway. mCanCast = false; - mCastingManualSpell = false; + mCastingScriptedSpell = false; setAttackingOrSpell(false); if (mUpperBodyState != UpperBodyState::None) mUpperBodyState = UpperBodyState::WeaponEquipped; @@ -2887,7 +2887,7 @@ namespace MWMechanics bool CharacterController::isCastingSpell() const { - return mCastingManualSpell || mUpperBodyState == UpperBodyState::Casting; + return mCastingScriptedSpell || mUpperBodyState == UpperBodyState::Casting; } bool CharacterController::isReadyToBlock() const @@ -2941,10 +2941,10 @@ namespace MWMechanics mPtr.getClass().getCreatureStats(mPtr).setAttackingOrSpell(attackingOrSpell); } - void CharacterController::castSpell(const ESM::RefId& spellId, bool manualSpell) + void CharacterController::castSpell(const ESM::RefId& spellId, bool scriptedSpell) { setAttackingOrSpell(true); - mCastingManualSpell = manualSpell; + mCastingScriptedSpell = scriptedSpell; ActionSpell action = ActionSpell(spellId); action.prepare(mPtr); } diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index 430635fff5..8ead23f659 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -192,7 +192,7 @@ namespace MWMechanics bool mCanCast{ false }; - bool mCastingManualSpell{ false }; + bool mCastingScriptedSpell{ false }; bool mIsMovingBackward{ false }; osg::Vec2f mSmoothedSpeed; @@ -312,7 +312,7 @@ namespace MWMechanics bool isAttackingOrSpell() const; void setVisibility(float visibility) const; - void castSpell(const ESM::RefId& spellId, bool manualSpell = false); + void castSpell(const ESM::RefId& spellId, bool scriptedSpell = false); void setAIAttackType(std::string_view attackType); static std::string_view getRandomAttackType(); diff --git a/apps/openmw/mwmechanics/enchanting.cpp b/apps/openmw/mwmechanics/enchanting.cpp index 1de55b0c8d..7d0007f9e3 100644 --- a/apps/openmw/mwmechanics/enchanting.cpp +++ b/apps/openmw/mwmechanics/enchanting.cpp @@ -198,13 +198,13 @@ namespace MWMechanics float enchantmentCost = 0.f; float cost = 0.f; - for (const ESM::ENAMstruct& effect : mEffectList.mList) + for (const ESM::IndexedENAMstruct& effect : mEffectList.mList) { - float baseCost = (store.get().find(effect.mEffectID))->mData.mBaseCost; - int magMin = std::max(1, effect.mMagnMin); - int magMax = std::max(1, effect.mMagnMax); - int area = std::max(1, effect.mArea); - float duration = static_cast(effect.mDuration); + float baseCost = (store.get().find(effect.mData.mEffectID))->mData.mBaseCost; + int magMin = std::max(1, effect.mData.mMagnMin); + int magMax = std::max(1, effect.mData.mMagnMax); + int area = std::max(1, effect.mData.mArea); + float duration = static_cast(effect.mData.mDuration); if (mCastStyle == ESM::Enchantment::ConstantEffect) duration = fEnchantmentConstantDurationMult; @@ -212,7 +212,7 @@ namespace MWMechanics cost = std::max(1.f, cost); - if (effect.mRange == ESM::RT_Target) + if (effect.mData.mRange == ESM::RT_Target) cost *= 1.5f; enchantmentCost += precise ? cost : std::floor(cost); @@ -244,13 +244,7 @@ namespace MWMechanics for (int i = 0; i < static_cast(iter->mEffects.mList.size()); ++i) { - const ESM::ENAMstruct& first = iter->mEffects.mList[i]; - const ESM::ENAMstruct& second = toFind.mEffects.mList[i]; - - if (first.mEffectID != second.mEffectID || first.mArea != second.mArea || first.mRange != second.mRange - || first.mSkill != second.mSkill || first.mAttribute != second.mAttribute - || first.mMagnMin != second.mMagnMin || first.mMagnMax != second.mMagnMax - || first.mDuration != second.mDuration) + if (iter->mEffects.mList[i] != toFind.mEffects.mList[i]) { mismatch = true; break; diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 9319d030b8..47fba46c75 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -261,10 +261,10 @@ namespace MWMechanics mObjects.addObject(ptr); } - void MechanicsManager::castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool manualSpell) + void MechanicsManager::castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell) { if (ptr.getClass().isActor()) - mActors.castSpell(ptr, spellId, manualSpell); + mActors.castSpell(ptr, spellId, scriptedSpell); } void MechanicsManager::remove(const MWWorld::Ptr& ptr, bool keepActive) @@ -1978,11 +1978,7 @@ namespace MWMechanics // Transforming removes all temporary effects actor.getClass().getCreatureStats(actor).getActiveSpells().purge( - [](const auto& params) { - return params.getType() == ESM::ActiveSpells::Type_Consumable - || params.getType() == ESM::ActiveSpells::Type_Temporary; - }, - actor); + [](const auto& params) { return params.hasFlag(ESM::ActiveSpells::Flag_Temporary); }, actor); mActors.updateActor(actor, 0.f); if (werewolf) diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index 516b778f1e..bf94589309 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -202,7 +202,7 @@ namespace MWMechanics /// Is \a ptr casting spell or using weapon now? bool isAttackingOrSpell(const MWWorld::Ptr& ptr) const override; - void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool manualSpell = false) override; + void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell = false) override; void processChangedSettings(const Settings::CategorySettingVector& settings) override; diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 721131dcb0..2fd250f8c1 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -29,11 +29,11 @@ namespace MWMechanics { CastSpell::CastSpell( - const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile, const bool manualSpell) + const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile, const bool scriptedSpell) : mCaster(caster) , mTarget(target) , mFromProjectile(fromProjectile) - , mManualSpell(manualSpell) + , mScriptedSpell(scriptedSpell) { } @@ -41,21 +41,21 @@ namespace MWMechanics const ESM::EffectList& effects, const MWWorld::Ptr& ignore, ESM::RangeType rangeType) const { const auto world = MWBase::Environment::get().getWorld(); - std::map> toApply; + std::map> toApply; int index = -1; - for (const ESM::ENAMstruct& effectInfo : effects.mList) + for (const ESM::IndexedENAMstruct& effectInfo : effects.mList) { ++index; - const ESM::MagicEffect* effect = world->getStore().get().find(effectInfo.mEffectID); + const ESM::MagicEffect* effect = world->getStore().get().find(effectInfo.mData.mEffectID); - if (effectInfo.mRange != rangeType - || (effectInfo.mArea <= 0 && !ignore.isEmpty() && ignore.getClass().isActor())) + if (effectInfo.mData.mRange != rangeType + || (effectInfo.mData.mArea <= 0 && !ignore.isEmpty() && ignore.getClass().isActor())) continue; // Not right range type, or not area effect and hit an actor - if (mFromProjectile && effectInfo.mArea <= 0) + if (mFromProjectile && effectInfo.mData.mArea <= 0) continue; // Don't play explosion for projectiles with 0-area effects - if (!mFromProjectile && effectInfo.mRange == ESM::RT_Touch && !ignore.isEmpty() + if (!mFromProjectile && effectInfo.mData.mRange == ESM::RT_Touch && !ignore.isEmpty() && !ignore.getClass().isActor() && !ignore.getClass().hasToolTip(ignore) && (mCaster.isEmpty() || mCaster.getClass().isActor())) continue; // Don't play explosion for touch spells on non-activatable objects except when spell is from @@ -70,16 +70,16 @@ namespace MWMechanics const std::string& texture = effect->mParticle; - if (effectInfo.mArea <= 0) + if (effectInfo.mData.mArea <= 0) { - if (effectInfo.mRange == ESM::RT_Target) + if (effectInfo.mData.mRange == ESM::RT_Target) world->spawnEffect( Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel), texture, mHitPosition, 1.0f); continue; } else world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel), texture, mHitPosition, - static_cast(effectInfo.mArea * 2)); + static_cast(effectInfo.mData.mArea * 2)); // Play explosion sound (make sure to use NoTrack, since we will delete the projectile now) { @@ -95,7 +95,7 @@ namespace MWMechanics std::vector objects; static const int unitsPerFoot = ceil(Constants::UnitsPerFoot); MWBase::Environment::get().getMechanicsManager()->getObjectsInRange( - mHitPosition, static_cast(effectInfo.mArea * unitsPerFoot), objects); + mHitPosition, static_cast(effectInfo.mData.mArea * unitsPerFoot), objects); for (const MWWorld::Ptr& affected : objects) { // Ignore actors without collisions here, otherwise it will be possible to hit actors outside processing @@ -104,13 +104,6 @@ namespace MWMechanics continue; auto& list = toApply[affected]; - while (list.size() < static_cast(index)) - { - // Insert dummy effects to preserve indices - auto& dummy = list.emplace_back(effectInfo); - dummy.mRange = ESM::RT_Self; - assert(dummy.mRange != rangeType); - } list.push_back(effectInfo); } } @@ -151,45 +144,34 @@ namespace MWMechanics void CastSpell::inflict( const MWWorld::Ptr& target, const ESM::EffectList& effects, ESM::RangeType range, bool exploded) const { + bool targetIsDeadActor = false; const bool targetIsActor = !target.isEmpty() && target.getClass().isActor(); if (targetIsActor) { - // Early-out for characters that have departed. const auto& stats = target.getClass().getCreatureStats(target); if (stats.isDead() && stats.isDeathAnimationFinished()) - return; + targetIsDeadActor = true; } // If none of the effects need to apply, we can early-out bool found = false; bool containsRecastable = false; - std::vector magicEffects; - magicEffects.reserve(effects.mList.size()); const auto& store = MWBase::Environment::get().getESMStore()->get(); - for (const ESM::ENAMstruct& effect : effects.mList) + for (const ESM::IndexedENAMstruct& effect : effects.mList) { - if (effect.mRange == range) + if (effect.mData.mRange == range) { found = true; - const ESM::MagicEffect* magicEffect = store.find(effect.mEffectID); - // caster needs to be an actor for linked effects (e.g. Absorb) - if (magicEffect->mData.mFlags & ESM::MagicEffect::CasterLinked - && (mCaster.isEmpty() || !mCaster.getClass().isActor())) - { - magicEffects.push_back(nullptr); - continue; - } + const ESM::MagicEffect* magicEffect = store.find(effect.mData.mEffectID); if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NonRecastable)) containsRecastable = true; - magicEffects.push_back(magicEffect); } - else - magicEffects.push_back(nullptr); } if (!found) return; - ActiveSpells::ActiveSpellParams params(*this, mCaster); + ActiveSpells::ActiveSpellParams params(mCaster, mId, mSourceName, mItem); + params.setFlag(mFlags); bool castByPlayer = (!mCaster.isEmpty() && mCaster == getPlayer()); const ActiveSpells* targetSpells = nullptr; @@ -204,31 +186,32 @@ namespace MWMechanics return; } - for (size_t currentEffectIndex = 0; !target.isEmpty() && currentEffectIndex < effects.mList.size(); - ++currentEffectIndex) + for (auto& enam : effects.mList) { - const ESM::ENAMstruct& enam = effects.mList[currentEffectIndex]; - if (enam.mRange != range) + if (enam.mData.mRange != range) continue; - - const ESM::MagicEffect* magicEffect = magicEffects[currentEffectIndex]; + const ESM::MagicEffect* magicEffect = store.find(enam.mData.mEffectID); if (!magicEffect) continue; + // caster needs to be an actor for linked effects (e.g. Absorb) + if (magicEffect->mData.mFlags & ESM::MagicEffect::CasterLinked + && (mCaster.isEmpty() || !mCaster.getClass().isActor())) + continue; ActiveSpells::ActiveEffect effect; - effect.mEffectId = enam.mEffectID; - effect.mArg = MWMechanics::EffectKey(enam).mArg; + effect.mEffectId = enam.mData.mEffectID; + effect.mArg = MWMechanics::EffectKey(enam.mData).mArg; effect.mMagnitude = 0.f; - effect.mMinMagnitude = enam.mMagnMin; - effect.mMaxMagnitude = enam.mMagnMax; + effect.mMinMagnitude = enam.mData.mMagnMin; + effect.mMaxMagnitude = enam.mData.mMagnMax; effect.mTimeLeft = 0.f; - effect.mEffectIndex = static_cast(currentEffectIndex); + effect.mEffectIndex = enam.mIndex; effect.mFlags = ESM::ActiveEffect::Flag_None; - if (mManualSpell) + if (mScriptedSpell) effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Reflect; bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); - effect.mDuration = hasDuration ? static_cast(enam.mDuration) : 1.f; + effect.mDuration = hasDuration ? static_cast(enam.mData.mDuration) : 1.f; bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce; if (!appliedOnce) @@ -240,8 +223,8 @@ namespace MWMechanics params.getEffects().emplace_back(effect); bool effectAffectsHealth = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful - || enam.mEffectID == ESM::MagicEffect::RestoreHealth; - if (castByPlayer && target != mCaster && targetIsActor && effectAffectsHealth) + || enam.mData.mEffectID == ESM::MagicEffect::RestoreHealth; + if (castByPlayer && target != mCaster && targetIsActor && !targetIsDeadActor && effectAffectsHealth) { // If player is attempting to cast a harmful spell on or is healing a living target, show the target's // HP bar. @@ -262,7 +245,10 @@ namespace MWMechanics if (!params.getEffects().empty()) { if (targetIsActor) - target.getClass().getCreatureStats(target).getActiveSpells().addSpell(params); + { + if (!targetIsDeadActor) + target.getClass().getCreatureStats(target).getActiveSpells().addSpell(params); + } else { // Apply effects instantly. We can ignore effect deletion since the entire params object gets @@ -336,7 +322,7 @@ namespace MWMechanics ESM::RefId school = ESM::Skill::Alteration; if (!enchantment->mEffects.mList.empty()) { - short effectId = enchantment->mEffects.mList.front().mEffectID; + short effectId = enchantment->mEffects.mList.front().mData.mEffectID; const ESM::MagicEffect* magicEffect = store->get().find(effectId); school = magicEffect->mData.mSchool; } @@ -387,7 +373,8 @@ namespace MWMechanics { mSourceName = potion->mName; mId = potion->mId; - mType = ESM::ActiveSpells::Type_Consumable; + mFlags = static_cast( + ESM::ActiveSpells::Flag_Temporary | ESM::ActiveSpells::Flag_Stackable); inflict(mCaster, potion->mEffects, ESM::RT_Self); @@ -403,7 +390,7 @@ namespace MWMechanics bool godmode = mCaster == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - if (mCaster.getClass().isActor() && !mAlwaysSucceed && !mManualSpell) + if (mCaster.getClass().isActor() && !mAlwaysSucceed && !mScriptedSpell) { school = getSpellSchool(spell, mCaster); @@ -438,7 +425,7 @@ namespace MWMechanics stats.getSpells().usePower(spell); } - if (!mManualSpell && mCaster == getPlayer() && spellIncreasesSkill(spell)) + if (!mScriptedSpell && mCaster == getPlayer() && spellIncreasesSkill(spell)) mCaster.getClass().skillUsageSucceeded(mCaster, school, ESM::Skill::Spellcast_Success); // A non-actor doesn't play its spell cast effects from a character controller, so play them here @@ -458,62 +445,27 @@ namespace MWMechanics bool CastSpell::cast(const ESM::Ingredient* ingredient) { mId = ingredient->mId; - mType = ESM::ActiveSpells::Type_Consumable; + mFlags = static_cast( + ESM::ActiveSpells::Flag_Temporary | ESM::ActiveSpells::Flag_Stackable); mSourceName = ingredient->mName; - ESM::ENAMstruct effect; - effect.mEffectID = ingredient->mData.mEffectID[0]; - effect.mSkill = ingredient->mData.mSkills[0]; - effect.mAttribute = ingredient->mData.mAttributes[0]; - effect.mRange = ESM::RT_Self; - effect.mArea = 0; - - const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); - const auto magicEffect = store.get().find(effect.mEffectID); - const MWMechanics::CreatureStats& creatureStats = mCaster.getClass().getCreatureStats(mCaster); + auto effect = rollIngredientEffect(mCaster, ingredient, mCaster != getPlayer()); - float x = (mCaster.getClass().getSkill(mCaster, ESM::Skill::Alchemy) - + 0.2f * creatureStats.getAttribute(ESM::Attribute::Intelligence).getModified() - + 0.1f * creatureStats.getAttribute(ESM::Attribute::Luck).getModified()) - * creatureStats.getFatigueTerm(); - - auto& prng = MWBase::Environment::get().getWorld()->getPrng(); - int roll = Misc::Rng::roll0to99(prng); - if (roll > x) + if (effect) + inflict(mCaster, *effect, ESM::RT_Self); + else { // "X has no effect on you" - std::string message = store.get().find("sNotifyMessage50")->mValue.getString(); + std::string message = MWBase::Environment::get() + .getESMStore() + ->get() + .find("sNotifyMessage50") + ->mValue.getString(); message = Misc::StringUtils::format(message, ingredient->mName); MWBase::Environment::get().getWindowManager()->messageBox(message); return false; } - float magnitude = 0; - float y = roll / std::min(x, 100.f); - y *= 0.25f * x; - if (magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) - effect.mDuration = 1; - else - effect.mDuration = static_cast(y); - if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) - { - if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) - magnitude = floor((0.05f * y) / (0.1f * magicEffect->mData.mBaseCost)); - else - magnitude = floor(y / (0.1f * magicEffect->mData.mBaseCost)); - magnitude = std::max(1.f, magnitude); - } - else - magnitude = 1; - - effect.mMagnMax = static_cast(magnitude); - effect.mMagnMin = static_cast(magnitude); - - ESM::EffectList effects; - effects.mList.push_back(effect); - - inflict(mCaster, effects, ESM::RT_Self); - return true; } @@ -527,14 +479,14 @@ namespace MWMechanics playSpellCastingEffects(spell->mEffects.mList); } - void CastSpell::playSpellCastingEffects(const std::vector& effects) const + void CastSpell::playSpellCastingEffects(const std::vector& effects) const { const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); std::vector addedEffects; - for (const ESM::ENAMstruct& effectData : effects) + for (const ESM::IndexedENAMstruct& effectData : effects) { - const auto effect = store.get().find(effectData.mEffectID); + const auto effect = store.get().find(effectData.mData.mEffectID); const ESM::Static* castStatic; @@ -587,7 +539,7 @@ namespace MWMechanics } if (animation && !mCaster.getClass().isActor()) - animation->addSpellCastGlow(effect); + animation->addSpellCastGlow(effect->getColor()); addedEffects.push_back(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel)); diff --git a/apps/openmw/mwmechanics/spellcasting.hpp b/apps/openmw/mwmechanics/spellcasting.hpp index 8f10066e04..23d3b80713 100644 --- a/apps/openmw/mwmechanics/spellcasting.hpp +++ b/apps/openmw/mwmechanics/spellcasting.hpp @@ -26,7 +26,7 @@ namespace MWMechanics MWWorld::Ptr mCaster; // May be empty MWWorld::Ptr mTarget; // May be empty - void playSpellCastingEffects(const std::vector& effects) const; + void playSpellCastingEffects(const std::vector& effects) const; void explodeSpell(const ESM::EffectList& effects, const MWWorld::Ptr& ignore, ESM::RangeType rangeType) const; @@ -41,13 +41,13 @@ namespace MWMechanics 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.) + bool mScriptedSpell; // True if spell is casted from script and ignores some checks (mana level, success chance, + // etc.) ESM::RefNum mItem; - ESM::ActiveSpells::EffectType mType{ ESM::ActiveSpells::Type_Temporary }; + ESM::ActiveSpells::Flags mFlags{ ESM::ActiveSpells::Flag_Temporary }; CastSpell(const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile = false, - const bool manualSpell = false); + const bool scriptedSpell = false); bool cast(const ESM::Spell* spell); diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index 8c415949f5..96044ebc5b 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -289,7 +289,7 @@ namespace animation->addEffect(Misc::ResourceHelpers::correctMeshPath(absorbStatic->mModel), ESM::MagicEffect::indexToName(ESM::MagicEffect::SpellAbsorption), false); int spellCost = 0; - if (const ESM::Spell* spell = esmStore.get().search(spellParams.getId())) + if (const ESM::Spell* spell = esmStore.get().search(spellParams.getSourceSpellId())) { spellCost = MWMechanics::calcSpellCost(*spell); } @@ -314,8 +314,7 @@ namespace auto& stats = target.getClass().getCreatureStats(target); auto& magnitudes = stats.getMagicEffects(); // Apply reflect and spell absorption - if (target != caster && spellParams.getType() != ESM::ActiveSpells::Type_Enchantment - && spellParams.getType() != ESM::ActiveSpells::Type_Permanent) + if (target != caster && spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary)) { bool canReflect = !(magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable) && !(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Reflect) @@ -358,9 +357,8 @@ namespace // Apply resistances if (!(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Resistances)) { - const ESM::Spell* spell = nullptr; - if (spellParams.getType() == ESM::ActiveSpells::Type_Temporary) - spell = MWBase::Environment::get().getESMStore()->get().search(spellParams.getId()); + const ESM::Spell* spell + = spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary) ? spellParams.getSpell() : nullptr; float magnitudeMult = MWMechanics::getEffectMultiplier(effect.mEffectId, target, caster, spell, &magnitudes); if (magnitudeMult == 0) @@ -429,10 +427,9 @@ namespace MWMechanics // Dispel removes entire spells at once target.getClass().getCreatureStats(target).getActiveSpells().purge( [magnitude = effect.mMagnitude](const ActiveSpells::ActiveSpellParams& params) { - if (params.getType() == ESM::ActiveSpells::Type_Temporary) + if (params.hasFlag(ESM::ActiveSpells::Flag_Temporary)) { - const ESM::Spell* spell - = MWBase::Environment::get().getESMStore()->get().search(params.getId()); + const ESM::Spell* spell = params.getSpell(); if (spell && spell->mData.mType == ESM::Spell::ST_Spell) { auto& prng = MWBase::Environment::get().getWorld()->getPrng(); @@ -645,7 +642,7 @@ namespace MWMechanics else if (effect.mEffectId == ESM::MagicEffect::DamageFatigue) index = 2; // Damage "Dynamic" abilities reduce the base value - if (spellParams.getType() == ESM::ActiveSpells::Type_Ability) + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) modDynamicStat(target, index, -effect.mMagnitude); else { @@ -666,7 +663,7 @@ namespace MWMechanics else if (!godmode) { // Damage Skill abilities reduce base skill :todd: - if (spellParams.getType() == ESM::ActiveSpells::Type_Ability) + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) { auto& npcStats = target.getClass().getNpcStats(target); SkillValue& skill = npcStats.getSkill(effect.getSkillOrAttribute()); @@ -725,7 +722,7 @@ namespace MWMechanics case ESM::MagicEffect::FortifyHealth: case ESM::MagicEffect::FortifyMagicka: case ESM::MagicEffect::FortifyFatigue: - if (spellParams.getType() == ESM::ActiveSpells::Type_Ability) + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) modDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, effect.mMagnitude); else adjustDynamicStat( @@ -737,7 +734,7 @@ namespace MWMechanics break; case ESM::MagicEffect::FortifyAttribute: // Abilities affect base stats, but not for drain - if (spellParams.getType() == ESM::ActiveSpells::Type_Ability) + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) { auto& creatureStats = target.getClass().getCreatureStats(target); auto attribute = effect.getSkillOrAttribute(); @@ -757,7 +754,7 @@ namespace MWMechanics case ESM::MagicEffect::FortifySkill: if (!target.getClass().isNpc()) invalid = true; - else if (spellParams.getType() == ESM::ActiveSpells::Type_Ability) + else if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) { // Abilities affect base stats, but not for drain auto& npcStats = target.getClass().getNpcStats(target); @@ -922,7 +919,7 @@ namespace MWMechanics { MWRender::Animation* animation = world->getAnimation(target); if (animation) - animation->addSpellCastGlow(magicEffect); + animation->addSpellCastGlow(magicEffect->getColor()); int magnitude = static_cast(roll(effect)); if (target.getCellRef().getLockLevel() < magnitude) // If the door is not already locked to a higher value, lock it to spell magnitude @@ -947,7 +944,7 @@ namespace MWMechanics MWRender::Animation* animation = world->getAnimation(target); if (animation) - animation->addSpellCastGlow(magicEffect); + animation->addSpellCastGlow(magicEffect->getColor()); int magnitude = static_cast(roll(effect)); if (target.getCellRef().getLockLevel() <= magnitude) { @@ -985,7 +982,7 @@ namespace MWMechanics return { MagicApplicationResult::Type::APPLIED, receivedMagicDamage, affectedHealth }; auto& stats = target.getClass().getCreatureStats(target); auto& magnitudes = stats.getMagicEffects(); - if (spellParams.getType() != ESM::ActiveSpells::Type_Ability + if (!spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues) && !(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) { MagicApplicationResult::Type result @@ -998,10 +995,9 @@ namespace MWMechanics oldMagnitude = effect.mMagnitude; else { - if (spellParams.getType() != ESM::ActiveSpells::Type_Enchantment) - playEffects(target, *magicEffect, - spellParams.getType() == ESM::ActiveSpells::Type_Consumable - || spellParams.getType() == ESM::ActiveSpells::Type_Temporary); + if (!spellParams.hasFlag(ESM::ActiveSpells::Flag_Equipment) + && !spellParams.hasFlag(ESM::ActiveSpells::Flag_Lua)) + playEffects(target, *magicEffect, spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary)); if (effect.mEffectId == ESM::MagicEffect::Soultrap && !target.getClass().isNpc() && target.getType() == ESM::Creature::sRecordId && target.get()->mBase->mData.mSoul == 0 && caster == getPlayer()) @@ -1016,8 +1012,7 @@ namespace MWMechanics if (effect.mDuration != 0) { float mult = dt; - if (spellParams.getType() == ESM::ActiveSpells::Type_Consumable - || spellParams.getType() == ESM::ActiveSpells::Type_Temporary) + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary)) mult = std::min(effect.mTimeLeft, dt); effect.mMagnitude *= mult; } @@ -1195,7 +1190,7 @@ namespace MWMechanics case ESM::MagicEffect::FortifyHealth: case ESM::MagicEffect::FortifyMagicka: case ESM::MagicEffect::FortifyFatigue: - if (spellParams.getType() == ESM::ActiveSpells::Type_Ability) + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) modDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, -effect.mMagnitude); else adjustDynamicStat( @@ -1206,7 +1201,7 @@ namespace MWMechanics break; case ESM::MagicEffect::FortifyAttribute: // Abilities affect base stats, but not for drain - if (spellParams.getType() == ESM::ActiveSpells::Type_Ability) + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) { auto& creatureStats = target.getClass().getCreatureStats(target); auto attribute = effect.getSkillOrAttribute(); @@ -1222,7 +1217,7 @@ namespace MWMechanics break; case ESM::MagicEffect::FortifySkill: // Abilities affect base stats, but not for drain - if (spellParams.getType() == ESM::ActiveSpells::Type_Ability) + if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)) { auto& npcStats = target.getClass().getNpcStats(target); auto& skill = npcStats.getSkill(effect.getSkillOrAttribute()); diff --git a/apps/openmw/mwmechanics/spellpriority.cpp b/apps/openmw/mwmechanics/spellpriority.cpp index 776381e6b2..d1407c4cbd 100644 --- a/apps/openmw/mwmechanics/spellpriority.cpp +++ b/apps/openmw/mwmechanics/spellpriority.cpp @@ -34,7 +34,7 @@ namespace if (effectFilter == -1) { const ESM::Spell* spell - = MWBase::Environment::get().getESMStore()->get().search(it->getId()); + = MWBase::Environment::get().getESMStore()->get().search(it->getSourceSpellId()); if (!spell || spell->mData.mType != ESM::Spell::ST_Spell) continue; } @@ -67,7 +67,7 @@ namespace const MWMechanics::ActiveSpells& activeSpells = actor.getClass().getCreatureStats(actor).getActiveSpells(); for (MWMechanics::ActiveSpells::TIterator it = activeSpells.begin(); it != activeSpells.end(); ++it) { - if (it->getId() != spellId) + if (it->getSourceSpellId() != spellId) continue; const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it; @@ -85,7 +85,7 @@ namespace int actorId = caster.getClass().getCreatureStats(caster).getActorId(); const auto& active = target.getClass().getCreatureStats(target).getActiveSpells(); return std::find_if(active.begin(), active.end(), [&](const auto& spell) { - return spell.getCasterActorId() == actorId && spell.getId() == id; + return spell.getCasterActorId() == actorId && spell.getSourceSpellId() == id; }) != active.end(); } @@ -110,13 +110,13 @@ namespace MWMechanics int getRangeTypes(const ESM::EffectList& effects) { int types = 0; - for (std::vector::const_iterator it = effects.mList.begin(); it != effects.mList.end(); ++it) + for (const ESM::IndexedENAMstruct& effect : effects.mList) { - if (it->mRange == ESM::RT_Self) + if (effect.mData.mRange == ESM::RT_Self) types |= RangeTypes::Self; - else if (it->mRange == ESM::RT_Touch) + else if (effect.mData.mRange == ESM::RT_Touch) types |= RangeTypes::Touch; - else if (it->mRange == ESM::RT_Target) + else if (effect.mData.mRange == ESM::RT_Target) types |= RangeTypes::Target; } return types; @@ -735,12 +735,12 @@ namespace MWMechanics static const float fAIMagicSpellMult = gmst.find("fAIMagicSpellMult")->mValue.getFloat(); static const float fAIRangeMagicSpellMult = gmst.find("fAIRangeMagicSpellMult")->mValue.getFloat(); - for (const ESM::ENAMstruct& effect : list.mList) + for (const ESM::IndexedENAMstruct& effect : list.mList) { - float effectRating = rateEffect(effect, actor, enemy); + float effectRating = rateEffect(effect.mData, actor, enemy); if (useSpellMult) { - if (effect.mRange == ESM::RT_Target) + if (effect.mData.mRange == ESM::RT_Target) effectRating *= fAIRangeMagicSpellMult; else effectRating *= fAIMagicSpellMult; @@ -760,10 +760,10 @@ namespace MWMechanics float mult = fAIMagicSpellMult; - for (std::vector::const_iterator effectIt = spell->mEffects.mList.begin(); + for (std::vector::const_iterator effectIt = spell->mEffects.mList.begin(); effectIt != spell->mEffects.mList.end(); ++effectIt) { - if (effectIt->mRange == ESM::RT_Target) + if (effectIt->mData.mRange == ESM::RT_Target) { if (!MWBase::Environment::get().getWorld()->isSwimming(enemy)) mult = fAIRangeMagicSpellMult; diff --git a/apps/openmw/mwmechanics/spells.cpp b/apps/openmw/mwmechanics/spells.cpp index 12da7cdde8..26f0ebe4a2 100644 --- a/apps/openmw/mwmechanics/spells.cpp +++ b/apps/openmw/mwmechanics/spells.cpp @@ -174,7 +174,7 @@ namespace MWMechanics { for (const auto& effectIt : spell->mEffects.mList) { - if (effectIt.mEffectID == ESM::MagicEffect::Corprus) + if (effectIt.mData.mEffectID == ESM::MagicEffect::Corprus) { return true; } diff --git a/apps/openmw/mwmechanics/spellutil.cpp b/apps/openmw/mwmechanics/spellutil.cpp index 671939cb00..022aaec262 100644 --- a/apps/openmw/mwmechanics/spellutil.cpp +++ b/apps/openmw/mwmechanics/spellutil.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include "../mwbase/environment.hpp" @@ -23,13 +24,13 @@ namespace MWMechanics { float cost = 0; - for (const ESM::ENAMstruct& effect : list.mList) + for (const ESM::IndexedENAMstruct& effect : list.mList) { - float effectCost = std::max(0.f, MWMechanics::calcEffectCost(effect, nullptr, method)); + float effectCost = std::max(0.f, MWMechanics::calcEffectCost(effect.mData, nullptr, method)); // This is applied to the whole spell cost for each effect when // creating spells, but is only applied on the effect itself in TES:CS. - if (effect.mRange == ESM::RT_Target) + if (effect.mData.mRange == ESM::RT_Target) effectCost *= 1.5; cost += effectCost; @@ -158,25 +159,83 @@ namespace MWMechanics return potion.mData.mValue; } + std::optional rollIngredientEffect( + MWWorld::Ptr caster, const ESM::Ingredient* ingredient, uint32_t index) + { + if (index >= 4) + throw std::range_error("Index out of range"); + + ESM::ENAMstruct effect; + effect.mEffectID = ingredient->mData.mEffectID[index]; + effect.mSkill = ingredient->mData.mSkills[index]; + effect.mAttribute = ingredient->mData.mAttributes[index]; + effect.mRange = ESM::RT_Self; + effect.mArea = 0; + + if (effect.mEffectID < 0) + return std::nullopt; + + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + const auto magicEffect = store.get().find(effect.mEffectID); + const MWMechanics::CreatureStats& creatureStats = caster.getClass().getCreatureStats(caster); + + float x = (caster.getClass().getSkill(caster, ESM::Skill::Alchemy) + + 0.2f * creatureStats.getAttribute(ESM::Attribute::Intelligence).getModified() + + 0.1f * creatureStats.getAttribute(ESM::Attribute::Luck).getModified()) + * creatureStats.getFatigueTerm(); + + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + int roll = Misc::Rng::roll0to99(prng); + if (roll > x) + { + return std::nullopt; + } + + float magnitude = 0; + float y = roll / std::min(x, 100.f); + y *= 0.25f * x; + if (magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) + effect.mDuration = 1; + else + effect.mDuration = static_cast(y); + if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) + { + if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) + magnitude = floor((0.05f * y) / (0.1f * magicEffect->mData.mBaseCost)); + else + magnitude = floor(y / (0.1f * magicEffect->mData.mBaseCost)); + magnitude = std::max(1.f, magnitude); + } + else + magnitude = 1; + + effect.mMagnMax = static_cast(magnitude); + effect.mMagnMin = static_cast(magnitude); + + ESM::EffectList effects; + effects.mList.push_back({ effect, index }); + return effects; + } + float calcSpellBaseSuccessChance(const ESM::Spell* spell, const MWWorld::Ptr& actor, ESM::RefId* effectiveSchool) { // Morrowind for some reason uses a formula slightly different from magicka cost calculation float y = std::numeric_limits::max(); float lowestSkill = 0; - for (const ESM::ENAMstruct& effect : spell->mEffects.mList) + for (const ESM::IndexedENAMstruct& effect : spell->mEffects.mList) { - float x = static_cast(effect.mDuration); + float x = static_cast(effect.mData.mDuration); const auto magicEffect - = MWBase::Environment::get().getESMStore()->get().find(effect.mEffectID); + = MWBase::Environment::get().getESMStore()->get().find(effect.mData.mEffectID); if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce)) x = std::max(1.f, x); x *= 0.1f * magicEffect->mData.mBaseCost; - x *= 0.5f * (effect.mMagnMin + effect.mMagnMax); - x += effect.mArea * 0.05f * magicEffect->mData.mBaseCost; - if (effect.mRange == ESM::RT_Target) + x *= 0.5f * (effect.mData.mMagnMin + effect.mData.mMagnMax); + x += effect.mData.mArea * 0.05f * magicEffect->mData.mBaseCost; + if (effect.mData.mRange == ESM::RT_Target) x *= 1.5f; static const float fEffectCostMult = MWBase::Environment::get() .getESMStore() diff --git a/apps/openmw/mwmechanics/spellutil.hpp b/apps/openmw/mwmechanics/spellutil.hpp index fa9b0c64b9..fb9d14c8a5 100644 --- a/apps/openmw/mwmechanics/spellutil.hpp +++ b/apps/openmw/mwmechanics/spellutil.hpp @@ -3,10 +3,14 @@ #include +#include + namespace ESM { + struct EffectList; struct ENAMstruct; struct Enchantment; + struct Ingredient; struct MagicEffect; struct Potion; struct Spell; @@ -36,6 +40,8 @@ namespace MWMechanics int getEnchantmentCharge(const ESM::Enchantment& enchantment); int getPotionValue(const ESM::Potion& potion); + std::optional rollIngredientEffect( + MWWorld::Ptr caster, const ESM::Ingredient* ingredient, uint32_t index = 0); /** * @param spell spell to cast diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 9cdbb19a98..101d4a3ea3 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -1512,7 +1512,7 @@ namespace MWRender return mObjectRoot.get(); } - void Animation::addSpellCastGlow(const ESM::MagicEffect* effect, float glowDuration) + void Animation::addSpellCastGlow(const osg::Vec4f& color, float glowDuration) { if (!mGlowUpdater || (mGlowUpdater->isDone() || (mGlowUpdater->isPermanentGlowUpdater() == true))) { @@ -1521,12 +1521,11 @@ namespace MWRender if (mGlowUpdater && mGlowUpdater->isPermanentGlowUpdater()) { - mGlowUpdater->setColor(effect->getColor()); + mGlowUpdater->setColor(color); mGlowUpdater->setDuration(glowDuration); } else - mGlowUpdater - = SceneUtil::addEnchantedGlow(mObjectRoot, mResourceSystem, effect->getColor(), glowDuration); + mGlowUpdater = SceneUtil::addEnchantedGlow(mObjectRoot, mResourceSystem, color, glowDuration); } } diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index a2226a3054..22b7167a9c 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -345,7 +345,7 @@ namespace MWRender // Add a spell casting glow to an object. From measuring video taken from the original engine, // the glow seems to be about 1.5 seconds except for telekinesis, which is 1 second. - void addSpellCastGlow(const ESM::MagicEffect* effect, float glowDuration = 1.5); + void addSpellCastGlow(const osg::Vec4f& color, float glowDuration = 1.5); virtual void updatePtr(const MWWorld::Ptr& ptr); diff --git a/apps/openmw/mwscript/statsextensions.cpp b/apps/openmw/mwscript/statsextensions.cpp index 4bc59e1524..064e90f114 100644 --- a/apps/openmw/mwscript/statsextensions.cpp +++ b/apps/openmw/mwscript/statsextensions.cpp @@ -560,7 +560,7 @@ namespace MWScript runtime.pop(); if (ptr.getClass().isActor()) - ptr.getClass().getCreatureStats(ptr).getActiveSpells().removeEffects(ptr, spellid); + ptr.getClass().getCreatureStats(ptr).getActiveSpells().removeEffectsBySourceSpellId(ptr, spellid); } }; diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index 932c290aaa..a1eec196eb 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -532,7 +532,7 @@ namespace MWWorld return result; const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getESMStore()->get().search( - enchantment->mEffects.mList.front().mEffectID); + enchantment->mEffects.mList.front().mData.mEffectID); if (!magicEffect) return result; diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 7ecaaa217d..ff3c73311e 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -171,31 +171,31 @@ namespace auto iter = spell.mEffects.mList.begin(); while (iter != spell.mEffects.mList.end()) { - const ESM::MagicEffect* mgef = magicEffects.search(iter->mEffectID); + const ESM::MagicEffect* mgef = magicEffects.search(iter->mData.mEffectID); if (!mgef) { Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId - << ": dropping invalid effect (index " << iter->mEffectID << ")"; + << ": dropping invalid effect (index " << iter->mData.mEffectID << ")"; iter = spell.mEffects.mList.erase(iter); changed = true; continue; } - if (!(mgef->mData.mFlags & ESM::MagicEffect::TargetAttribute) && iter->mAttribute != -1) + if (!(mgef->mData.mFlags & ESM::MagicEffect::TargetAttribute) && iter->mData.mAttribute != -1) { - iter->mAttribute = -1; + iter->mData.mAttribute = -1; Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId << ": dropping unexpected attribute argument of " - << ESM::MagicEffect::indexToGmstString(iter->mEffectID) << " effect"; + << ESM::MagicEffect::indexToGmstString(iter->mData.mEffectID) << " effect"; changed = true; } - if (!(mgef->mData.mFlags & ESM::MagicEffect::TargetSkill) && iter->mSkill != -1) + if (!(mgef->mData.mFlags & ESM::MagicEffect::TargetSkill) && iter->mData.mSkill != -1) { - iter->mSkill = -1; + iter->mData.mSkill = -1; Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId << ": dropping unexpected skill argument of " - << ESM::MagicEffect::indexToGmstString(iter->mEffectID) << " effect"; + << ESM::MagicEffect::indexToGmstString(iter->mData.mEffectID) << " effect"; changed = true; } @@ -742,7 +742,16 @@ namespace MWWorld case ESM::REC_DYNA: reader.getSubNameIs("COUN"); - reader.getHT(mDynamicCount); + if (reader.getFormatVersion() <= ESM::MaxActiveSpellTypeVersion) + { + uint32_t dynamicCount32 = 0; + reader.getHT(dynamicCount32); + mDynamicCount = dynamicCount32; + } + else + { + reader.getHT(mDynamicCount); + } return true; default: diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index 16062c97db..c6271a4428 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -162,7 +162,7 @@ namespace MWWorld std::vector mStores; std::vector mDynamicStores; - unsigned int mDynamicCount; + uint64_t mDynamicCount; mutable std::unordered_map> mSpellListCache; @@ -209,6 +209,7 @@ namespace MWWorld void clearDynamic(); void rebuildIdsIndex(); + ESM::RefId generateId() { return ESM::RefId::generated(mDynamicCount++); } void movePlayerRecord(); @@ -229,7 +230,7 @@ namespace MWWorld template const T* insert(const T& x) { - const ESM::RefId id = ESM::RefId::generated(mDynamicCount++); + const ESM::RefId id = generateId(); Store& store = getWritable(); if (store.search(id) != nullptr) diff --git a/apps/openmw/mwworld/magiceffects.cpp b/apps/openmw/mwworld/magiceffects.cpp index 4ff0e60c46..ce83b3e18f 100644 --- a/apps/openmw/mwworld/magiceffects.cpp +++ b/apps/openmw/mwworld/magiceffects.cpp @@ -46,8 +46,8 @@ namespace MWWorld for (auto& effect : spell->mEffects.mList) { - if (effect.mEffectID == ESM::MagicEffect::DrainAttribute) - stats.mWorsenings[effect.mAttribute] = oldStats.mWorsenings; + if (effect.mData.mEffectID == ESM::MagicEffect::DrainAttribute) + stats.mWorsenings[effect.mData.mAttribute] = oldStats.mWorsenings; } creatureStats.mCorprusSpells[id] = stats; } @@ -58,30 +58,30 @@ namespace MWWorld 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.mSourceSpellId = id; params.mDisplayName = spell->mName; params.mCasterActorId = creatureStats.mActorId; if (spell->mData.mType == ESM::Spell::ST_Ability) - params.mType = ESM::ActiveSpells::Type_Ability; + params.mFlags = ESM::Compatibility::ActiveSpells::Type_Ability_Flags; else - params.mType = ESM::ActiveSpells::Type_Permanent; + params.mFlags = ESM::Compatibility::ActiveSpells::Type_Permanent_Flags; params.mWorsenings = -1; params.mNextWorsening = ESM::TimeStamp(); - int effectIndex = 0; for (const auto& enam : spell->mEffects.mList) { - if (oldParams.mPurgedEffects.find(effectIndex) == oldParams.mPurgedEffects.end()) + if (oldParams.mPurgedEffects.find(enam.mIndex) == oldParams.mPurgedEffects.end()) { ESM::ActiveEffect effect; - effect.mEffectId = enam.mEffectID; - effect.mArg = MWMechanics::EffectKey(enam).mArg; + effect.mEffectId = enam.mData.mEffectID; + effect.mArg = MWMechanics::EffectKey(enam.mData).mArg; effect.mDuration = -1; effect.mTimeLeft = -1; - effect.mEffectIndex = effectIndex; - auto rand = oldParams.mEffectRands.find(effectIndex); + effect.mEffectIndex = enam.mIndex; + auto rand = oldParams.mEffectRands.find(enam.mIndex); if (rand != oldParams.mEffectRands.end()) { - float magnitude = (enam.mMagnMax - enam.mMagnMin) * rand->second + enam.mMagnMin; + float magnitude + = (enam.mData.mMagnMax - enam.mData.mMagnMin) * rand->second + enam.mData.mMagnMin; effect.mMagnitude = magnitude; effect.mMinMagnitude = magnitude; effect.mMaxMagnitude = magnitude; @@ -92,13 +92,12 @@ namespace MWWorld else { effect.mMagnitude = 0.f; - effect.mMinMagnitude = enam.mMagnMin; - effect.mMaxMagnitude = enam.mMagnMax; + effect.mMinMagnitude = enam.mData.mMagnMin; + effect.mMaxMagnitude = enam.mData.mMagnMax; effect.mFlags = ESM::ActiveEffect::Flag_None; } params.mEffects.emplace_back(effect); } - effectIndex++; } creatureStats.mActiveSpells.mSpells.emplace_back(params); } @@ -132,30 +131,28 @@ namespace MWWorld if (!enchantment) continue; ESM::ActiveSpells::ActiveSpellParams params; - params.mId = id; + params.mSourceSpellId = id; params.mDisplayName = std::move(name); params.mCasterActorId = creatureStats.mActorId; - params.mType = ESM::ActiveSpells::Type_Enchantment; + params.mFlags = ESM::Compatibility::ActiveSpells::Type_Enchantment_Flags; params.mWorsenings = -1; params.mNextWorsening = ESM::TimeStamp(); - for (std::size_t effectIndex = 0; - effectIndex < oldMagnitudes.size() && effectIndex < enchantment->mEffects.mList.size(); ++effectIndex) + for (const auto& enam : enchantment->mEffects.mList) { - const auto& enam = enchantment->mEffects.mList[effectIndex]; - auto [random, multiplier] = oldMagnitudes[effectIndex]; - float magnitude = (enam.mMagnMax - enam.mMagnMin) * random + enam.mMagnMin; + auto [random, multiplier] = oldMagnitudes[enam.mIndex]; + float magnitude = (enam.mData.mMagnMax - enam.mData.mMagnMin) * random + enam.mData.mMagnMin; magnitude *= multiplier; if (magnitude <= 0) continue; ESM::ActiveEffect effect; - effect.mEffectId = enam.mEffectID; + effect.mEffectId = enam.mData.mEffectID; effect.mMagnitude = magnitude; effect.mMinMagnitude = magnitude; effect.mMaxMagnitude = magnitude; - effect.mArg = MWMechanics::EffectKey(enam).mArg; + effect.mArg = MWMechanics::EffectKey(enam.mData).mArg; effect.mDuration = -1; effect.mTimeLeft = -1; - effect.mEffectIndex = static_cast(effectIndex); + effect.mEffectIndex = enam.mIndex; // Prevent recalculation of resistances and don't reflect or absorb the effect effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances | ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; @@ -172,7 +169,7 @@ namespace MWWorld { auto it = std::find_if(creatureStats.mActiveSpells.mSpells.begin(), creatureStats.mActiveSpells.mSpells.end(), - [&](const auto& params) { return params.mId == spell.first; }); + [&](const auto& params) { return params.mSourceSpellId == spell.first; }); if (it != creatureStats.mActiveSpells.mSpells.end()) { it->mNextWorsening = spell.second.mNextWorsening; @@ -188,7 +185,7 @@ namespace MWWorld continue; for (auto& params : creatureStats.mActiveSpells.mSpells) { - if (params.mId == key.mSourceId) + if (params.mSourceSpellId == key.mSourceId) { bool found = false; for (auto& effect : params.mEffects) diff --git a/apps/openmw/mwworld/projectilemanager.cpp b/apps/openmw/mwworld/projectilemanager.cpp index 6fc515981b..d6715c5ad2 100644 --- a/apps/openmw/mwworld/projectilemanager.cpp +++ b/apps/openmw/mwworld/projectilemanager.cpp @@ -79,18 +79,17 @@ namespace int count = 0; speed = 0.0f; ESM::EffectList projectileEffects; - for (std::vector::const_iterator iter(effects->mList.begin()); iter != effects->mList.end(); - ++iter) + for (const ESM::IndexedENAMstruct& effect : effects->mList) { const ESM::MagicEffect* magicEffect - = MWBase::Environment::get().getESMStore()->get().find(iter->mEffectID); + = MWBase::Environment::get().getESMStore()->get().find(effect.mData.mEffectID); // Speed of multi-effect projectiles should be the average of the constituent effects, // based on observation of the original engine. speed += magicEffect->mData.mSpeed; count++; - if (iter->mRange != ESM::RT_Target) + if (effect.mData.mRange != ESM::RT_Target) continue; if (magicEffect->mBolt.empty()) @@ -106,7 +105,7 @@ namespace ->get() .find(magicEffect->mData.mSchool) ->mSchool->mBoltSound); - projectileEffects.mList.push_back(*iter); + projectileEffects.mList.push_back(effect); } if (count != 0) @@ -117,7 +116,7 @@ namespace { const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getESMStore()->get().find( - effects->mList.begin()->mEffectID); + effects->mList.begin()->mData.mEffectID); texture = magicEffect->mParticle; } @@ -136,10 +135,10 @@ namespace { // Calculate combined light diffuse color from magical effects osg::Vec4 lightDiffuseColor; - for (const ESM::ENAMstruct& enam : effects.mList) + for (const ESM::IndexedENAMstruct& enam : effects.mList) { const ESM::MagicEffect* magicEffect - = MWBase::Environment::get().getESMStore()->get().find(enam.mEffectID); + = MWBase::Environment::get().getESMStore()->get().find(enam.mData.mEffectID); lightDiffuseColor += magicEffect->getColor(); } int numberOfEffects = effects.mList.size(); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 0468b36a2f..9ca8a0d12a 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -2935,14 +2935,14 @@ namespace MWWorld return result; } - void World::castSpell(const Ptr& actor, bool manualSpell) + void World::castSpell(const Ptr& actor, bool scriptedSpell) { MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); const bool casterIsPlayer = actor == MWMechanics::getPlayer(); MWWorld::Ptr target; // For scripted spells we should not use hit contact - if (manualSpell) + if (scriptedSpell) { if (!casterIsPlayer) { @@ -3010,7 +3010,7 @@ namespace MWWorld const ESM::RefId& selectedSpell = stats.getSpells().getSelectedSpell(); - MWMechanics::CastSpell cast(actor, target, false, manualSpell); + MWMechanics::CastSpell cast(actor, target, false, scriptedSpell); cast.mHitPosition = hitPosition; if (!selectedSpell.empty()) @@ -3698,22 +3698,22 @@ namespace MWWorld void World::preloadEffects(const ESM::EffectList* effectList) { - for (const ESM::ENAMstruct& effectInfo : effectList->mList) + for (const ESM::IndexedENAMstruct& effectInfo : effectList->mList) { - const ESM::MagicEffect* effect = mStore.get().find(effectInfo.mEffectID); + const ESM::MagicEffect* effect = mStore.get().find(effectInfo.mData.mEffectID); - if (MWMechanics::isSummoningEffect(effectInfo.mEffectID)) + if (MWMechanics::isSummoningEffect(effectInfo.mData.mEffectID)) { preload(mWorldScene.get(), mStore, ESM::RefId::stringRefId("VFX_Summon_Start")); - preload(mWorldScene.get(), mStore, MWMechanics::getSummonedCreature(effectInfo.mEffectID)); + preload(mWorldScene.get(), mStore, MWMechanics::getSummonedCreature(effectInfo.mData.mEffectID)); } preload(mWorldScene.get(), mStore, effect->mCasting); preload(mWorldScene.get(), mStore, effect->mHit); - if (effectInfo.mArea > 0) + if (effectInfo.mData.mArea > 0) preload(mWorldScene.get(), mStore, effect->mArea); - if (effectInfo.mRange == ESM::RT_Target) + if (effectInfo.mData.mRange == ESM::RT_Target) preload(mWorldScene.get(), mStore, effect->mBolt); } } diff --git a/apps/openmw_test_suite/esm3/testsaveload.cpp b/apps/openmw_test_suite/esm3/testsaveload.cpp index 6d5fdf1c14..2629442563 100644 --- a/apps/openmw_test_suite/esm3/testsaveload.cpp +++ b/apps/openmw_test_suite/esm3/testsaveload.cpp @@ -530,29 +530,30 @@ namespace ESM TEST_P(Esm3SaveLoadRecordTest, enamShouldNotChange) { EffectList record; - record.mList.emplace_back(ENAMstruct{ - .mEffectID = 1, - .mSkill = 2, - .mAttribute = 3, - .mRange = 4, - .mArea = 5, - .mDuration = 6, - .mMagnMin = 7, - .mMagnMax = 8, - }); + record.mList.emplace_back(IndexedENAMstruct{ { + .mEffectID = 1, + .mSkill = 2, + .mAttribute = 3, + .mRange = 4, + .mArea = 5, + .mDuration = 6, + .mMagnMin = 7, + .mMagnMax = 8, + }, + 0 }); EffectList result; saveAndLoadRecord(record, GetParam(), result); EXPECT_EQ(result.mList.size(), record.mList.size()); - EXPECT_EQ(result.mList[0].mEffectID, record.mList[0].mEffectID); - EXPECT_EQ(result.mList[0].mSkill, record.mList[0].mSkill); - EXPECT_EQ(result.mList[0].mAttribute, record.mList[0].mAttribute); - EXPECT_EQ(result.mList[0].mRange, record.mList[0].mRange); - EXPECT_EQ(result.mList[0].mArea, record.mList[0].mArea); - EXPECT_EQ(result.mList[0].mDuration, record.mList[0].mDuration); - EXPECT_EQ(result.mList[0].mMagnMin, record.mList[0].mMagnMin); - EXPECT_EQ(result.mList[0].mMagnMax, record.mList[0].mMagnMax); + EXPECT_EQ(result.mList[0].mData.mEffectID, record.mList[0].mData.mEffectID); + EXPECT_EQ(result.mList[0].mData.mSkill, record.mList[0].mData.mSkill); + EXPECT_EQ(result.mList[0].mData.mAttribute, record.mList[0].mData.mAttribute); + EXPECT_EQ(result.mList[0].mData.mRange, record.mList[0].mData.mRange); + EXPECT_EQ(result.mList[0].mData.mArea, record.mList[0].mData.mArea); + EXPECT_EQ(result.mList[0].mData.mDuration, record.mList[0].mData.mDuration); + EXPECT_EQ(result.mList[0].mData.mMagnMin, record.mList[0].mData.mMagnMin); + EXPECT_EQ(result.mList[0].mData.mMagnMax, record.mList[0].mData.mMagnMax); } TEST_P(Esm3SaveLoadRecordTest, weaponShouldNotChange) diff --git a/components/esm3/activespells.cpp b/components/esm3/activespells.cpp index 8ce47b7719..c3d86c3ebf 100644 --- a/components/esm3/activespells.cpp +++ b/components/esm3/activespells.cpp @@ -93,11 +93,12 @@ namespace ESM { for (const auto& params : spells) { - esm.writeHNRefId(tag, params.mId); + esm.writeHNRefId(tag, params.mSourceSpellId); + esm.writeHNRefId("SPID", params.mActiveSpellId); esm.writeHNT("CAST", params.mCasterActorId); esm.writeHNString("DISP", params.mDisplayName); - esm.writeHNT("TYPE", params.mType); + esm.writeHNT("FLAG", params.mFlags); if (params.mItem.isSet()) esm.writeFormId(params.mItem, true, "ITEM"); if (params.mWorsenings >= 0) @@ -130,14 +131,42 @@ namespace ESM while (esm.isNextSub(tag)) { ActiveSpells::ActiveSpellParams params; - params.mId = esm.getRefId(); + params.mSourceSpellId = esm.getRefId(); + if (format > MaxActiveSpellTypeVersion) + params.mActiveSpellId = esm.getHNRefId("SPID"); esm.getHNT(params.mCasterActorId, "CAST"); params.mDisplayName = esm.getHNString("DISP"); if (format <= MaxClearModifiersFormatVersion) - params.mType = ActiveSpells::Type_Temporary; + params.mFlags = Compatibility::ActiveSpells::Type_Temporary_Flags; else { - esm.getHNT(params.mType, "TYPE"); + if (format <= MaxActiveSpellTypeVersion) + { + Compatibility::ActiveSpells::EffectType type; + esm.getHNT(type, "TYPE"); + switch (type) + { + case Compatibility::ActiveSpells::Type_Ability: + params.mFlags = Compatibility::ActiveSpells::Type_Ability_Flags; + break; + case Compatibility::ActiveSpells::Type_Consumable: + params.mFlags = Compatibility::ActiveSpells::Type_Consumable_Flags; + break; + case Compatibility::ActiveSpells::Type_Enchantment: + params.mFlags = Compatibility::ActiveSpells::Type_Enchantment_Flags; + break; + case Compatibility::ActiveSpells::Type_Permanent: + params.mFlags = Compatibility::ActiveSpells::Type_Permanent_Flags; + break; + case Compatibility::ActiveSpells::Type_Temporary: + params.mFlags = Compatibility::ActiveSpells::Type_Temporary_Flags; + break; + } + } + else + { + esm.getHNT(params.mFlags, "FLAG"); + } if (esm.peekNextSub("ITEM")) { if (format <= MaxActiveSpellSlotIndexFormatVersion) diff --git a/components/esm3/activespells.hpp b/components/esm3/activespells.hpp index 0e4e01eda3..daec9fc515 100644 --- a/components/esm3/activespells.hpp +++ b/components/esm3/activespells.hpp @@ -46,23 +46,28 @@ namespace ESM // format 0, saved games only struct ActiveSpells { - enum EffectType + enum Flags : uint32_t { - Type_Temporary, - Type_Ability, - Type_Enchantment, - Type_Permanent, - Type_Consumable + Flag_Temporary = 1 << 0, //!< Effect will end automatically once its duration ends. + Flag_Equipment = 1 << 1, //!< Effect will end automatically if item is unequipped. + Flag_SpellStore = 1 << 2, //!< Effect will end automatically if removed from the actor's spell store. + Flag_AffectsBaseValues = 1 << 3, //!< Effects will affect base values instead of current values. + Flag_Stackable + = 1 << 4, //!< Effect can stack. If this flag is not set, spells from the same caster and item cannot stack. + Flag_Lua + = 1 << 5, //!< Effect was added via Lua. Should not do any vfx/sound as this is handled by Lua scripts. }; struct ActiveSpellParams { - RefId mId; + RefId mActiveSpellId; + RefId mSourceSpellId; std::vector mEffects; std::string mDisplayName; int32_t mCasterActorId; RefNum mItem; - EffectType mType; + Flags mFlags; + bool mStackable; int32_t mWorsenings; TimeStamp mNextWorsening; }; @@ -73,6 +78,29 @@ namespace ESM void load(ESMReader& esm); void save(ESMWriter& esm) const; }; + + namespace Compatibility + { + namespace ActiveSpells + { + enum EffectType + { + Type_Temporary, + Type_Ability, + Type_Enchantment, + Type_Permanent, + Type_Consumable, + }; + + using Flags = ESM::ActiveSpells::Flags; + constexpr Flags Type_Temporary_Flags = Flags::Flag_Temporary; + constexpr Flags Type_Consumable_Flags = static_cast(Flags::Flag_Temporary | Flags::Flag_Stackable); + constexpr Flags Type_Permanent_Flags = Flags::Flag_SpellStore; + constexpr Flags Type_Ability_Flags + = static_cast(Flags::Flag_SpellStore | Flags::Flag_AffectsBaseValues); + constexpr Flags Type_Enchantment_Flags = Flags::Flag_Equipment; + } + } } #endif diff --git a/components/esm3/effectlist.cpp b/components/esm3/effectlist.cpp index 4f21f47fa2..a71eccfb84 100644 --- a/components/esm3/effectlist.cpp +++ b/components/esm3/effectlist.cpp @@ -22,19 +22,40 @@ namespace ESM } } + void EffectList::populate(const std::vector& effects) + { + mList.clear(); + for (size_t i = 0; i < effects.size(); i++) + mList.push_back({ effects[i], static_cast(i) }); + } + + void EffectList::updateIndexes() + { + for (size_t i = 0; i < mList.size(); i++) + mList[i].mIndex = i; + } + void EffectList::add(ESMReader& esm) { ENAMstruct s; esm.getSubComposite(s); - mList.push_back(s); + mList.push_back({ s, static_cast(mList.size()) }); } void EffectList::save(ESMWriter& esm) const { - for (const ENAMstruct& enam : mList) + for (const IndexedENAMstruct& enam : mList) { - esm.writeNamedComposite("ENAM", enam); + esm.writeNamedComposite("ENAM", enam.mData); } } + bool IndexedENAMstruct::operator!=(const IndexedENAMstruct& rhs) const + { + return mData.mEffectID != rhs.mData.mEffectID || mData.mArea != rhs.mData.mArea + || mData.mRange != rhs.mData.mRange || mData.mSkill != rhs.mData.mSkill + || mData.mAttribute != rhs.mData.mAttribute || mData.mMagnMin != rhs.mData.mMagnMin + || mData.mMagnMax != rhs.mData.mMagnMax || mData.mDuration != rhs.mData.mDuration; + } + } // end namespace diff --git a/components/esm3/effectlist.hpp b/components/esm3/effectlist.hpp index de13797496..8eb347d6c8 100644 --- a/components/esm3/effectlist.hpp +++ b/components/esm3/effectlist.hpp @@ -26,10 +26,21 @@ namespace ESM int32_t mArea, mDuration, mMagnMin, mMagnMax; }; + struct IndexedENAMstruct + { + bool operator!=(const IndexedENAMstruct& rhs) const; + bool operator==(const IndexedENAMstruct& rhs) const { return !(this->operator!=(rhs)); } + ENAMstruct mData; + uint32_t mIndex; + }; + /// EffectList, ENAM subrecord struct EffectList { - std::vector mList; + std::vector mList; + + void populate(const std::vector& effects); + void updateIndexes(); /// Load one effect, assumes subrecord name was already read void add(ESMReader& esm); diff --git a/components/esm3/formatversion.hpp b/components/esm3/formatversion.hpp index d90742a512..36e43728e2 100644 --- a/components/esm3/formatversion.hpp +++ b/components/esm3/formatversion.hpp @@ -26,7 +26,8 @@ namespace ESM inline constexpr FormatVersion MaxUseEsmCellIdFormatVersion = 26; inline constexpr FormatVersion MaxActiveSpellSlotIndexFormatVersion = 27; inline constexpr FormatVersion MaxOldCountFormatVersion = 30; - inline constexpr FormatVersion CurrentSaveGameFormatVersion = 31; + inline constexpr FormatVersion MaxActiveSpellTypeVersion = 31; + inline constexpr FormatVersion CurrentSaveGameFormatVersion = 32; inline constexpr FormatVersion MinSupportedSaveGameFormatVersion = 5; inline constexpr FormatVersion OpenMW0_48SaveGameFormatVersion = 21; diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index 66fb817362..7963853e2b 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -343,6 +343,11 @@ -- @field #string id Record id of the spell or item used to cast the spell -- @field #GameObject item The enchanted item used to cast the spell, or nil if the spell was not cast from an enchanted item. Note that if the spell was cast for a single-use enchantment such as a scroll, this will be nil. -- @field #GameObject caster The caster object, or nil if the spell has no defined caster +-- @field #boolean fromEquipment If set, this spell is tied to an equipped item and can only be ended by unequipping the item. +-- @field #boolean temporary If set, this spell effect is temporary and should end on its own. Either after a single application or after its duration has run out. +-- @field #boolean affectsBaseValues If set, this spell affects the base values of affected stats, rather than modifying current values. +-- @field #boolean stackable If set, this spell can be applied multiple times. If not set, the same spell can only be applied once from the same source (where source is determined by caster + item). In vanilla rules, consumables are stackable while spells and enchantments are not. +-- @field #number activeSpellId A number uniquely identifying this active spell within the affected actor's list of active spells. -- @field #list<#ActiveSpellEffect> effects The active effects (@{#ActiveSpellEffect}) of this spell. --- @@ -373,7 +378,7 @@ -- @type Enchantment -- @field #string id Enchantment id -- @field #number type @{#EnchantmentType} --- @field #number autocalcFlag If set, the casting cost should be computer rather than reading the cost field +-- @field #boolean autocalcFlag If set, the casting cost should be computed based on the effect list rather than read from the cost field -- @field #number cost -- @field #number charge Charge capacity. Should not be confused with current charge. -- @field #list<#MagicEffectWithParams> effects The effects (@{#MagicEffectWithParams}) of the enchantment @@ -664,6 +669,8 @@ -- @field #number type @{#SpellType} -- @field #number cost -- @field #list<#MagicEffectWithParams> effects The effects (@{#MagicEffectWithParams}) of the spell +-- @field #boolean alwaysSucceedFlag If set, the spell should ignore skill checks and always succeed. +-- @field #boolean autocalcFlag If set, the casting cost should be computed based on the effect list rather than read from the cost field --- -- @type MagicEffect @@ -673,16 +680,28 @@ -- @field #string school Skill ID that is this effect's school -- @field #number baseCost -- @field openmw.util#Color color --- @field #boolean harmful +-- @field #boolean harmful If set, the effect is considered harmful and should elicit a hostile reaction from affected NPCs. -- @field #boolean continuousVfx Whether the magic effect's vfx should loop or not +-- @field #boolean hasDuration If set, the magic effect has a duration. As an example, divine intervention has no duration while fire damage does. +-- @field #boolean hasMagnitude If set, the magic effect depends on a magnitude. As an example, cure common disease has no magnitude while chameleon does. +-- @field #boolean isAppliedOnce If set, the magic effect is applied fully on cast, rather than being continuously applied over the effect's duration. For example, chameleon is applied once, while fire damage is continuously applied for the duration. +-- @field #boolean casterLinked If set, it is implied the magic effect links back to the caster in some way and should end immediately or never be applied if the caster dies or is not an actor. +-- @field #boolean nonRecastable If set, this effect cannot be re-applied until it has ended. This is used by bound equipment spells. -- @field #string particle Identifier of the particle texture --- @field #string castingStatic Identifier of the vfx static used for casting +-- @field #string castStatic Identifier of the vfx static used for casting -- @field #string hitStatic Identifier of the vfx static used on hit -- @field #string areaStatic Identifier of the vfx static used for AOE spells +-- @field #string boltStatic Identifier of the projectile vfx static used for ranged spells +-- @field #string castSound Identifier of the sound used for casting +-- @field #string hitSound Identifier of the sound used on hit +-- @field #string areaSound Identifier of the sound used for AOE spells +-- @field #string boltSound Identifier of the projectile sound used for ranged spells + --- -- @type MagicEffectWithParams -- @field #MagicEffect effect @{#MagicEffect} +-- @field #string id ID of the associated @{#MagicEffect} -- @field #string affectedSkill Optional skill ID -- @field #string affectedAttribute Optional attribute ID -- @field #number range @@ -690,6 +709,7 @@ -- @field #number magnitudeMin -- @field #number magnitudeMax -- @field #number duration +-- @field #number index Index of this effect within the original list of @{#MagicEffectWithParams} of the spell/enchantment/potion this effect came from. --- -- @type ActiveEffect @@ -701,6 +721,7 @@ -- @field #number magnitude current magnitude of the effect. Will be set to 0 when effect is removed or expires. -- @field #number magnitudeBase -- @field #number magnitudeModifier +-- @field #number index Index of this effect within the original list of @{#MagicEffectWithParams} of the spell/enchantment/potion this effect came from. --- @{#Sound}: Sounds and Speech -- @field [parent=#core] #Sound sound diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index 60f3e79628..64dab547f1 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -302,17 +302,59 @@ -- end --- --- Get whether a specific spell is active on the actor. +-- Get whether any instance of the specific spell is active on the actor. -- @function [parent=#ActorActiveSpells] isSpellActive -- @param self --- @param #any recordOrId record or string record ID of the active spell's source. valid records are @{openmw.core#Spell}, @{openmw.core#Enchantment}, #IngredientRecord, or #PotionRecord +-- @param #any recordOrId A record or string record ID. Valid records are @{openmw.core#Spell}, enchanted @{#Item}, @{#IngredientRecord}, or @{#PotionRecord}. -- @return true if spell is active, false otherwise --- --- Remove the given spell and all its effects from the given actor's active spells. +-- If true, the actor has not used this power in the last 24h. Will return true for powers the actor does not have. +-- @function [parent=#ActorActiveSpells] canUsePower +-- @param self +-- @param #any spellOrId A @{openmw.core#Spell} or string record id. + +--- +-- Remove an active spell based on active spell ID (see @{openmw.core#ActiveSpell.activeSpellId}). Can only be used in global scripts or on self. Can only be used to remove spells with the temporary flag set (see @{openmw.core#ActiveSpell.temporary}). -- @function [parent=#ActorActiveSpells] remove -- @param self --- @param #any spellOrId @{openmw.core#Spell} or string spell id +-- @param #any id Active spell ID. + +--- +-- Adds a new spell to the list of active spells (only in global scripts or on self). +-- Note that this does not play any related VFX or sounds. +-- @function [parent=#ActorActiveSpells] add +-- @param self +-- @param #table options A table of parameters. Must contain the following required parameters: +-- +-- * `id` - A string record ID. Valid records are @{openmw.core#Spell}, enchanted @{#Item}, @{#IngredientRecord}, or @{#PotionRecord}. +-- * `effects` - A list of indexes of the effects to be applied. These indexes must be in range of the record's list of @{openmw.core#MagicEffectWithParams}. Note that for Ingredients, normal ingredient consumption rules will be applied to effects. +-- +-- And may contain the following optional parameters: +-- +-- * `name` - The name to show in the list of active effects in the UI. Default: Name of the record identified by the id. +-- * `ignoreResistances` - If true, resistances will be ignored. Default: false +-- * `ignoreSpellAbsorption` - If true, spell absorption will not be applied. Default: false. +-- * `ignoreReflect` - If true, reflects will not be applied. Default: false. +-- * `caster` - A game object that identifies the caster. Default: nil +-- * `item` - A game object that identifies the specific enchanted item instance used to cast the spell. Default: nil +-- * `stackable` - If true, the spell will be able to stack. If false, existing instances of spells with the same id from the same source (where source is caster + item) +-- * `quiet` - If true, no messages will be printed if the spell is an Ingredient and it had no effect. Always true if the target is not the player. +-- @usage +-- -- Adds the effect of the chameleon spell to the character +-- Actor.activeSpells(self):add({id = 'chameleon', effects = { 0 }}) +-- @usage +-- -- Adds the effect of a standard potion of intelligence, without consuming any potions from the character's inventory. +-- -- Note that stackable = true to let the effect stack like a potion should. +-- Actor.activeSpells(self):add({id = 'p_fortify_intelligence_s', effects = { 0 }, stackable = true}) +-- @usage +-- -- Adds the negative effect of Greef twice over, and renames it to Good Greef. +-- Actor.activeSpells(self):add({id = 'potion_comberry_brandy_01', effects = { 1, 1 }, stackable = true, name = 'Good Greef'}) +-- @usage +-- -- Has the same effect as if the actor ate a chokeweed. With the same variable effect based on skill / random chance. +-- Actor.activeSpells(self):add({id = 'ingred_chokeweed_01', effects = { 0 }, stackable = true, name = 'Chokeweed'}) +-- -- Same as above, but uses a different index. Note that if multiple indexes are used, the randomicity is applied separately for each effect. +-- Actor.activeSpells(self):add({id = 'ingred_chokeweed_01', effects = { 1 }, stackable = true, name = 'Chokeweed'}) --- -- Return the spells (@{#ActorSpells}) of the given actor.