Merge branch 'spellcast-refactor' into 'master'

Spellcast related Lua API + spellcasting/activespell refactor

See merge request OpenMW/openmw!3922
fix-osga-rotate-wildly
psi29a 9 months ago
commit 012d10703f

@ -180,22 +180,23 @@ namespace
void printEffectList(const ESM::EffectList& effects) void printEffectList(const ESM::EffectList& effects)
{ {
int i = 0; 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::cout << " Effect[" << i << "]: " << magicEffectLabel(effect.mData.mEffectID) << " ("
<< ")" << std::endl; << effect.mData.mEffectID << ")" << std::endl;
if (effect.mSkill != -1) if (effect.mData.mSkill != -1)
std::cout << " Skill: " << skillLabel(effect.mSkill) << " (" << (int)effect.mSkill << ")" std::cout << " Skill: " << skillLabel(effect.mData.mSkill) << " (" << (int)effect.mData.mSkill << ")"
<< std::endl; << std::endl;
if (effect.mAttribute != -1) if (effect.mData.mAttribute != -1)
std::cout << " Attribute: " << attributeLabel(effect.mAttribute) << " (" << (int)effect.mAttribute std::cout << " Attribute: " << attributeLabel(effect.mData.mAttribute) << " ("
<< ")" << std::endl; << (int)effect.mData.mAttribute << ")" << std::endl;
std::cout << " Range: " << rangeTypeLabel(effect.mRange) << " (" << effect.mRange << ")" << std::endl; std::cout << " Range: " << rangeTypeLabel(effect.mData.mRange) << " (" << effect.mData.mRange << ")"
<< std::endl;
// Area is always zero if range type is "Self" // Area is always zero if range type is "Self"
if (effect.mRange != ESM::RT_Self) if (effect.mData.mRange != ESM::RT_Self)
std::cout << " Area: " << effect.mArea << std::endl; std::cout << " Area: " << effect.mData.mArea << std::endl;
std::cout << " Duration: " << effect.mDuration << std::endl; std::cout << " Duration: " << effect.mData.mDuration << std::endl;
std::cout << " Magnitude: " << effect.mMagnMin << "-" << effect.mMagnMax << std::endl; std::cout << " Magnitude: " << effect.mData.mMagnMin << "-" << effect.mData.mMagnMax << std::endl;
i++; i++;
} }
} }

@ -60,38 +60,38 @@ void CSMTools::EnchantmentCheckStage::perform(int stage, CSMDoc::Messages& messa
} }
else else
{ {
std::vector<ESM::ENAMstruct>::const_iterator effect = enchantment.mEffects.mList.begin(); std::vector<ESM::IndexedENAMstruct>::const_iterator effect = enchantment.mEffects.mList.begin();
for (size_t i = 1; i <= enchantment.mEffects.mList.size(); i++) for (size_t i = 1; i <= enchantment.mEffects.mList.size(); i++)
{ {
const std::string number = std::to_string(i); const std::string number = std::to_string(i);
// At the time of writing this effects, attributes and skills are hardcoded // 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); messages.add(id, "Effect #" + number + " is invalid", "", CSMDoc::Message::Severity_Error);
++effect; ++effect;
continue; continue;
} }
if (effect->mSkill < -1 || effect->mSkill > 26) if (effect->mData.mSkill < -1 || effect->mData.mSkill > 26)
messages.add( messages.add(
id, "Effect #" + number + " affected skill is invalid", "", CSMDoc::Message::Severity_Error); 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( messages.add(
id, "Effect #" + number + " affected attribute is invalid", "", CSMDoc::Message::Severity_Error); 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); 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); 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); messages.add(id, "Effect #" + number + " duration is negative", "", CSMDoc::Message::Severity_Error);
if (effect->mMagnMin < 0) if (effect->mData.mMagnMin < 0)
messages.add( messages.add(
id, "Effect #" + number + " minimum magnitude is negative", "", CSMDoc::Message::Severity_Error); id, "Effect #" + number + " minimum magnitude is negative", "", CSMDoc::Message::Severity_Error);
if (effect->mMagnMax < 0) if (effect->mData.mMagnMax < 0)
messages.add( messages.add(
id, "Effect #" + number + " maximum magnitude is negative", "", CSMDoc::Message::Severity_Error); 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", "", messages.add(id, "Effect #" + number + " minimum magnitude is higher than maximum magnitude", "",
CSMDoc::Message::Severity_Error); CSMDoc::Message::Severity_Error);
++effect; ++effect;

@ -255,20 +255,22 @@ namespace CSMWorld
{ {
ESXRecordT magic = record.get(); ESXRecordT magic = record.get();
std::vector<ESM::ENAMstruct>& effectsList = magic.mEffects.mList; std::vector<ESM::IndexedENAMstruct>& effectsList = magic.mEffects.mList;
// blank row // blank row
ESM::ENAMstruct effect; ESM::IndexedENAMstruct effect;
effect.mEffectID = 0; effect.mIndex = position;
effect.mSkill = -1; effect.mData.mEffectID = 0;
effect.mAttribute = -1; effect.mData.mSkill = -1;
effect.mRange = 0; effect.mData.mAttribute = -1;
effect.mArea = 0; effect.mData.mRange = 0;
effect.mDuration = 0; effect.mData.mArea = 0;
effect.mMagnMin = 0; effect.mData.mDuration = 0;
effect.mMagnMax = 0; effect.mData.mMagnMin = 0;
effect.mData.mMagnMax = 0;
effectsList.insert(effectsList.begin() + position, effect); effectsList.insert(effectsList.begin() + position, effect);
magic.mEffects.updateIndexes();
record.setModified(magic); record.setModified(magic);
} }
@ -277,12 +279,13 @@ namespace CSMWorld
{ {
ESXRecordT magic = record.get(); ESXRecordT magic = record.get();
std::vector<ESM::ENAMstruct>& effectsList = magic.mEffects.mList; std::vector<ESM::IndexedENAMstruct>& effectsList = magic.mEffects.mList;
if (rowToRemove < 0 || rowToRemove >= static_cast<int>(effectsList.size())) if (rowToRemove < 0 || rowToRemove >= static_cast<int>(effectsList.size()))
throw std::runtime_error("index out of range"); throw std::runtime_error("index out of range");
effectsList.erase(effectsList.begin() + rowToRemove); effectsList.erase(effectsList.begin() + rowToRemove);
magic.mEffects.updateIndexes();
record.setModified(magic); record.setModified(magic);
} }
@ -292,7 +295,7 @@ namespace CSMWorld
ESXRecordT magic = record.get(); ESXRecordT magic = record.get();
magic.mEffects.mList magic.mEffects.mList
= static_cast<const NestedTableWrapper<std::vector<ESM::ENAMstruct>>&>(nestedTable).mNestedTable; = static_cast<const NestedTableWrapper<std::vector<ESM::IndexedENAMstruct>>&>(nestedTable).mNestedTable;
record.setModified(magic); record.setModified(magic);
} }
@ -300,19 +303,19 @@ namespace CSMWorld
NestedTableWrapperBase* table(const Record<ESXRecordT>& record) const override NestedTableWrapperBase* table(const Record<ESXRecordT>& record) const override
{ {
// deleted by dtor of NestedTableStoring // deleted by dtor of NestedTableStoring
return new NestedTableWrapper<std::vector<ESM::ENAMstruct>>(record.get().mEffects.mList); return new NestedTableWrapper<std::vector<ESM::IndexedENAMstruct>>(record.get().mEffects.mList);
} }
QVariant getData(const Record<ESXRecordT>& record, int subRowIndex, int subColIndex) const override QVariant getData(const Record<ESXRecordT>& record, int subRowIndex, int subColIndex) const override
{ {
ESXRecordT magic = record.get(); ESXRecordT magic = record.get();
std::vector<ESM::ENAMstruct>& effectsList = magic.mEffects.mList; std::vector<ESM::IndexedENAMstruct>& effectsList = magic.mEffects.mList;
if (subRowIndex < 0 || subRowIndex >= static_cast<int>(effectsList.size())) if (subRowIndex < 0 || subRowIndex >= static_cast<int>(effectsList.size()))
throw std::runtime_error("index out of range"); throw std::runtime_error("index out of range");
ESM::ENAMstruct effect = effectsList[subRowIndex]; ESM::ENAMstruct effect = effectsList[subRowIndex].mData;
switch (subColIndex) switch (subColIndex)
{ {
case 0: case 0:
@ -374,12 +377,12 @@ namespace CSMWorld
{ {
ESXRecordT magic = record.get(); ESXRecordT magic = record.get();
std::vector<ESM::ENAMstruct>& effectsList = magic.mEffects.mList; std::vector<ESM::IndexedENAMstruct>& effectsList = magic.mEffects.mList;
if (subRowIndex < 0 || subRowIndex >= static_cast<int>(effectsList.size())) if (subRowIndex < 0 || subRowIndex >= static_cast<int>(effectsList.size()))
throw std::runtime_error("index out of range"); throw std::runtime_error("index out of range");
ESM::ENAMstruct effect = effectsList[subRowIndex]; ESM::ENAMstruct effect = effectsList[subRowIndex].mData;
switch (subColIndex) switch (subColIndex)
{ {
case 0: case 0:
@ -438,7 +441,7 @@ namespace CSMWorld
throw std::runtime_error("Magic Effects subcolumn index out of range"); 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); record.setModified(magic);
} }

@ -265,7 +265,7 @@ namespace MWBase
virtual bool isReadyToBlock(const MWWorld::Ptr& ptr) const = 0; virtual bool isReadyToBlock(const MWWorld::Ptr& ptr) const = 0;
virtual bool isAttackingOrSpell(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<std::pair<std::string, std::string>>& settings) = 0; virtual void processChangedSettings(const std::set<std::pair<std::string, std::string>>& settings) = 0;

@ -461,7 +461,7 @@ namespace MWBase
*/ */
virtual MWWorld::SpellCastState startSpellCast(const MWWorld::Ptr& actor) = 0; 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, virtual void launchMagicBolt(const ESM::RefId& spellId, const MWWorld::Ptr& caster,
const osg::Vec3f& fallbackDirection, ESM::RefNum item) const osg::Vec3f& fallbackDirection, ESM::RefNum item)

@ -136,7 +136,7 @@ namespace MWClass
const ESM::MagicEffect* effect = store.get<ESM::MagicEffect>().find(ESM::MagicEffect::Telekinesis); const ESM::MagicEffect* effect = store.get<ESM::MagicEffect>().find(ESM::MagicEffect::Telekinesis);
animation->addSpellCastGlow( 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
} }
} }

@ -273,7 +273,7 @@ namespace MWGui
void EnchantingDialog::notifyEffectsChanged() void EnchantingDialog::notifyEffectsChanged()
{ {
mEffectList.mList = mEffects; mEffectList.populate(mEffects);
mEnchanting.setEffect(mEffectList); mEnchanting.setEffect(mEffectList);
updateLabels(); updateLabels();
} }

@ -427,7 +427,7 @@ namespace MWGui
{ {
// use the icon of the first effect // use the icon of the first effect
const ESM::MagicEffect* effect = MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find( const ESM::MagicEffect* effect = MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(
spell->mEffects.mList.front().mEffectID); spell->mEffects.mList.front().mData.mEffectID);
std::string icon = effect->mIcon; std::string icon = effect->mIcon;
std::replace(icon.begin(), icon.end(), '/', '\\'); std::replace(icon.begin(), icon.end(), '/', '\\');
size_t slashPos = icon.rfind('\\'); size_t slashPos = icon.rfind('\\');

@ -299,7 +299,8 @@ namespace MWGui
mSelected->button->setUserString("Spell", spellId.serialize()); mSelected->button->setUserString("Spell", spellId.serialize());
// use the icon of the first effect // use the icon of the first effect
const ESM::MagicEffect* effect = esmStore.get<ESM::MagicEffect>().find(spell->mEffects.mList.front().mEffectID); const ESM::MagicEffect* effect
= esmStore.get<ESM::MagicEffect>().find(spell->mEffects.mList.front().mData.mEffectID);
std::string path = effect->mIcon; std::string path = effect->mIcon;
std::replace(path.begin(), path.end(), '/', '\\'); std::replace(path.begin(), path.end(), '/', '\\');

@ -470,9 +470,7 @@ namespace MWGui
y *= 1.5; y *= 1.5;
} }
ESM::EffectList effectList; mSpell.mEffects.populate(mEffects);
effectList.mList = mEffects;
mSpell.mEffects = std::move(effectList);
mSpell.mData.mCost = int(y); mSpell.mData.mCost = int(y);
mSpell.mData.mType = ESM::Spell::ST_Spell; mSpell.mData.mType = ESM::Spell::ST_Spell;
mSpell.mData.mFlags = 0; mSpell.mData.mFlags = 0;
@ -528,10 +526,11 @@ namespace MWGui
if (spell->mData.mType != ESM::Spell::ST_Spell) if (spell->mData.mType != ESM::Spell::ST_Spell)
continue; 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 const ESM::MagicEffect* effect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(effectInfo.mEffectID); = MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(effectId);
// skip effects that do not allow spellmaking/enchanting // skip effects that do not allow spellmaking/enchanting
int requiredFlags int requiredFlags
@ -539,8 +538,8 @@ namespace MWGui
if (!(effect->mData.mFlags & requiredFlags)) if (!(effect->mData.mFlags & requiredFlags))
continue; continue;
if (std::find(knownEffects.begin(), knownEffects.end(), effectInfo.mEffectID) == knownEffects.end()) if (std::find(knownEffects.begin(), knownEffects.end(), effectId) == knownEffects.end())
knownEffects.push_back(effectInfo.mEffectID); knownEffects.push_back(effectId);
} }
} }

@ -48,14 +48,14 @@ namespace MWGui
for (const auto& effect : effects.mList) for (const auto& effect : effects.mList)
{ {
short effectId = effect.mEffectID; short effectId = effect.mData.mEffectID;
if (effectId != -1) if (effectId != -1)
{ {
const ESM::MagicEffect* magicEffect = store.get<ESM::MagicEffect>().find(effectId); const ESM::MagicEffect* magicEffect = store.get<ESM::MagicEffect>().find(effectId);
const ESM::Attribute* attribute const ESM::Attribute* attribute
= store.get<ESM::Attribute>().search(ESM::Attribute::indexToRefId(effect.mAttribute)); = store.get<ESM::Attribute>().search(ESM::Attribute::indexToRefId(effect.mData.mAttribute));
const ESM::Skill* skill = store.get<ESM::Skill>().search(ESM::Skill::indexToRefId(effect.mSkill)); const ESM::Skill* skill = store.get<ESM::Skill>().search(ESM::Skill::indexToRefId(effect.mData.mSkill));
std::string fullEffectName = MWMechanics::getMagicEffectString(*magicEffect, attribute, skill); std::string fullEffectName = MWMechanics::getMagicEffectString(*magicEffect, attribute, skill);
std::string convert = Utf8Stream::lowerCaseUtf8(fullEffectName); std::string convert = Utf8Stream::lowerCaseUtf8(fullEffectName);

@ -222,17 +222,17 @@ namespace MWGui
= store->get<ESM::Spell>().find(ESM::RefId::deserialize(focus->getUserString("Spell"))); = store->get<ESM::Spell>().find(ESM::RefId::deserialize(focus->getUserString("Spell")));
info.caption = spell->mName; info.caption = spell->mName;
Widgets::SpellEffectList effects; Widgets::SpellEffectList effects;
for (const ESM::ENAMstruct& spellEffect : spell->mEffects.mList) for (const ESM::IndexedENAMstruct& spellEffect : spell->mEffects.mList)
{ {
Widgets::SpellEffectParams params; Widgets::SpellEffectParams params;
params.mEffectID = spellEffect.mEffectID; params.mEffectID = spellEffect.mData.mEffectID;
params.mSkill = ESM::Skill::indexToRefId(spellEffect.mSkill); params.mSkill = ESM::Skill::indexToRefId(spellEffect.mData.mSkill);
params.mAttribute = ESM::Attribute::indexToRefId(spellEffect.mAttribute); params.mAttribute = ESM::Attribute::indexToRefId(spellEffect.mData.mAttribute);
params.mDuration = spellEffect.mDuration; params.mDuration = spellEffect.mData.mDuration;
params.mMagnMin = spellEffect.mMagnMin; params.mMagnMin = spellEffect.mData.mMagnMin;
params.mMagnMax = spellEffect.mMagnMax; params.mMagnMax = spellEffect.mData.mMagnMax;
params.mRange = spellEffect.mRange; params.mRange = spellEffect.mData.mRange;
params.mArea = spellEffect.mArea; params.mArea = spellEffect.mData.mArea;
params.mIsConstant = (spell->mData.mType == ESM::Spell::ST_Ability); params.mIsConstant = (spell->mData.mType == ESM::Spell::ST_Ability);
params.mNoTarget = false; params.mNoTarget = false;
effects.push_back(params); effects.push_back(params);

@ -195,18 +195,18 @@ namespace MWGui::Widgets
const ESM::Spell* spell = store.get<ESM::Spell>().search(mId); const ESM::Spell* spell = store.get<ESM::Spell>().search(mId);
MYGUI_ASSERT(spell, "spell with id '" << mId << "' not found"); 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 MWSpellEffectPtr effect
= creator->createWidget<MWSpellEffect>("MW_EffectImage", coord, MyGUI::Align::Default); = creator->createWidget<MWSpellEffect>("MW_EffectImage", coord, MyGUI::Align::Default);
SpellEffectParams params; SpellEffectParams params;
params.mEffectID = effectInfo.mEffectID; params.mEffectID = effectInfo.mData.mEffectID;
params.mSkill = ESM::Skill::indexToRefId(effectInfo.mSkill); params.mSkill = ESM::Skill::indexToRefId(effectInfo.mData.mSkill);
params.mAttribute = ESM::Attribute::indexToRefId(effectInfo.mAttribute); params.mAttribute = ESM::Attribute::indexToRefId(effectInfo.mData.mAttribute);
params.mDuration = effectInfo.mDuration; params.mDuration = effectInfo.mData.mDuration;
params.mMagnMin = effectInfo.mMagnMin; params.mMagnMin = effectInfo.mData.mMagnMin;
params.mMagnMax = effectInfo.mMagnMax; params.mMagnMax = effectInfo.mData.mMagnMax;
params.mRange = effectInfo.mRange; params.mRange = effectInfo.mData.mRange;
params.mIsConstant = (flags & MWEffectList::EF_Constant) != 0; params.mIsConstant = (flags & MWEffectList::EF_Constant) != 0;
params.mNoTarget = (flags & MWEffectList::EF_NoTarget); params.mNoTarget = (flags & MWEffectList::EF_NoTarget);
params.mNoMagnitude = (flags & MWEffectList::EF_NoMagnitude); params.mNoMagnitude = (flags & MWEffectList::EF_NoMagnitude);
@ -308,17 +308,17 @@ namespace MWGui::Widgets
SpellEffectList MWEffectList::effectListFromESM(const ESM::EffectList* effects) SpellEffectList MWEffectList::effectListFromESM(const ESM::EffectList* effects)
{ {
SpellEffectList result; SpellEffectList result;
for (const ESM::ENAMstruct& effectInfo : effects->mList) for (const ESM::IndexedENAMstruct& effectInfo : effects->mList)
{ {
SpellEffectParams params; SpellEffectParams params;
params.mEffectID = effectInfo.mEffectID; params.mEffectID = effectInfo.mData.mEffectID;
params.mSkill = ESM::Skill::indexToRefId(effectInfo.mSkill); params.mSkill = ESM::Skill::indexToRefId(effectInfo.mData.mSkill);
params.mAttribute = ESM::Attribute::indexToRefId(effectInfo.mAttribute); params.mAttribute = ESM::Attribute::indexToRefId(effectInfo.mData.mAttribute);
params.mDuration = effectInfo.mDuration; params.mDuration = effectInfo.mData.mDuration;
params.mMagnMin = effectInfo.mMagnMin; params.mMagnMin = effectInfo.mData.mMagnMin;
params.mMagnMax = effectInfo.mMagnMax; params.mMagnMax = effectInfo.mData.mMagnMax;
params.mRange = effectInfo.mRange; params.mRange = effectInfo.mData.mRange;
params.mArea = effectInfo.mArea; params.mArea = effectInfo.mData.mArea;
result.push_back(params); result.push_back(params);
} }
return result; return result;

@ -13,12 +13,14 @@
#include <components/lua/luastate.hpp> #include <components/lua/luastate.hpp>
#include <components/misc/color.hpp> #include <components/misc/color.hpp>
#include <components/misc/resourcehelpers.hpp> #include <components/misc/resourcehelpers.hpp>
#include <components/misc/strings/format.hpp>
#include <components/resource/resourcesystem.hpp> #include <components/resource/resourcesystem.hpp>
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "../mwmechanics/activespells.hpp" #include "../mwmechanics/activespells.hpp"
#include "../mwmechanics/actorutil.hpp"
#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/creaturestats.hpp"
#include "../mwmechanics/magiceffects.hpp" #include "../mwmechanics/magiceffects.hpp"
#include "../mwmechanics/spellutil.hpp" #include "../mwmechanics/spellutil.hpp"
@ -141,7 +143,7 @@ namespace sol
{ {
}; };
template <> template <>
struct is_automagical<ESM::ENAMstruct> : std::false_type struct is_automagical<ESM::IndexedENAMstruct> : std::false_type
{ {
}; };
template <> template <>
@ -189,6 +191,26 @@ namespace MWLua
return ESM::RefId::deserializeText(LuaUtil::cast<std::string_view>(recordOrId)); return ESM::RefId::deserializeText(LuaUtil::cast<std::string_view>(recordOrId));
} }
static const ESM::Spell* toSpell(const sol::object& spellOrId)
{
if (spellOrId.is<ESM::Spell>())
return spellOrId.as<const ESM::Spell*>();
else
{
auto& store = MWBase::Environment::get().getWorld()->getStore();
auto refId = ESM::RefId::deserializeText(LuaUtil::cast<std::string_view>(spellOrId));
return store.get<ESM::Spell>().find(refId);
}
}
static sol::table effectParamsListToTable(sol::state_view& lua, const std::vector<ESM::IndexedENAMstruct>& 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::table initCoreMagicBindings(const Context& context)
{ {
sol::state_view& lua = context.mLua->sol(); sol::state_view& lua = context.mLua->sol();
@ -281,12 +303,12 @@ namespace MWLua
spellT["name"] = sol::readonly_property([](const ESM::Spell& rec) -> std::string_view { return rec.mName; }); 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["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["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 { spellT["alwaysSucceedFlag"] = sol::readonly_property(
sol::table res(lua, sol::create); [](const ESM::Spell& rec) -> bool { return !!(rec.mData.mFlags & ESM::Spell::F_Always); });
for (size_t i = 0; i < rec.mEffects.mList.size(); ++i) spellT["autocalcFlag"] = sol::readonly_property(
res[i + 1] = rec.mEffects.mList[i]; // ESM::ENAMstruct (effect params) [](const ESM::Spell& rec) -> bool { return !!(rec.mData.mFlags & ESM::Spell::F_Autocalc); });
return res; spellT["effects"] = sol::readonly_property(
}); [&lua](const ESM::Spell& rec) -> sol::table { return effectParamsListToTable(lua, rec.mEffects.mList); });
// Enchantment record // Enchantment record
auto enchantT = lua.new_usertype<ESM::Enchantment>("ESM3_Enchantment"); auto enchantT = lua.new_usertype<ESM::Enchantment>("ESM3_Enchantment");
@ -301,46 +323,49 @@ namespace MWLua
enchantT["charge"] enchantT["charge"]
= sol::readonly_property([](const ESM::Enchantment& rec) -> int { return rec.mData.mCharge; }); = sol::readonly_property([](const ESM::Enchantment& rec) -> int { return rec.mData.mCharge; });
enchantT["effects"] = sol::readonly_property([&lua](const ESM::Enchantment& rec) -> sol::table { enchantT["effects"] = sol::readonly_property([&lua](const ESM::Enchantment& rec) -> sol::table {
sol::table res(lua, sol::create); return effectParamsListToTable(lua, rec.mEffects.mList);
for (size_t i = 0; i < rec.mEffects.mList.size(); ++i)
res[i + 1] = rec.mEffects.mList[i]; // ESM::ENAMstruct (effect params)
return res;
}); });
// Effect params // Effect params
auto effectParamsT = lua.new_usertype<ESM::ENAMstruct>("ESM3_EffectParams"); auto effectParamsT = lua.new_usertype<ESM::IndexedENAMstruct>("ESM3_EffectParams");
effectParamsT[sol::meta_function::to_string] = [magicEffectStore](const ESM::ENAMstruct& params) { effectParamsT[sol::meta_function::to_string] = [magicEffectStore](const ESM::IndexedENAMstruct& params) {
const ESM::MagicEffect* const rec = magicEffectStore->find(params.mEffectID); const ESM::MagicEffect* const rec = magicEffectStore->find(params.mData.mEffectID);
return "ESM3_EffectParams[" + ESM::MagicEffect::indexToGmstString(rec->mIndex) + "]"; return "ESM3_EffectParams[" + ESM::MagicEffect::indexToGmstString(rec->mIndex) + "]";
}; };
effectParamsT["effect"] effectParamsT["effect"] = sol::readonly_property(
= sol::readonly_property([magicEffectStore](const ESM::ENAMstruct& params) -> const ESM::MagicEffect* { [magicEffectStore](const ESM::IndexedENAMstruct& params) -> const ESM::MagicEffect* {
return magicEffectStore->find(params.mEffectID); 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"] effectParamsT["affectedSkill"]
= sol::readonly_property([](const ESM::ENAMstruct& params) -> sol::optional<std::string> { = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> sol::optional<std::string> {
ESM::RefId id = ESM::Skill::indexToRefId(params.mSkill); ESM::RefId id = ESM::Skill::indexToRefId(params.mData.mSkill);
if (!id.empty()) if (!id.empty())
return id.serializeText(); return id.serializeText();
return sol::nullopt; return sol::nullopt;
}); });
effectParamsT["affectedAttribute"] effectParamsT["affectedAttribute"]
= sol::readonly_property([](const ESM::ENAMstruct& params) -> sol::optional<std::string> { = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> sol::optional<std::string> {
ESM::RefId id = ESM::Attribute::indexToRefId(params.mAttribute); ESM::RefId id = ESM::Attribute::indexToRefId(params.mData.mAttribute);
if (!id.empty()) if (!id.empty())
return id.serializeText(); return id.serializeText();
return sol::nullopt; return sol::nullopt;
}); });
effectParamsT["range"] 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"] 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"] 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"] effectParamsT["magnitudeMax"]
= sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mMagnMax; }); = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mMagnMax; });
effectParamsT["duration"] effectParamsT["duration"] = sol::readonly_property(
= sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mDuration; }); [](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mDuration; });
effectParamsT["index"]
= sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mIndex; });
// MagicEffect record // MagicEffect record
auto magicEffectT = context.mLua->sol().new_usertype<ESM::MagicEffect>("ESM3_MagicEffect"); auto magicEffectT = context.mLua->sol().new_usertype<ESM::MagicEffect>("ESM3_MagicEffect");
@ -361,12 +386,22 @@ namespace MWLua
magicEffectT["continuousVfx"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> bool { magicEffectT["continuousVfx"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> bool {
return (rec.mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; 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(); }); [](const ESM::MagicEffect& rec) -> std::string { return rec.mCasting.serializeText(); });
magicEffectT["hitStatic"] = sol::readonly_property( magicEffectT["hitStatic"] = sol::readonly_property(
[](const ESM::MagicEffect& rec) -> std::string { return rec.mHit.serializeText(); }); [](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 { magicEffectT["name"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> std::string_view {
return MWBase::Environment::get() return MWBase::Environment::get()
.getWorld() .getWorld()
@ -382,8 +417,20 @@ namespace MWLua
magicEffectT["color"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> Misc::Color { 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); 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( magicEffectT["harmful"] = sol::readonly_property(
[](const ESM::MagicEffect& rec) -> bool { return rec.mData.mFlags & ESM::MagicEffect::Harmful; }); [](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? // TODO: Should we expose it? What happens if a spell has several effects with different projectileSpeed?
// magicEffectT["projectileSpeed"] // magicEffectT["projectileSpeed"]
@ -397,6 +444,8 @@ namespace MWLua
auto name = ESM::MagicEffect::indexToName(effect.mEffectId); auto name = ESM::MagicEffect::indexToName(effect.mEffectId);
return Misc::StringUtils::lowerCase(name); 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 { activeSpellEffectT["name"] = sol::readonly_property([](const ESM::ActiveEffect& effect) -> std::string {
return MWMechanics::EffectKey(effect.mEffectId, effect.getSkillOrAttribute()).toString(); return MWMechanics::EffectKey(effect.mEffectId, effect.getSkillOrAttribute()).toString();
}); });
@ -460,12 +509,13 @@ namespace MWLua
auto activeSpellT = context.mLua->sol().new_usertype<ActiveSpell>("ActiveSpellParams"); auto activeSpellT = context.mLua->sol().new_usertype<ActiveSpell>("ActiveSpellParams");
activeSpellT[sol::meta_function::to_string] = [](const ActiveSpell& activeSpell) { 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( activeSpellT["name"] = sol::readonly_property(
[](const ActiveSpell& activeSpell) -> std::string_view { return activeSpell.mParams.getDisplayName(); }); [](const ActiveSpell& activeSpell) -> std::string_view { return activeSpell.mParams.getDisplayName(); });
activeSpellT["id"] = sol::readonly_property( activeSpellT["id"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> std::string {
[](const ActiveSpell& activeSpell) -> std::string { return activeSpell.mParams.getId().serializeText(); }); return activeSpell.mParams.getSourceSpellId().serializeText();
});
activeSpellT["item"] = sol::readonly_property([&lua](const ActiveSpell& activeSpell) -> sol::object { activeSpellT["item"] = sol::readonly_property([&lua](const ActiveSpell& activeSpell) -> sol::object {
auto item = activeSpell.mParams.getItem(); auto item = activeSpell.mParams.getItem();
if (!item.isSet()) if (!item.isSet())
@ -502,6 +552,21 @@ namespace MWLua
} }
return res; 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>("ActiveEffect"); auto activeEffectT = context.mLua->sol().new_usertype<ActiveEffect>("ActiveEffect");
@ -540,6 +605,78 @@ namespace MWLua
return LuaUtil::makeReadOnly(magicApi); return LuaUtil::makeReadOnly(magicApi);
} }
static std::pair<std::string_view, std::vector<ESM::IndexedENAMstruct>> getNameAndMagicEffects(
const MWWorld::Ptr& actor, ESM::RefId id, const sol::table& effects, bool quiet)
{
std::vector<int32_t> effectIndexes;
for (const auto& entry : effects)
{
if (entry.second.is<int32_t>())
effectIndexes.push_back(entry.second.as<int32_t>());
else if (entry.second.is<ESM::IndexedENAMstruct>())
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<ESM::IndexedENAMstruct> enams;
for (auto index : effectIndexes)
enams.push_back(effects.mList.at(index));
return enams;
};
auto getNameAndEffects = [&](auto* record) {
return std::pair<std::string_view, std::vector<ESM::IndexedENAMstruct>>(
record->mName, getEffectsFromIndexes(record->mEffects));
};
auto getNameAndEffectsEnch = [&](auto* record) {
auto* enchantment = esmStore.get<ESM::Enchantment>().find(record->mEnchant);
return std::pair<std::string_view, std::vector<ESM::IndexedENAMstruct>>(
record->mName, getEffectsFromIndexes(enchantment->mEffects));
};
switch (esmStore.find(id))
{
case ESM::REC_ALCH:
return getNameAndEffects(esmStore.get<ESM::Potion>().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<ESM::Ingredient>().find(id);
std::vector<ESM::IndexedENAMstruct> 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<ESM::GameSetting>().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<ESM::Armor>().find(id));
case ESM::REC_BOOK:
return getNameAndEffectsEnch(esmStore.get<ESM::Book>().find(id));
case ESM::REC_CLOT:
return getNameAndEffectsEnch(esmStore.get<ESM::Clothing>().find(id));
case ESM::REC_WEAP:
return getNameAndEffectsEnch(esmStore.get<ESM::Weapon>().find(id));
default:
// esmStore.find doesn't find REC_SPELs
case ESM::REC_SPEL:
return getNameAndEffects(esmStore.get<ESM::Spell>().find(id));
}
}
void addActorMagicBindings(sol::table& actor, const Context& context) void addActorMagicBindings(sol::table& actor, const Context& context)
{ {
const MWWorld::Store<ESM::Spell>* spellStore const MWWorld::Store<ESM::Spell>* spellStore
@ -698,6 +835,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)) // pairs(types.Actor.activeSpells(o))
activeSpellsT["__pairs"] = [](sol::this_state ts, ActorActiveSpells& self) { activeSpellsT["__pairs"] = [](sol::this_state ts, ActorActiveSpells& self) {
sol::state_view lua(ts); sol::state_view lua(ts);
@ -705,7 +852,7 @@ namespace MWLua
return sol::as_function([lua, self]() mutable -> std::pair<sol::object, sol::object> { return sol::as_function([lua, self]() mutable -> std::pair<sol::object, sol::object> {
if (!self.isEnd()) 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 }); auto params = sol::make_object(lua, ActiveSpell{ self.mActor, *self.mIterator });
self.advance(); self.advance();
return { id, params }; return { id, params };
@ -729,14 +876,97 @@ namespace MWLua
}; };
// types.Actor.activeSpells(o):remove(id) // 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()) if (spells.isLObject())
throw std::runtime_error("Local scripts can modify effect only on the actor they are attached to."); 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()) if (auto* store = spells.getStore())
{ {
store->removeEffects(spells.mActor.ptr(), id); ESM::RefId id = ESM::RefId::deserializeText(options.get<std::string_view>("id"));
sol::optional<Object> item = options.get<sol::optional<Object>>("item");
ESM::RefNum itemId;
if (item)
itemId = item->id();
sol::optional<Object> caster = options.get<sol::optional<Object>>("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<sol::table>("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<std::string_view>("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<ESM::MagicEffect>().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<float>(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());
} }
}; };

@ -47,15 +47,16 @@ namespace MWLua
{ {
if (rec.mData.mEffectID[i] < 0) if (rec.mData.mEffectID[i] < 0)
continue; continue;
ESM::ENAMstruct effect; ESM::IndexedENAMstruct effect;
effect.mEffectID = rec.mData.mEffectID[i]; effect.mData.mEffectID = rec.mData.mEffectID[i];
effect.mSkill = rec.mData.mSkills[i]; effect.mData.mSkill = rec.mData.mSkills[i];
effect.mAttribute = rec.mData.mAttributes[i]; effect.mData.mAttribute = rec.mData.mAttributes[i];
effect.mRange = ESM::RT_Self; effect.mData.mRange = ESM::RT_Self;
effect.mArea = 0; effect.mData.mArea = 0;
effect.mDuration = 0; effect.mData.mDuration = 0;
effect.mMagnMin = 0; effect.mData.mMagnMin = 0;
effect.mMagnMax = 0; effect.mData.mMagnMax = 0;
effect.mIndex = i;
res[i + 1] = effect; res[i + 1] = effect;
} }
return res; return res;

@ -46,7 +46,10 @@ namespace
size_t numEffects = effectsTable.size(); size_t numEffects = effectsTable.size();
potion.mEffects.mList.resize(numEffects); potion.mEffects.mList.resize(numEffects);
for (size_t i = 0; i < numEffects; ++i) for (size_t i = 0; i < numEffects; ++i)
potion.mEffects.mList[i] = LuaUtil::cast<ESM::ENAMstruct>(effectsTable[i + 1]); {
potion.mEffects.mList[i] = LuaUtil::cast<ESM::IndexedENAMstruct>(effectsTable[i + 1]);
}
potion.mEffects.updateIndexes();
} }
return potion; return potion;
} }
@ -83,7 +86,7 @@ namespace MWLua
record["effects"] = sol::readonly_property([context](const ESM::Potion& rec) -> sol::table { record["effects"] = sol::readonly_property([context](const ESM::Potion& rec) -> sol::table {
sol::table res(context.mLua->sol(), sol::create); sol::table res(context.mLua->sol(), sol::create);
for (size_t i = 0; i < rec.mEffects.mList.size(); ++i) 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; return res;
}); });
} }

@ -8,6 +8,7 @@
#include <components/misc/strings/algorithm.hpp> #include <components/misc/strings/algorithm.hpp>
#include <components/esm/generatedrefid.hpp>
#include <components/esm3/loadench.hpp> #include <components/esm3/loadench.hpp>
#include <components/esm3/loadmgef.hpp> #include <components/esm3/loadmgef.hpp>
#include <components/esm3/loadstat.hpp> #include <components/esm3/loadstat.hpp>
@ -49,16 +50,15 @@ namespace
void addEffects( void addEffects(
std::vector<ESM::ActiveEffect>& effects, const ESM::EffectList& list, bool ignoreResistances = false) std::vector<ESM::ActiveEffect>& effects, const ESM::EffectList& list, bool ignoreResistances = false)
{ {
int currentEffectIndex = 0;
for (const auto& enam : list.mList) for (const auto& enam : list.mList)
{ {
ESM::ActiveEffect effect; ESM::ActiveEffect effect;
effect.mEffectId = enam.mEffectID; effect.mEffectId = enam.mData.mEffectID;
effect.mArg = MWMechanics::EffectKey(enam).mArg; effect.mArg = MWMechanics::EffectKey(enam.mData).mArg;
effect.mMagnitude = 0.f; effect.mMagnitude = 0.f;
effect.mMinMagnitude = enam.mMagnMin; effect.mMinMagnitude = enam.mData.mMagnMin;
effect.mMaxMagnitude = enam.mMagnMax; effect.mMaxMagnitude = enam.mData.mMagnMax;
effect.mEffectIndex = currentEffectIndex++; effect.mEffectIndex = enam.mIndex;
effect.mFlags = ESM::ActiveEffect::Flag_None; effect.mFlags = ESM::ActiveEffect::Flag_None;
if (ignoreResistances) if (ignoreResistances)
effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Resistances; effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Resistances;
@ -82,12 +82,13 @@ namespace MWMechanics
mActiveSpells.mIterating = false; mActiveSpells.mIterating = false;
} }
ActiveSpells::ActiveSpellParams::ActiveSpellParams(const CastSpell& cast, const MWWorld::Ptr& caster) ActiveSpells::ActiveSpellParams::ActiveSpellParams(
: mId(cast.mId) const MWWorld::Ptr& caster, const ESM::RefId& id, std::string_view sourceName, ESM::RefNum item)
, mDisplayName(cast.mSourceName) : mSourceSpellId(id)
, mDisplayName(sourceName)
, mCasterActorId(-1) , mCasterActorId(-1)
, mItem(cast.mItem) , mItem(item)
, mType(cast.mType) , mFlags()
, mWorsenings(-1) , mWorsenings(-1)
{ {
if (!caster.isEmpty() && caster.getClass().isActor()) if (!caster.isEmpty() && caster.getClass().isActor())
@ -96,48 +97,52 @@ namespace MWMechanics
ActiveSpells::ActiveSpellParams::ActiveSpellParams( ActiveSpells::ActiveSpellParams::ActiveSpellParams(
const ESM::Spell* spell, const MWWorld::Ptr& actor, bool ignoreResistances) const ESM::Spell* spell, const MWWorld::Ptr& actor, bool ignoreResistances)
: mId(spell->mId) : mSourceSpellId(spell->mId)
, mDisplayName(spell->mName) , mDisplayName(spell->mName)
, mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()) , mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId())
, mType(spell->mData.mType == ESM::Spell::ST_Ability ? ESM::ActiveSpells::Type_Ability , mFlags()
: ESM::ActiveSpells::Type_Permanent)
, mWorsenings(-1) , mWorsenings(-1)
{ {
assert(spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power); 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); addEffects(mEffects, spell->mEffects, ignoreResistances);
} }
ActiveSpells::ActiveSpellParams::ActiveSpellParams( ActiveSpells::ActiveSpellParams::ActiveSpellParams(
const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, const MWWorld::Ptr& actor) 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)) , mDisplayName(item.getClass().getName(item))
, mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()) , mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId())
, mItem(item.getCellRef().getRefNum()) , mItem(item.getCellRef().getRefNum())
, mType(ESM::ActiveSpells::Type_Enchantment) , mFlags()
, mWorsenings(-1) , mWorsenings(-1)
{ {
assert(enchantment->mData.mType == ESM::Enchantment::ConstantEffect); assert(enchantment->mData.mType == ESM::Enchantment::ConstantEffect);
addEffects(mEffects, enchantment->mEffects); addEffects(mEffects, enchantment->mEffects);
setFlag(ESM::ActiveSpells::Flag_Equipment);
} }
ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ESM::ActiveSpells::ActiveSpellParams& params) ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ESM::ActiveSpells::ActiveSpellParams& params)
: mId(params.mId) : mActiveSpellId(params.mActiveSpellId)
, mSourceSpellId(params.mSourceSpellId)
, mEffects(params.mEffects) , mEffects(params.mEffects)
, mDisplayName(params.mDisplayName) , mDisplayName(params.mDisplayName)
, mCasterActorId(params.mCasterActorId) , mCasterActorId(params.mCasterActorId)
, mItem(params.mItem) , mItem(params.mItem)
, mType(params.mType) , mFlags(params.mFlags)
, mWorsenings(params.mWorsenings) , mWorsenings(params.mWorsenings)
, mNextWorsening({ params.mNextWorsening }) , mNextWorsening({ params.mNextWorsening })
{ {
} }
ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ActiveSpellParams& params, const MWWorld::Ptr& actor) ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ActiveSpellParams& params, const MWWorld::Ptr& actor)
: mId(params.mId) : mSourceSpellId(params.mSourceSpellId)
, mDisplayName(params.mDisplayName) , mDisplayName(params.mDisplayName)
, mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId()) , mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId())
, mItem(params.mItem) , mItem(params.mItem)
, mType(params.mType) , mFlags(params.mFlags)
, mWorsenings(-1) , mWorsenings(-1)
{ {
} }
@ -145,17 +150,23 @@ namespace MWMechanics
ESM::ActiveSpells::ActiveSpellParams ActiveSpells::ActiveSpellParams::toEsm() const ESM::ActiveSpells::ActiveSpellParams ActiveSpells::ActiveSpellParams::toEsm() const
{ {
ESM::ActiveSpells::ActiveSpellParams params; ESM::ActiveSpells::ActiveSpellParams params;
params.mId = mId; params.mActiveSpellId = mActiveSpellId;
params.mSourceSpellId = mSourceSpellId;
params.mEffects = mEffects; params.mEffects = mEffects;
params.mDisplayName = mDisplayName; params.mDisplayName = mDisplayName;
params.mCasterActorId = mCasterActorId; params.mCasterActorId = mCasterActorId;
params.mItem = mItem; params.mItem = mItem;
params.mType = mType; params.mFlags = mFlags;
params.mWorsenings = mWorsenings; params.mWorsenings = mWorsenings;
params.mNextWorsening = mNextWorsening.toEsm(); params.mNextWorsening = mNextWorsening.toEsm();
return params; return params;
} }
void ActiveSpells::ActiveSpellParams::setFlag(ESM::ActiveSpells::Flags flag)
{
mFlags = static_cast<ESM::ActiveSpells::Flags>(mFlags | flag);
}
void ActiveSpells::ActiveSpellParams::worsen() void ActiveSpells::ActiveSpellParams::worsen()
{ {
++mWorsenings; ++mWorsenings;
@ -178,21 +189,31 @@ namespace MWMechanics
{ {
// Enchantment id is not stored directly. Instead the enchanted item is stored. // Enchantment id is not stored directly. Instead the enchanted item is stored.
const auto& store = MWBase::Environment::get().getESMStore(); const auto& store = MWBase::Environment::get().getESMStore();
switch (store->find(mId)) switch (store->find(mSourceSpellId))
{ {
case ESM::REC_ARMO: case ESM::REC_ARMO:
return store->get<ESM::Armor>().find(mId)->mEnchant; return store->get<ESM::Armor>().find(mSourceSpellId)->mEnchant;
case ESM::REC_BOOK: case ESM::REC_BOOK:
return store->get<ESM::Book>().find(mId)->mEnchant; return store->get<ESM::Book>().find(mSourceSpellId)->mEnchant;
case ESM::REC_CLOT: case ESM::REC_CLOT:
return store->get<ESM::Clothing>().find(mId)->mEnchant; return store->get<ESM::Clothing>().find(mSourceSpellId)->mEnchant;
case ESM::REC_WEAP: case ESM::REC_WEAP:
return store->get<ESM::Weapon>().find(mId)->mEnchant; return store->get<ESM::Weapon>().find(mSourceSpellId)->mEnchant;
default: default:
return {}; return {};
} }
} }
const ESM::Spell* ActiveSpells::ActiveSpellParams::getSpell() const
{
return MWBase::Environment::get().getESMStore()->get<ESM::Spell>().search(getSourceSpellId());
}
bool ActiveSpells::ActiveSpellParams::hasFlag(ESM::ActiveSpells::Flags flags) const
{
return static_cast<ESM::ActiveSpells::Flags>(mFlags & flags) == flags;
}
void ActiveSpells::update(const MWWorld::Ptr& ptr, float duration) void ActiveSpells::update(const MWWorld::Ptr& ptr, float duration)
{ {
if (mIterating) if (mIterating)
@ -203,8 +224,7 @@ namespace MWMechanics
// Erase no longer active spells and effects // Erase no longer active spells and effects
for (auto spellIt = mSpells.begin(); spellIt != mSpells.end();) for (auto spellIt = mSpells.begin(); spellIt != mSpells.end();)
{ {
if (spellIt->mType != ESM::ActiveSpells::Type_Temporary if (!spellIt->hasFlag(ESM::ActiveSpells::Flag_Temporary))
&& spellIt->mType != ESM::ActiveSpells::Type_Consumable)
{ {
++spellIt; ++spellIt;
continue; continue;
@ -244,7 +264,10 @@ namespace MWMechanics
{ {
if (spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power if (spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power
&& !isSpellActive(spell->mId)) && !isSpellActive(spell->mId))
{
mSpells.emplace_back(ActiveSpellParams{ spell, ptr }); mSpells.emplace_back(ActiveSpellParams{ spell, ptr });
mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId());
}
} }
bool updateSpellWindow = false; bool updateSpellWindow = false;
@ -270,8 +293,8 @@ namespace MWMechanics
if (std::find_if(mSpells.begin(), mSpells.end(), if (std::find_if(mSpells.begin(), mSpells.end(),
[&](const ActiveSpellParams& params) { [&](const ActiveSpellParams& params) {
return params.mItem == slot->getCellRef().getRefNum() return params.mItem == slot->getCellRef().getRefNum()
&& params.mType == ESM::ActiveSpells::Type_Enchantment && params.hasFlag(ESM::ActiveSpells::Flag_Equipment)
&& params.mId == slot->getCellRef().getRefId(); && params.mSourceSpellId == slot->getCellRef().getRefId();
}) })
!= mSpells.end()) != mSpells.end())
continue; continue;
@ -279,8 +302,8 @@ namespace MWMechanics
// invisibility manually // invisibility manually
purgeEffect(ptr, ESM::MagicEffect::Invisibility); purgeEffect(ptr, ESM::MagicEffect::Invisibility);
applyPurges(ptr); applyPurges(ptr);
const ActiveSpellParams& params ActiveSpellParams& params = mSpells.emplace_back(ActiveSpellParams{ *slot, enchantment, ptr });
= mSpells.emplace_back(ActiveSpellParams{ *slot, enchantment, ptr }); params.setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId());
for (const auto& effect : params.mEffects) for (const auto& effect : params.mEffects)
MWMechanics::playEffects( MWMechanics::playEffects(
ptr, *world->getStore().get<ESM::MagicEffect>().find(effect.mEffectId), playNonLooping); ptr, *world->getStore().get<ESM::MagicEffect>().find(effect.mEffectId), playNonLooping);
@ -350,12 +373,11 @@ namespace MWMechanics
continue; continue;
bool remove = false; bool remove = false;
if (spellIt->mType == ESM::ActiveSpells::Type_Ability if (spellIt->hasFlag(ESM::ActiveSpells::Flag_SpellStore))
|| spellIt->mType == ESM::ActiveSpells::Type_Permanent)
{ {
try try
{ {
remove = !spells.hasSpell(spellIt->mId); remove = !spells.hasSpell(spellIt->mSourceSpellId);
} }
catch (const std::runtime_error& e) catch (const std::runtime_error& e)
{ {
@ -363,9 +385,9 @@ namespace MWMechanics
Log(Debug::Error) << "Removing active effect: " << e.what(); 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); const auto& store = ptr.getClass().getInventoryStore(ptr);
remove = true; remove = true;
for (int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++) 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) 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) { auto found = std::find_if(mSpells.begin(), mSpells.end(), [&](const auto& existing) {
return spell.mId == existing.mId && spell.mCasterActorId == existing.mCasterActorId return spell.mSourceSpellId == existing.mSourceSpellId
&& spell.mItem == existing.mItem; && spell.mCasterActorId == existing.mCasterActorId && spell.mItem == existing.mItem;
}); });
if (found != mSpells.end()) if (found != mSpells.end())
{ {
@ -428,6 +450,7 @@ namespace MWMechanics
} }
} }
mSpells.emplace_back(spell); mSpells.emplace_back(spell);
mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId());
} }
ActiveSpells::ActiveSpells() ActiveSpells::ActiveSpells()
@ -445,10 +468,19 @@ namespace MWMechanics
return mSpells.end(); 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 bool ActiveSpells::isSpellActive(const ESM::RefId& id) const
{ {
return std::find_if(mSpells.begin(), mSpells.end(), [&](const auto& spell) { return spell.mId == id; }) return std::find_if(mSpells.begin(), mSpells.end(), [&](const auto& spell) {
!= mSpells.end(); return spell.mSourceSpellId == id;
}) != mSpells.end();
} }
bool ActiveSpells::isEnchantmentActive(const ESM::RefId& id) const bool ActiveSpells::isEnchantmentActive(const ESM::RefId& id) const
@ -557,9 +589,14 @@ namespace MWMechanics
return removedCurrentSpell; 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) 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) void ActiveSpells::readState(const ESM::ActiveSpells& state)
{ {
for (const ESM::ActiveSpells::ActiveSpellParams& spell : state.mSpells) for (const ESM::ActiveSpells::ActiveSpellParams& spell : state.mSpells)
{
mSpells.emplace_back(ActiveSpellParams{ spell }); 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) for (const ESM::ActiveSpells::ActiveSpellParams& spell : state.mQueue)
mQueue.emplace_back(ActiveSpellParams{ spell }); mQueue.emplace_back(ActiveSpellParams{ spell });
} }
void ActiveSpells::unloadActor(const MWWorld::Ptr& ptr) void ActiveSpells::unloadActor(const MWWorld::Ptr& ptr)
{ {
purge( purge([](const auto& spell) { return spell.hasFlag(ESM::ActiveSpells::Flag_Temporary); }, ptr);
[](const auto& spell) {
return spell.getType() == ESM::ActiveSpells::Type_Consumable
|| spell.getType() == ESM::ActiveSpells::Type_Temporary;
},
ptr);
mQueue.clear(); mQueue.clear();
} }
} }

@ -33,12 +33,13 @@ namespace MWMechanics
using ActiveEffect = ESM::ActiveEffect; using ActiveEffect = ESM::ActiveEffect;
class ActiveSpellParams class ActiveSpellParams
{ {
ESM::RefId mId; ESM::RefId mActiveSpellId;
ESM::RefId mSourceSpellId;
std::vector<ActiveEffect> mEffects; std::vector<ActiveEffect> mEffects;
std::string mDisplayName; std::string mDisplayName;
int mCasterActorId; int mCasterActorId;
ESM::RefNum mItem; ESM::RefNum mItem;
ESM::ActiveSpells::EffectType mType; ESM::ActiveSpells::Flags mFlags;
int mWorsenings; int mWorsenings;
MWWorld::TimeStamp mNextWorsening; MWWorld::TimeStamp mNextWorsening;
MWWorld::Ptr mSource; MWWorld::Ptr mSource;
@ -57,15 +58,17 @@ namespace MWMechanics
friend class ActiveSpells; friend class ActiveSpells;
public: 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<ActiveEffect>& getEffects() const { return mEffects; } const std::vector<ActiveEffect>& getEffects() const { return mEffects; }
std::vector<ActiveEffect>& getEffects() { return mEffects; } std::vector<ActiveEffect>& getEffects() { return mEffects; }
ESM::ActiveSpells::EffectType getType() const { return mType; }
int getCasterActorId() const { return mCasterActorId; } int getCasterActorId() const { return mCasterActorId; }
int getWorsenings() const { return mWorsenings; } int getWorsenings() const { return mWorsenings; }
@ -75,6 +78,10 @@ namespace MWMechanics
ESM::RefNum getItem() const { return mItem; } ESM::RefNum getItem() const { return mItem; }
ESM::RefId getEnchantment() const; 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 // Increments worsenings count and sets the next timestamp
void worsen(); void worsen();
@ -93,6 +100,8 @@ namespace MWMechanics
TIterator end() const; TIterator end() const;
TIterator getActiveSpellById(const ESM::RefId& id);
void update(const MWWorld::Ptr& ptr, float duration); void update(const MWWorld::Ptr& ptr, float duration);
private: private:
@ -132,7 +141,9 @@ namespace MWMechanics
void addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor); void addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor);
/// Removes the active effects from this spell/potion/.. with \a id /// 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 /// Remove all active effects with this effect id
void purgeEffect(const MWWorld::Ptr& ptr, int effectId, ESM::RefId effectArg = {}); void purgeEffect(const MWWorld::Ptr& ptr, int effectId, ESM::RefId effectArg = {});

@ -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); const auto iter = mIndex.find(ptr.mRef);
if (iter != mIndex.end()) 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 bool Actors::isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) const

@ -67,7 +67,7 @@ namespace MWMechanics
void resurrect(const MWWorld::Ptr& ptr) const; 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; void updateActor(const MWWorld::Ptr& old, const MWWorld::Ptr& ptr) const;
///< Updates an actor with a new Ptr ///< Updates an actor with a new Ptr

@ -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) : mTargetId(targetId)
, mSpellId(spellId) , mSpellId(spellId)
, mCasting(false) , mCasting(false)
, mManual(manualSpell) , mScripted(scriptedSpell)
, mDistance(getInitialDistance(spellId)) , mDistance(getInitialDistance(spellId))
{ {
} }
@ -49,7 +49,7 @@ bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::Charac
if (target.isEmpty()) if (target.isEmpty())
return true; return true;
if (!mManual if (!mScripted
&& !pathTo(actor, target.getRefData().getPosition().asVec3(), duration, && !pathTo(actor, target.getRefData().getPosition().asVec3(), duration,
characterController.getSupportedMovementDirections(), mDistance)) characterController.getSupportedMovementDirections(), mDistance))
{ {
@ -85,7 +85,7 @@ bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::Charac
if (!mCasting) if (!mCasting)
{ {
MWBase::Environment::get().getMechanicsManager()->castSpell(actor, mSpellId, mManual); MWBase::Environment::get().getMechanicsManager()->castSpell(actor, mSpellId, mScripted);
mCasting = true; mCasting = true;
return false; return false;
} }

@ -15,7 +15,7 @@ namespace MWMechanics
class AiCast final : public TypedAiPackage<AiCast> class AiCast final : public TypedAiPackage<AiCast>
{ {
public: 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, bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state,
float duration) override; float duration) override;
@ -37,7 +37,7 @@ namespace MWMechanics
const ESM::RefId mTargetId; const ESM::RefId mTargetId;
const ESM::RefId mSpellId; const ESM::RefId mSpellId;
bool mCasting; bool mCasting;
const bool mManual; const bool mScripted;
const float mDistance; const float mDistance;
}; };
} }

@ -275,7 +275,7 @@ namespace MWMechanics
if (!spellId.empty()) if (!spellId.empty())
{ {
const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get<ESM::Spell>().find(spellId); const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get<ESM::Spell>().find(spellId);
if (spell->mEffects.mList.empty() || spell->mEffects.mList[0].mRange != ESM::RT_Target) if (spell->mEffects.mList.empty() || spell->mEffects.mList[0].mData.mRange != ESM::RT_Target)
canShout = false; canShout = false;
} }
storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat, canShout); storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat, canShout);

@ -355,14 +355,14 @@ namespace MWMechanics
{ {
const ESM::Spell* spell const ESM::Spell* spell
= MWBase::Environment::get().getESMStore()->get<ESM::Spell>().find(selectedSpellId); = MWBase::Environment::get().getESMStore()->get<ESM::Spell>().find(selectedSpellId);
for (std::vector<ESM::ENAMstruct>::const_iterator effectIt = spell->mEffects.mList.begin(); for (std::vector<ESM::IndexedENAMstruct>::const_iterator effectIt = spell->mEffects.mList.begin();
effectIt != spell->mEffects.mList.end(); ++effectIt) effectIt != spell->mEffects.mList.end(); ++effectIt)
{ {
if (effectIt->mRange == ESM::RT_Target) if (effectIt->mData.mRange == ESM::RT_Target)
{ {
const ESM::MagicEffect* effect const ESM::MagicEffect* effect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find( = MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(
effectIt->mEffectID); effectIt->mData.mEffectID);
dist = effect->mData.mSpeed; dist = effect->mData.mSpeed;
break; break;
} }
@ -375,14 +375,14 @@ namespace MWMechanics
{ {
const ESM::Enchantment* ench const ESM::Enchantment* ench
= MWBase::Environment::get().getESMStore()->get<ESM::Enchantment>().find(enchId); = MWBase::Environment::get().getESMStore()->get<ESM::Enchantment>().find(enchId);
for (std::vector<ESM::ENAMstruct>::const_iterator effectIt = ench->mEffects.mList.begin(); for (std::vector<ESM::IndexedENAMstruct>::const_iterator effectIt = ench->mEffects.mList.begin();
effectIt != ench->mEffects.mList.end(); ++effectIt) effectIt != ench->mEffects.mList.end(); ++effectIt)
{ {
if (effectIt->mRange == ESM::RT_Target) if (effectIt->mData.mRange == ESM::RT_Target)
{ {
const ESM::MagicEffect* effect const ESM::MagicEffect* effect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find( = MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(
effectIt->mEffectID); effectIt->mData.mEffectID);
dist = effect->mData.mSpeed; dist = effect->mData.mSpeed;
break; break;
} }

@ -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) 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]; const ESM::ENAMstruct& second = mEffects[i];
if (first.mEffectID != second.mEffectID || first.mArea != second.mArea || first.mRange != second.mRange if (first.mData.mEffectID != second.mEffectID || first.mData.mArea != second.mArea
|| first.mSkill != second.mSkill || first.mAttribute != second.mAttribute || first.mData.mRange != second.mRange || first.mData.mSkill != second.mSkill
|| first.mMagnMin != second.mMagnMin || first.mMagnMax != second.mMagnMax || first.mData.mAttribute != second.mAttribute || first.mData.mMagnMin != second.mMagnMin
|| first.mDuration != second.mDuration) || first.mData.mMagnMax != second.mMagnMax || first.mData.mDuration != second.mDuration)
{ {
mismatch = true; mismatch = true;
break; 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.mModel = "m\\misc_potion_" + std::string(meshes[index]) + "_01.nif";
newRecord.mIcon = "m\\tx_potion_" + std::string(meshes[index]) + "_01.dds"; 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); const ESM::Potion* record = getRecord(newRecord);
if (!record) if (!record)

@ -221,7 +221,7 @@ namespace MWMechanics
for (const auto& spellEffect : spell->mEffects.mList) for (const auto& spellEffect : spell->mEffects.mList)
{ {
const ESM::MagicEffect* magicEffect const ESM::MagicEffect* magicEffect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(spellEffect.mEffectID); = MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(spellEffect.mData.mEffectID);
static const int iAutoSpellAttSkillMin = MWBase::Environment::get() static const int iAutoSpellAttSkillMin = MWBase::Environment::get()
.getESMStore() .getESMStore()
->get<ESM::GameSetting>() ->get<ESM::GameSetting>()
@ -230,7 +230,7 @@ namespace MWMechanics
if ((magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill)) 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); auto found = actorSkills.find(skill);
if (found == actorSkills.end() || found->second.getBase() < iAutoSpellAttSkillMin) if (found == actorSkills.end() || found->second.getBase() < iAutoSpellAttSkillMin)
return false; return false;
@ -238,7 +238,7 @@ namespace MWMechanics
if ((magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute)) 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); auto found = actorAttributes.find(attribute);
if (found == actorAttributes.end() || found->second.getBase() < iAutoSpellAttSkillMin) if (found == actorAttributes.end() || found->second.getBase() < iAutoSpellAttSkillMin)
return false; return false;
@ -253,22 +253,22 @@ namespace MWMechanics
{ {
// Morrowind for some reason uses a formula slightly different from magicka cost calculation // Morrowind for some reason uses a formula slightly different from magicka cost calculation
float minChance = std::numeric_limits<float>::max(); float minChance = std::numeric_limits<float>::max();
for (const ESM::ENAMstruct& effect : spell->mEffects.mList) for (const ESM::IndexedENAMstruct& effect : spell->mEffects.mList)
{ {
const ESM::MagicEffect* magicEffect const ESM::MagicEffect* magicEffect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(effect.mEffectID); = MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(effect.mData.mEffectID);
int minMagn = 1; int minMagn = 1;
int maxMagn = 1; int maxMagn = 1;
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude))
{ {
minMagn = effect.mMagnMin; minMagn = effect.mData.mMagnMin;
maxMagn = effect.mMagnMax; maxMagn = effect.mData.mMagnMax;
} }
int duration = 0; int duration = 0;
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration))
duration = effect.mDuration; duration = effect.mData.mDuration;
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce)) if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce))
duration = std::max(1, duration); duration = std::max(1, duration);
@ -281,10 +281,10 @@ namespace MWMechanics
float x = 0.5 * (std::max(1, minMagn) + std::max(1, maxMagn)); float x = 0.5 * (std::max(1, minMagn) + std::max(1, maxMagn));
x *= 0.1 * magicEffect->mData.mBaseCost; x *= 0.1 * magicEffect->mData.mBaseCost;
x *= 1 + duration; 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; x *= fEffectCostMult;
if (effect.mRange == ESM::RT_Target) if (effect.mData.mRange == ESM::RT_Target)
x *= 1.5f; x *= 1.5f;
float s = 0.f; float s = 0.f;

@ -1155,8 +1155,8 @@ namespace MWMechanics
else if (groupname == "spellcast" && action == mAttackType + " release") else if (groupname == "spellcast" && action == mAttackType + " release")
{ {
if (mCanCast) if (mCanCast)
MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell); MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingScriptedSpell);
mCastingManualSpell = false; mCastingScriptedSpell = false;
mCanCast = false; mCanCast = false;
} }
else if (groupname == "containeropen" && action == "loot") else if (groupname == "containeropen" && action == "loot")
@ -1526,9 +1526,9 @@ namespace MWMechanics
bool isMagicItem = false; bool isMagicItem = false;
// Play hand VFX and allow castSpell use (assuming an animation is going to be played) if // 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; MWWorld::SpellCastState spellCastResult = MWWorld::SpellCastState::Success;
if (!mCastingManualSpell) if (!mCastingScriptedSpell)
spellCastResult = world->startSpellCast(mPtr); spellCastResult = world->startSpellCast(mPtr);
mCanCast = spellCastResult == MWWorld::SpellCastState::Success; mCanCast = spellCastResult == MWWorld::SpellCastState::Success;
@ -1558,9 +1558,9 @@ namespace MWMechanics
else if (!spellid.empty() && spellCastResult != MWWorld::SpellCastState::PowerAlreadyUsed) else if (!spellid.empty() && spellCastResult != MWWorld::SpellCastState::PowerAlreadyUsed)
{ {
world->breakInvisibility(mPtr); world->breakInvisibility(mPtr);
MWMechanics::CastSpell cast(mPtr, {}, false, mCastingManualSpell); MWMechanics::CastSpell cast(mPtr, {}, false, mCastingScriptedSpell);
const std::vector<ESM::ENAMstruct>* effects{ nullptr }; const std::vector<ESM::IndexedENAMstruct>* effects{ nullptr };
const MWWorld::ESMStore& store = world->getStore(); const MWWorld::ESMStore& store = world->getStore();
if (isMagicItem) if (isMagicItem)
{ {
@ -1579,7 +1579,7 @@ namespace MWMechanics
if (mCanCast) if (mCanCast)
{ {
const ESM::MagicEffect* effect = store.get<ESM::MagicEffect>().find( const ESM::MagicEffect* effect = store.get<ESM::MagicEffect>().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 const ESM::Static* castStatic
= world->getStore().get<ESM::Static>().find(ESM::RefId::stringRefId("VFX_Hands")); = world->getStore().get<ESM::Static>().find(ESM::RefId::stringRefId("VFX_Hands"));
@ -1593,7 +1593,7 @@ namespace MWMechanics
"", false, "Bip01 R Hand", effect->mParticle); "", false, "Bip01 R Hand", effect->mParticle);
} }
// first effect used for casting animation // first effect used for casting animation
const ESM::ENAMstruct& firstEffect = effects->front(); const ESM::ENAMstruct& firstEffect = effects->front().mData;
std::string startKey; std::string startKey;
std::string stopKey; std::string stopKey;
@ -1602,9 +1602,9 @@ namespace MWMechanics
startKey = "start"; startKey = "start";
stopKey = "stop"; stopKey = "stop";
if (mCanCast) if (mCanCast)
world->castSpell( world->castSpell(mPtr,
mPtr, mCastingManualSpell); // No "release" text key to use, so cast immediately mCastingScriptedSpell); // No "release" text key to use, so cast immediately
mCastingManualSpell = false; mCastingScriptedSpell = false;
mCanCast = false; mCanCast = false;
} }
else else
@ -2735,7 +2735,7 @@ namespace MWMechanics
// Make sure we canceled the current attack or spellcasting, // Make sure we canceled the current attack or spellcasting,
// because we disabled attack animations anyway. // because we disabled attack animations anyway.
mCanCast = false; mCanCast = false;
mCastingManualSpell = false; mCastingScriptedSpell = false;
setAttackingOrSpell(false); setAttackingOrSpell(false);
if (mUpperBodyState != UpperBodyState::None) if (mUpperBodyState != UpperBodyState::None)
mUpperBodyState = UpperBodyState::WeaponEquipped; mUpperBodyState = UpperBodyState::WeaponEquipped;
@ -2887,7 +2887,7 @@ namespace MWMechanics
bool CharacterController::isCastingSpell() const bool CharacterController::isCastingSpell() const
{ {
return mCastingManualSpell || mUpperBodyState == UpperBodyState::Casting; return mCastingScriptedSpell || mUpperBodyState == UpperBodyState::Casting;
} }
bool CharacterController::isReadyToBlock() const bool CharacterController::isReadyToBlock() const
@ -2941,10 +2941,10 @@ namespace MWMechanics
mPtr.getClass().getCreatureStats(mPtr).setAttackingOrSpell(attackingOrSpell); 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); setAttackingOrSpell(true);
mCastingManualSpell = manualSpell; mCastingScriptedSpell = scriptedSpell;
ActionSpell action = ActionSpell(spellId); ActionSpell action = ActionSpell(spellId);
action.prepare(mPtr); action.prepare(mPtr);
} }

@ -192,7 +192,7 @@ namespace MWMechanics
bool mCanCast{ false }; bool mCanCast{ false };
bool mCastingManualSpell{ false }; bool mCastingScriptedSpell{ false };
bool mIsMovingBackward{ false }; bool mIsMovingBackward{ false };
osg::Vec2f mSmoothedSpeed; osg::Vec2f mSmoothedSpeed;
@ -312,7 +312,7 @@ namespace MWMechanics
bool isAttackingOrSpell() const; bool isAttackingOrSpell() const;
void setVisibility(float visibility) 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); void setAIAttackType(std::string_view attackType);
static std::string_view getRandomAttackType(); static std::string_view getRandomAttackType();

@ -198,13 +198,13 @@ namespace MWMechanics
float enchantmentCost = 0.f; float enchantmentCost = 0.f;
float cost = 0.f; float cost = 0.f;
for (const ESM::ENAMstruct& effect : mEffectList.mList) for (const ESM::IndexedENAMstruct& effect : mEffectList.mList)
{ {
float baseCost = (store.get<ESM::MagicEffect>().find(effect.mEffectID))->mData.mBaseCost; float baseCost = (store.get<ESM::MagicEffect>().find(effect.mData.mEffectID))->mData.mBaseCost;
int magMin = std::max(1, effect.mMagnMin); int magMin = std::max(1, effect.mData.mMagnMin);
int magMax = std::max(1, effect.mMagnMax); int magMax = std::max(1, effect.mData.mMagnMax);
int area = std::max(1, effect.mArea); int area = std::max(1, effect.mData.mArea);
float duration = static_cast<float>(effect.mDuration); float duration = static_cast<float>(effect.mData.mDuration);
if (mCastStyle == ESM::Enchantment::ConstantEffect) if (mCastStyle == ESM::Enchantment::ConstantEffect)
duration = fEnchantmentConstantDurationMult; duration = fEnchantmentConstantDurationMult;
@ -212,7 +212,7 @@ namespace MWMechanics
cost = std::max(1.f, cost); cost = std::max(1.f, cost);
if (effect.mRange == ESM::RT_Target) if (effect.mData.mRange == ESM::RT_Target)
cost *= 1.5f; cost *= 1.5f;
enchantmentCost += precise ? cost : std::floor(cost); enchantmentCost += precise ? cost : std::floor(cost);
@ -244,13 +244,7 @@ namespace MWMechanics
for (int i = 0; i < static_cast<int>(iter->mEffects.mList.size()); ++i) for (int i = 0; i < static_cast<int>(iter->mEffects.mList.size()); ++i)
{ {
const ESM::ENAMstruct& first = iter->mEffects.mList[i]; if (iter->mEffects.mList[i] != toFind.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)
{ {
mismatch = true; mismatch = true;
break; break;

@ -261,10 +261,10 @@ namespace MWMechanics
mObjects.addObject(ptr); 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()) if (ptr.getClass().isActor())
mActors.castSpell(ptr, spellId, manualSpell); mActors.castSpell(ptr, spellId, scriptedSpell);
} }
void MechanicsManager::remove(const MWWorld::Ptr& ptr, bool keepActive) void MechanicsManager::remove(const MWWorld::Ptr& ptr, bool keepActive)
@ -1978,11 +1978,7 @@ namespace MWMechanics
// Transforming removes all temporary effects // Transforming removes all temporary effects
actor.getClass().getCreatureStats(actor).getActiveSpells().purge( actor.getClass().getCreatureStats(actor).getActiveSpells().purge(
[](const auto& params) { [](const auto& params) { return params.hasFlag(ESM::ActiveSpells::Flag_Temporary); }, actor);
return params.getType() == ESM::ActiveSpells::Type_Consumable
|| params.getType() == ESM::ActiveSpells::Type_Temporary;
},
actor);
mActors.updateActor(actor, 0.f); mActors.updateActor(actor, 0.f);
if (werewolf) if (werewolf)

@ -202,7 +202,7 @@ namespace MWMechanics
/// Is \a ptr casting spell or using weapon now? /// Is \a ptr casting spell or using weapon now?
bool isAttackingOrSpell(const MWWorld::Ptr& ptr) const override; 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; void processChangedSettings(const Settings::CategorySettingVector& settings) override;

@ -29,11 +29,11 @@
namespace MWMechanics namespace MWMechanics
{ {
CastSpell::CastSpell( 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) : mCaster(caster)
, mTarget(target) , mTarget(target)
, mFromProjectile(fromProjectile) , 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 ESM::EffectList& effects, const MWWorld::Ptr& ignore, ESM::RangeType rangeType) const
{ {
const auto world = MWBase::Environment::get().getWorld(); const auto world = MWBase::Environment::get().getWorld();
std::map<MWWorld::Ptr, std::vector<ESM::ENAMstruct>> toApply; std::map<MWWorld::Ptr, std::vector<ESM::IndexedENAMstruct>> toApply;
int index = -1; int index = -1;
for (const ESM::ENAMstruct& effectInfo : effects.mList) for (const ESM::IndexedENAMstruct& effectInfo : effects.mList)
{ {
++index; ++index;
const ESM::MagicEffect* effect = world->getStore().get<ESM::MagicEffect>().find(effectInfo.mEffectID); const ESM::MagicEffect* effect = world->getStore().get<ESM::MagicEffect>().find(effectInfo.mData.mEffectID);
if (effectInfo.mRange != rangeType if (effectInfo.mData.mRange != rangeType
|| (effectInfo.mArea <= 0 && !ignore.isEmpty() && ignore.getClass().isActor())) || (effectInfo.mData.mArea <= 0 && !ignore.isEmpty() && ignore.getClass().isActor()))
continue; // Not right range type, or not area effect and hit an actor 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 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) && !ignore.getClass().isActor() && !ignore.getClass().hasToolTip(ignore)
&& (mCaster.isEmpty() || mCaster.getClass().isActor())) && (mCaster.isEmpty() || mCaster.getClass().isActor()))
continue; // Don't play explosion for touch spells on non-activatable objects except when spell is from 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; 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( world->spawnEffect(
Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel), texture, mHitPosition, 1.0f); Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel), texture, mHitPosition, 1.0f);
continue; continue;
} }
else else
world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel), texture, mHitPosition, world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel), texture, mHitPosition,
static_cast<float>(effectInfo.mArea * 2)); static_cast<float>(effectInfo.mData.mArea * 2));
// Play explosion sound (make sure to use NoTrack, since we will delete the projectile now) // Play explosion sound (make sure to use NoTrack, since we will delete the projectile now)
{ {
@ -95,7 +95,7 @@ namespace MWMechanics
std::vector<MWWorld::Ptr> objects; std::vector<MWWorld::Ptr> objects;
static const int unitsPerFoot = ceil(Constants::UnitsPerFoot); static const int unitsPerFoot = ceil(Constants::UnitsPerFoot);
MWBase::Environment::get().getMechanicsManager()->getObjectsInRange( MWBase::Environment::get().getMechanicsManager()->getObjectsInRange(
mHitPosition, static_cast<float>(effectInfo.mArea * unitsPerFoot), objects); mHitPosition, static_cast<float>(effectInfo.mData.mArea * unitsPerFoot), objects);
for (const MWWorld::Ptr& affected : objects) for (const MWWorld::Ptr& affected : objects)
{ {
// Ignore actors without collisions here, otherwise it will be possible to hit actors outside processing // Ignore actors without collisions here, otherwise it will be possible to hit actors outside processing
@ -104,13 +104,6 @@ namespace MWMechanics
continue; continue;
auto& list = toApply[affected]; auto& list = toApply[affected];
while (list.size() < static_cast<std::size_t>(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); list.push_back(effectInfo);
} }
} }
@ -151,45 +144,34 @@ namespace MWMechanics
void CastSpell::inflict( void CastSpell::inflict(
const MWWorld::Ptr& target, const ESM::EffectList& effects, ESM::RangeType range, bool exploded) const 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(); const bool targetIsActor = !target.isEmpty() && target.getClass().isActor();
if (targetIsActor) if (targetIsActor)
{ {
// Early-out for characters that have departed.
const auto& stats = target.getClass().getCreatureStats(target); const auto& stats = target.getClass().getCreatureStats(target);
if (stats.isDead() && stats.isDeathAnimationFinished()) if (stats.isDead() && stats.isDeathAnimationFinished())
return; targetIsDeadActor = true;
} }
// If none of the effects need to apply, we can early-out // If none of the effects need to apply, we can early-out
bool found = false; bool found = false;
bool containsRecastable = false; bool containsRecastable = false;
std::vector<const ESM::MagicEffect*> magicEffects;
magicEffects.reserve(effects.mList.size());
const auto& store = MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>(); const auto& store = MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>();
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; found = true;
const ESM::MagicEffect* magicEffect = store.find(effect.mEffectID); const ESM::MagicEffect* magicEffect = store.find(effect.mData.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;
}
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NonRecastable)) if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NonRecastable))
containsRecastable = true; containsRecastable = true;
magicEffects.push_back(magicEffect);
} }
else
magicEffects.push_back(nullptr);
} }
if (!found) if (!found)
return; return;
ActiveSpells::ActiveSpellParams params(*this, mCaster); ActiveSpells::ActiveSpellParams params(mCaster, mId, mSourceName, mItem);
params.setFlag(mFlags);
bool castByPlayer = (!mCaster.isEmpty() && mCaster == getPlayer()); bool castByPlayer = (!mCaster.isEmpty() && mCaster == getPlayer());
const ActiveSpells* targetSpells = nullptr; const ActiveSpells* targetSpells = nullptr;
@ -204,31 +186,32 @@ namespace MWMechanics
return; return;
} }
for (size_t currentEffectIndex = 0; !target.isEmpty() && currentEffectIndex < effects.mList.size(); for (auto& enam : effects.mList)
++currentEffectIndex)
{ {
const ESM::ENAMstruct& enam = effects.mList[currentEffectIndex]; if (enam.mData.mRange != range)
if (enam.mRange != range)
continue; continue;
const ESM::MagicEffect* magicEffect = store.find(enam.mData.mEffectID);
const ESM::MagicEffect* magicEffect = magicEffects[currentEffectIndex];
if (!magicEffect) if (!magicEffect)
continue; 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; ActiveSpells::ActiveEffect effect;
effect.mEffectId = enam.mEffectID; effect.mEffectId = enam.mData.mEffectID;
effect.mArg = MWMechanics::EffectKey(enam).mArg; effect.mArg = MWMechanics::EffectKey(enam.mData).mArg;
effect.mMagnitude = 0.f; effect.mMagnitude = 0.f;
effect.mMinMagnitude = enam.mMagnMin; effect.mMinMagnitude = enam.mData.mMagnMin;
effect.mMaxMagnitude = enam.mMagnMax; effect.mMaxMagnitude = enam.mData.mMagnMax;
effect.mTimeLeft = 0.f; effect.mTimeLeft = 0.f;
effect.mEffectIndex = static_cast<int>(currentEffectIndex); effect.mEffectIndex = enam.mIndex;
effect.mFlags = ESM::ActiveEffect::Flag_None; effect.mFlags = ESM::ActiveEffect::Flag_None;
if (mManualSpell) if (mScriptedSpell)
effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Reflect; effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Reflect;
bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration);
effect.mDuration = hasDuration ? static_cast<float>(enam.mDuration) : 1.f; effect.mDuration = hasDuration ? static_cast<float>(enam.mData.mDuration) : 1.f;
bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce; bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce;
if (!appliedOnce) if (!appliedOnce)
@ -240,8 +223,8 @@ namespace MWMechanics
params.getEffects().emplace_back(effect); params.getEffects().emplace_back(effect);
bool effectAffectsHealth = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful bool effectAffectsHealth = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful
|| enam.mEffectID == ESM::MagicEffect::RestoreHealth; || enam.mData.mEffectID == ESM::MagicEffect::RestoreHealth;
if (castByPlayer && target != mCaster && targetIsActor && effectAffectsHealth) 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 // If player is attempting to cast a harmful spell on or is healing a living target, show the target's
// HP bar. // HP bar.
@ -262,7 +245,10 @@ namespace MWMechanics
if (!params.getEffects().empty()) if (!params.getEffects().empty())
{ {
if (targetIsActor) if (targetIsActor)
target.getClass().getCreatureStats(target).getActiveSpells().addSpell(params); {
if (!targetIsDeadActor)
target.getClass().getCreatureStats(target).getActiveSpells().addSpell(params);
}
else else
{ {
// Apply effects instantly. We can ignore effect deletion since the entire params object gets // 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; ESM::RefId school = ESM::Skill::Alteration;
if (!enchantment->mEffects.mList.empty()) 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<ESM::MagicEffect>().find(effectId); const ESM::MagicEffect* magicEffect = store->get<ESM::MagicEffect>().find(effectId);
school = magicEffect->mData.mSchool; school = magicEffect->mData.mSchool;
} }
@ -387,7 +373,8 @@ namespace MWMechanics
{ {
mSourceName = potion->mName; mSourceName = potion->mName;
mId = potion->mId; mId = potion->mId;
mType = ESM::ActiveSpells::Type_Consumable; mFlags = static_cast<ESM::ActiveSpells::Flags>(
ESM::ActiveSpells::Flag_Temporary | ESM::ActiveSpells::Flag_Stackable);
inflict(mCaster, potion->mEffects, ESM::RT_Self); inflict(mCaster, potion->mEffects, ESM::RT_Self);
@ -403,7 +390,7 @@ namespace MWMechanics
bool godmode = mCaster == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); 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); school = getSpellSchool(spell, mCaster);
@ -438,7 +425,7 @@ namespace MWMechanics
stats.getSpells().usePower(spell); 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); 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 // 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) bool CastSpell::cast(const ESM::Ingredient* ingredient)
{ {
mId = ingredient->mId; mId = ingredient->mId;
mType = ESM::ActiveSpells::Type_Consumable; mFlags = static_cast<ESM::ActiveSpells::Flags>(
ESM::ActiveSpells::Flag_Temporary | ESM::ActiveSpells::Flag_Stackable);
mSourceName = ingredient->mName; mSourceName = ingredient->mName;
ESM::ENAMstruct effect; auto effect = rollIngredientEffect(mCaster, ingredient, mCaster != getPlayer());
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<ESM::MagicEffect>().find(effect.mEffectID);
const MWMechanics::CreatureStats& creatureStats = mCaster.getClass().getCreatureStats(mCaster);
float x = (mCaster.getClass().getSkill(mCaster, ESM::Skill::Alchemy) if (effect)
+ 0.2f * creatureStats.getAttribute(ESM::Attribute::Intelligence).getModified() inflict(mCaster, *effect, ESM::RT_Self);
+ 0.1f * creatureStats.getAttribute(ESM::Attribute::Luck).getModified()) else
* creatureStats.getFatigueTerm();
auto& prng = MWBase::Environment::get().getWorld()->getPrng();
int roll = Misc::Rng::roll0to99(prng);
if (roll > x)
{ {
// "X has no effect on you" // "X has no effect on you"
std::string message = store.get<ESM::GameSetting>().find("sNotifyMessage50")->mValue.getString(); std::string message = MWBase::Environment::get()
.getESMStore()
->get<ESM::GameSetting>()
.find("sNotifyMessage50")
->mValue.getString();
message = Misc::StringUtils::format(message, ingredient->mName); message = Misc::StringUtils::format(message, ingredient->mName);
MWBase::Environment::get().getWindowManager()->messageBox(message); MWBase::Environment::get().getWindowManager()->messageBox(message);
return false; 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<int>(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<int>(magnitude);
effect.mMagnMin = static_cast<int>(magnitude);
ESM::EffectList effects;
effects.mList.push_back(effect);
inflict(mCaster, effects, ESM::RT_Self);
return true; return true;
} }
@ -527,14 +479,14 @@ namespace MWMechanics
playSpellCastingEffects(spell->mEffects.mList); playSpellCastingEffects(spell->mEffects.mList);
} }
void CastSpell::playSpellCastingEffects(const std::vector<ESM::ENAMstruct>& effects) const void CastSpell::playSpellCastingEffects(const std::vector<ESM::IndexedENAMstruct>& effects) const
{ {
const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore();
std::vector<std::string> addedEffects; std::vector<std::string> addedEffects;
for (const ESM::ENAMstruct& effectData : effects) for (const ESM::IndexedENAMstruct& effectData : effects)
{ {
const auto effect = store.get<ESM::MagicEffect>().find(effectData.mEffectID); const auto effect = store.get<ESM::MagicEffect>().find(effectData.mData.mEffectID);
const ESM::Static* castStatic; const ESM::Static* castStatic;
@ -587,7 +539,7 @@ namespace MWMechanics
} }
if (animation && !mCaster.getClass().isActor()) if (animation && !mCaster.getClass().isActor())
animation->addSpellCastGlow(effect); animation->addSpellCastGlow(effect->getColor());
addedEffects.push_back(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel)); addedEffects.push_back(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel));

@ -26,7 +26,7 @@ namespace MWMechanics
MWWorld::Ptr mCaster; // May be empty MWWorld::Ptr mCaster; // May be empty
MWWorld::Ptr mTarget; // May be empty MWWorld::Ptr mTarget; // May be empty
void playSpellCastingEffects(const std::vector<ESM::ENAMstruct>& effects) const; void playSpellCastingEffects(const std::vector<ESM::IndexedENAMstruct>& effects) const;
void explodeSpell(const ESM::EffectList& effects, const MWWorld::Ptr& ignore, ESM::RangeType rangeType) const; void explodeSpell(const ESM::EffectList& effects, const MWWorld::Ptr& ignore, ESM::RangeType rangeType) const;
@ -41,13 +41,13 @@ namespace MWMechanics
false false
}; // Always succeed spells casted by NPCs/creatures regardless of their chance (default: 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 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, bool mScriptedSpell; // True if spell is casted from script and ignores some checks (mana level, success chance,
// etc.) // etc.)
ESM::RefNum mItem; 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, 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); bool cast(const ESM::Spell* spell);

@ -289,7 +289,7 @@ namespace
animation->addEffect(Misc::ResourceHelpers::correctMeshPath(absorbStatic->mModel), animation->addEffect(Misc::ResourceHelpers::correctMeshPath(absorbStatic->mModel),
ESM::MagicEffect::indexToName(ESM::MagicEffect::SpellAbsorption), false); ESM::MagicEffect::indexToName(ESM::MagicEffect::SpellAbsorption), false);
int spellCost = 0; int spellCost = 0;
if (const ESM::Spell* spell = esmStore.get<ESM::Spell>().search(spellParams.getId())) if (const ESM::Spell* spell = esmStore.get<ESM::Spell>().search(spellParams.getSourceSpellId()))
{ {
spellCost = MWMechanics::calcSpellCost(*spell); spellCost = MWMechanics::calcSpellCost(*spell);
} }
@ -314,8 +314,7 @@ namespace
auto& stats = target.getClass().getCreatureStats(target); auto& stats = target.getClass().getCreatureStats(target);
auto& magnitudes = stats.getMagicEffects(); auto& magnitudes = stats.getMagicEffects();
// Apply reflect and spell absorption // Apply reflect and spell absorption
if (target != caster && spellParams.getType() != ESM::ActiveSpells::Type_Enchantment if (target != caster && spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary))
&& spellParams.getType() != ESM::ActiveSpells::Type_Permanent)
{ {
bool canReflect = !(magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable) bool canReflect = !(magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable)
&& !(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Reflect) && !(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Reflect)
@ -358,9 +357,8 @@ namespace
// Apply resistances // Apply resistances
if (!(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Resistances)) if (!(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Resistances))
{ {
const ESM::Spell* spell = nullptr; const ESM::Spell* spell
if (spellParams.getType() == ESM::ActiveSpells::Type_Temporary) = spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary) ? spellParams.getSpell() : nullptr;
spell = MWBase::Environment::get().getESMStore()->get<ESM::Spell>().search(spellParams.getId());
float magnitudeMult float magnitudeMult
= MWMechanics::getEffectMultiplier(effect.mEffectId, target, caster, spell, &magnitudes); = MWMechanics::getEffectMultiplier(effect.mEffectId, target, caster, spell, &magnitudes);
if (magnitudeMult == 0) if (magnitudeMult == 0)
@ -429,10 +427,9 @@ namespace MWMechanics
// Dispel removes entire spells at once // Dispel removes entire spells at once
target.getClass().getCreatureStats(target).getActiveSpells().purge( target.getClass().getCreatureStats(target).getActiveSpells().purge(
[magnitude = effect.mMagnitude](const ActiveSpells::ActiveSpellParams& params) { [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 const ESM::Spell* spell = params.getSpell();
= MWBase::Environment::get().getESMStore()->get<ESM::Spell>().search(params.getId());
if (spell && spell->mData.mType == ESM::Spell::ST_Spell) if (spell && spell->mData.mType == ESM::Spell::ST_Spell)
{ {
auto& prng = MWBase::Environment::get().getWorld()->getPrng(); auto& prng = MWBase::Environment::get().getWorld()->getPrng();
@ -645,7 +642,7 @@ namespace MWMechanics
else if (effect.mEffectId == ESM::MagicEffect::DamageFatigue) else if (effect.mEffectId == ESM::MagicEffect::DamageFatigue)
index = 2; index = 2;
// Damage "Dynamic" abilities reduce the base value // 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); modDynamicStat(target, index, -effect.mMagnitude);
else else
{ {
@ -666,7 +663,7 @@ namespace MWMechanics
else if (!godmode) else if (!godmode)
{ {
// Damage Skill abilities reduce base skill :todd: // 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); auto& npcStats = target.getClass().getNpcStats(target);
SkillValue& skill = npcStats.getSkill(effect.getSkillOrAttribute()); SkillValue& skill = npcStats.getSkill(effect.getSkillOrAttribute());
@ -725,7 +722,7 @@ namespace MWMechanics
case ESM::MagicEffect::FortifyHealth: case ESM::MagicEffect::FortifyHealth:
case ESM::MagicEffect::FortifyMagicka: case ESM::MagicEffect::FortifyMagicka:
case ESM::MagicEffect::FortifyFatigue: 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); modDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, effect.mMagnitude);
else else
adjustDynamicStat( adjustDynamicStat(
@ -737,7 +734,7 @@ namespace MWMechanics
break; break;
case ESM::MagicEffect::FortifyAttribute: case ESM::MagicEffect::FortifyAttribute:
// Abilities affect base stats, but not for drain // 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& creatureStats = target.getClass().getCreatureStats(target);
auto attribute = effect.getSkillOrAttribute(); auto attribute = effect.getSkillOrAttribute();
@ -757,7 +754,7 @@ namespace MWMechanics
case ESM::MagicEffect::FortifySkill: case ESM::MagicEffect::FortifySkill:
if (!target.getClass().isNpc()) if (!target.getClass().isNpc())
invalid = true; 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 // Abilities affect base stats, but not for drain
auto& npcStats = target.getClass().getNpcStats(target); auto& npcStats = target.getClass().getNpcStats(target);
@ -922,7 +919,7 @@ namespace MWMechanics
{ {
MWRender::Animation* animation = world->getAnimation(target); MWRender::Animation* animation = world->getAnimation(target);
if (animation) if (animation)
animation->addSpellCastGlow(magicEffect); animation->addSpellCastGlow(magicEffect->getColor());
int magnitude = static_cast<int>(roll(effect)); int magnitude = static_cast<int>(roll(effect));
if (target.getCellRef().getLockLevel() if (target.getCellRef().getLockLevel()
< magnitude) // If the door is not already locked to a higher value, lock it to spell magnitude < 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); MWRender::Animation* animation = world->getAnimation(target);
if (animation) if (animation)
animation->addSpellCastGlow(magicEffect); animation->addSpellCastGlow(magicEffect->getColor());
int magnitude = static_cast<int>(roll(effect)); int magnitude = static_cast<int>(roll(effect));
if (target.getCellRef().getLockLevel() <= magnitude) if (target.getCellRef().getLockLevel() <= magnitude)
{ {
@ -985,7 +982,7 @@ namespace MWMechanics
return { MagicApplicationResult::Type::APPLIED, receivedMagicDamage, affectedHealth }; return { MagicApplicationResult::Type::APPLIED, receivedMagicDamage, affectedHealth };
auto& stats = target.getClass().getCreatureStats(target); auto& stats = target.getClass().getCreatureStats(target);
auto& magnitudes = stats.getMagicEffects(); auto& magnitudes = stats.getMagicEffects();
if (spellParams.getType() != ESM::ActiveSpells::Type_Ability if (!spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)
&& !(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) && !(effect.mFlags & ESM::ActiveEffect::Flag_Applied))
{ {
MagicApplicationResult::Type result MagicApplicationResult::Type result
@ -998,10 +995,9 @@ namespace MWMechanics
oldMagnitude = effect.mMagnitude; oldMagnitude = effect.mMagnitude;
else else
{ {
if (spellParams.getType() != ESM::ActiveSpells::Type_Enchantment) if (!spellParams.hasFlag(ESM::ActiveSpells::Flag_Equipment)
playEffects(target, *magicEffect, && !spellParams.hasFlag(ESM::ActiveSpells::Flag_Lua))
spellParams.getType() == ESM::ActiveSpells::Type_Consumable playEffects(target, *magicEffect, spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary));
|| spellParams.getType() == ESM::ActiveSpells::Type_Temporary);
if (effect.mEffectId == ESM::MagicEffect::Soultrap && !target.getClass().isNpc() if (effect.mEffectId == ESM::MagicEffect::Soultrap && !target.getClass().isNpc()
&& target.getType() == ESM::Creature::sRecordId && target.getType() == ESM::Creature::sRecordId
&& target.get<ESM::Creature>()->mBase->mData.mSoul == 0 && caster == getPlayer()) && target.get<ESM::Creature>()->mBase->mData.mSoul == 0 && caster == getPlayer())
@ -1016,8 +1012,7 @@ namespace MWMechanics
if (effect.mDuration != 0) if (effect.mDuration != 0)
{ {
float mult = dt; float mult = dt;
if (spellParams.getType() == ESM::ActiveSpells::Type_Consumable if (spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary))
|| spellParams.getType() == ESM::ActiveSpells::Type_Temporary)
mult = std::min(effect.mTimeLeft, dt); mult = std::min(effect.mTimeLeft, dt);
effect.mMagnitude *= mult; effect.mMagnitude *= mult;
} }
@ -1195,7 +1190,7 @@ namespace MWMechanics
case ESM::MagicEffect::FortifyHealth: case ESM::MagicEffect::FortifyHealth:
case ESM::MagicEffect::FortifyMagicka: case ESM::MagicEffect::FortifyMagicka:
case ESM::MagicEffect::FortifyFatigue: 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); modDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, -effect.mMagnitude);
else else
adjustDynamicStat( adjustDynamicStat(
@ -1206,7 +1201,7 @@ namespace MWMechanics
break; break;
case ESM::MagicEffect::FortifyAttribute: case ESM::MagicEffect::FortifyAttribute:
// Abilities affect base stats, but not for drain // 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& creatureStats = target.getClass().getCreatureStats(target);
auto attribute = effect.getSkillOrAttribute(); auto attribute = effect.getSkillOrAttribute();
@ -1222,7 +1217,7 @@ namespace MWMechanics
break; break;
case ESM::MagicEffect::FortifySkill: case ESM::MagicEffect::FortifySkill:
// Abilities affect base stats, but not for drain // 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& npcStats = target.getClass().getNpcStats(target);
auto& skill = npcStats.getSkill(effect.getSkillOrAttribute()); auto& skill = npcStats.getSkill(effect.getSkillOrAttribute());

@ -34,7 +34,7 @@ namespace
if (effectFilter == -1) if (effectFilter == -1)
{ {
const ESM::Spell* spell const ESM::Spell* spell
= MWBase::Environment::get().getESMStore()->get<ESM::Spell>().search(it->getId()); = MWBase::Environment::get().getESMStore()->get<ESM::Spell>().search(it->getSourceSpellId());
if (!spell || spell->mData.mType != ESM::Spell::ST_Spell) if (!spell || spell->mData.mType != ESM::Spell::ST_Spell)
continue; continue;
} }
@ -67,7 +67,7 @@ namespace
const MWMechanics::ActiveSpells& activeSpells = actor.getClass().getCreatureStats(actor).getActiveSpells(); const MWMechanics::ActiveSpells& activeSpells = actor.getClass().getCreatureStats(actor).getActiveSpells();
for (MWMechanics::ActiveSpells::TIterator it = activeSpells.begin(); it != activeSpells.end(); ++it) for (MWMechanics::ActiveSpells::TIterator it = activeSpells.begin(); it != activeSpells.end(); ++it)
{ {
if (it->getId() != spellId) if (it->getSourceSpellId() != spellId)
continue; continue;
const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it; const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it;
@ -85,7 +85,7 @@ namespace
int actorId = caster.getClass().getCreatureStats(caster).getActorId(); int actorId = caster.getClass().getCreatureStats(caster).getActorId();
const auto& active = target.getClass().getCreatureStats(target).getActiveSpells(); const auto& active = target.getClass().getCreatureStats(target).getActiveSpells();
return std::find_if(active.begin(), active.end(), [&](const auto& spell) { 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(); }) != active.end();
} }
@ -110,13 +110,13 @@ namespace MWMechanics
int getRangeTypes(const ESM::EffectList& effects) int getRangeTypes(const ESM::EffectList& effects)
{ {
int types = 0; int types = 0;
for (std::vector<ESM::ENAMstruct>::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; types |= RangeTypes::Self;
else if (it->mRange == ESM::RT_Touch) else if (effect.mData.mRange == ESM::RT_Touch)
types |= RangeTypes::Touch; types |= RangeTypes::Touch;
else if (it->mRange == ESM::RT_Target) else if (effect.mData.mRange == ESM::RT_Target)
types |= RangeTypes::Target; types |= RangeTypes::Target;
} }
return types; return types;
@ -735,12 +735,12 @@ namespace MWMechanics
static const float fAIMagicSpellMult = gmst.find("fAIMagicSpellMult")->mValue.getFloat(); static const float fAIMagicSpellMult = gmst.find("fAIMagicSpellMult")->mValue.getFloat();
static const float fAIRangeMagicSpellMult = gmst.find("fAIRangeMagicSpellMult")->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 (useSpellMult)
{ {
if (effect.mRange == ESM::RT_Target) if (effect.mData.mRange == ESM::RT_Target)
effectRating *= fAIRangeMagicSpellMult; effectRating *= fAIRangeMagicSpellMult;
else else
effectRating *= fAIMagicSpellMult; effectRating *= fAIMagicSpellMult;
@ -760,10 +760,10 @@ namespace MWMechanics
float mult = fAIMagicSpellMult; float mult = fAIMagicSpellMult;
for (std::vector<ESM::ENAMstruct>::const_iterator effectIt = spell->mEffects.mList.begin(); for (std::vector<ESM::IndexedENAMstruct>::const_iterator effectIt = spell->mEffects.mList.begin();
effectIt != spell->mEffects.mList.end(); ++effectIt) 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)) if (!MWBase::Environment::get().getWorld()->isSwimming(enemy))
mult = fAIRangeMagicSpellMult; mult = fAIRangeMagicSpellMult;

@ -174,7 +174,7 @@ namespace MWMechanics
{ {
for (const auto& effectIt : spell->mEffects.mList) for (const auto& effectIt : spell->mEffects.mList)
{ {
if (effectIt.mEffectID == ESM::MagicEffect::Corprus) if (effectIt.mData.mEffectID == ESM::MagicEffect::Corprus)
{ {
return true; return true;
} }

@ -4,6 +4,7 @@
#include <components/esm3/loadalch.hpp> #include <components/esm3/loadalch.hpp>
#include <components/esm3/loadench.hpp> #include <components/esm3/loadench.hpp>
#include <components/esm3/loadingr.hpp>
#include <components/esm3/loadmgef.hpp> #include <components/esm3/loadmgef.hpp>
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
@ -23,13 +24,13 @@ namespace MWMechanics
{ {
float cost = 0; 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 // 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. // 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; effectCost *= 1.5;
cost += effectCost; cost += effectCost;
@ -158,25 +159,83 @@ namespace MWMechanics
return potion.mData.mValue; return potion.mData.mValue;
} }
std::optional<ESM::EffectList> 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<ESM::MagicEffect>().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<int>(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<int>(magnitude);
effect.mMagnMin = static_cast<int>(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) 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 // Morrowind for some reason uses a formula slightly different from magicka cost calculation
float y = std::numeric_limits<float>::max(); float y = std::numeric_limits<float>::max();
float lowestSkill = 0; float lowestSkill = 0;
for (const ESM::ENAMstruct& effect : spell->mEffects.mList) for (const ESM::IndexedENAMstruct& effect : spell->mEffects.mList)
{ {
float x = static_cast<float>(effect.mDuration); float x = static_cast<float>(effect.mData.mDuration);
const auto magicEffect const auto magicEffect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(effect.mEffectID); = MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(effect.mData.mEffectID);
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce)) if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce))
x = std::max(1.f, x); x = std::max(1.f, x);
x *= 0.1f * magicEffect->mData.mBaseCost; x *= 0.1f * magicEffect->mData.mBaseCost;
x *= 0.5f * (effect.mMagnMin + effect.mMagnMax); x *= 0.5f * (effect.mData.mMagnMin + effect.mData.mMagnMax);
x += effect.mArea * 0.05f * magicEffect->mData.mBaseCost; x += effect.mData.mArea * 0.05f * magicEffect->mData.mBaseCost;
if (effect.mRange == ESM::RT_Target) if (effect.mData.mRange == ESM::RT_Target)
x *= 1.5f; x *= 1.5f;
static const float fEffectCostMult = MWBase::Environment::get() static const float fEffectCostMult = MWBase::Environment::get()
.getESMStore() .getESMStore()

@ -3,10 +3,14 @@
#include <components/esm3/loadskil.hpp> #include <components/esm3/loadskil.hpp>
#include <optional>
namespace ESM namespace ESM
{ {
struct EffectList;
struct ENAMstruct; struct ENAMstruct;
struct Enchantment; struct Enchantment;
struct Ingredient;
struct MagicEffect; struct MagicEffect;
struct Potion; struct Potion;
struct Spell; struct Spell;
@ -36,6 +40,8 @@ namespace MWMechanics
int getEnchantmentCharge(const ESM::Enchantment& enchantment); int getEnchantmentCharge(const ESM::Enchantment& enchantment);
int getPotionValue(const ESM::Potion& potion); int getPotionValue(const ESM::Potion& potion);
std::optional<ESM::EffectList> rollIngredientEffect(
MWWorld::Ptr caster, const ESM::Ingredient* ingredient, uint32_t index = 0);
/** /**
* @param spell spell to cast * @param spell spell to cast

@ -1511,7 +1511,7 @@ namespace MWRender
return mObjectRoot.get(); 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))) if (!mGlowUpdater || (mGlowUpdater->isDone() || (mGlowUpdater->isPermanentGlowUpdater() == true)))
{ {
@ -1520,12 +1520,11 @@ namespace MWRender
if (mGlowUpdater && mGlowUpdater->isPermanentGlowUpdater()) if (mGlowUpdater && mGlowUpdater->isPermanentGlowUpdater())
{ {
mGlowUpdater->setColor(effect->getColor()); mGlowUpdater->setColor(color);
mGlowUpdater->setDuration(glowDuration); mGlowUpdater->setDuration(glowDuration);
} }
else else
mGlowUpdater mGlowUpdater = SceneUtil::addEnchantedGlow(mObjectRoot, mResourceSystem, color, glowDuration);
= SceneUtil::addEnchantedGlow(mObjectRoot, mResourceSystem, effect->getColor(), glowDuration);
} }
} }

@ -345,7 +345,7 @@ namespace MWRender
// Add a spell casting glow to an object. From measuring video taken from the original engine, // 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. // 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); virtual void updatePtr(const MWWorld::Ptr& ptr);

@ -560,7 +560,7 @@ namespace MWScript
runtime.pop(); runtime.pop();
if (ptr.getClass().isActor()) if (ptr.getClass().isActor())
ptr.getClass().getCreatureStats(ptr).getActiveSpells().removeEffects(ptr, spellid); ptr.getClass().getCreatureStats(ptr).getActiveSpells().removeEffectsBySourceSpellId(ptr, spellid);
} }
}; };

@ -532,7 +532,7 @@ namespace MWWorld
return result; return result;
const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().search( const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().search(
enchantment->mEffects.mList.front().mEffectID); enchantment->mEffects.mList.front().mData.mEffectID);
if (!magicEffect) if (!magicEffect)
return result; return result;

@ -171,31 +171,31 @@ namespace
auto iter = spell.mEffects.mList.begin(); auto iter = spell.mEffects.mList.begin();
while (iter != spell.mEffects.mList.end()) 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) if (!mgef)
{ {
Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId 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); iter = spell.mEffects.mList.erase(iter);
changed = true; changed = true;
continue; 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 Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId
<< ": dropping unexpected attribute argument of " << ": dropping unexpected attribute argument of "
<< ESM::MagicEffect::indexToGmstString(iter->mEffectID) << " effect"; << ESM::MagicEffect::indexToGmstString(iter->mData.mEffectID) << " effect";
changed = true; 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 Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId
<< ": dropping unexpected skill argument of " << ": dropping unexpected skill argument of "
<< ESM::MagicEffect::indexToGmstString(iter->mEffectID) << " effect"; << ESM::MagicEffect::indexToGmstString(iter->mData.mEffectID) << " effect";
changed = true; changed = true;
} }
@ -742,7 +742,16 @@ namespace MWWorld
case ESM::REC_DYNA: case ESM::REC_DYNA:
reader.getSubNameIs("COUN"); 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; return true;
default: default:

@ -162,7 +162,7 @@ namespace MWWorld
std::vector<StoreBase*> mStores; std::vector<StoreBase*> mStores;
std::vector<DynamicStore*> mDynamicStores; std::vector<DynamicStore*> mDynamicStores;
unsigned int mDynamicCount; uint64_t mDynamicCount;
mutable std::unordered_map<ESM::RefId, std::weak_ptr<MWMechanics::SpellList>> mSpellListCache; mutable std::unordered_map<ESM::RefId, std::weak_ptr<MWMechanics::SpellList>> mSpellListCache;
@ -209,6 +209,7 @@ namespace MWWorld
void clearDynamic(); void clearDynamic();
void rebuildIdsIndex(); void rebuildIdsIndex();
ESM::RefId generateId() { return ESM::RefId::generated(mDynamicCount++); }
void movePlayerRecord(); void movePlayerRecord();
@ -229,7 +230,7 @@ namespace MWWorld
template <class T> template <class T>
const T* insert(const T& x) const T* insert(const T& x)
{ {
const ESM::RefId id = ESM::RefId::generated(mDynamicCount++); const ESM::RefId id = generateId();
Store<T>& store = getWritable<T>(); Store<T>& store = getWritable<T>();
if (store.search(id) != nullptr) if (store.search(id) != nullptr)

@ -46,8 +46,8 @@ namespace MWWorld
for (auto& effect : spell->mEffects.mList) for (auto& effect : spell->mEffects.mList)
{ {
if (effect.mEffectID == ESM::MagicEffect::DrainAttribute) if (effect.mData.mEffectID == ESM::MagicEffect::DrainAttribute)
stats.mWorsenings[effect.mAttribute] = oldStats.mWorsenings; stats.mWorsenings[effect.mData.mAttribute] = oldStats.mWorsenings;
} }
creatureStats.mCorprusSpells[id] = stats; 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) if (!spell || spell->mData.mType == ESM::Spell::ST_Spell || spell->mData.mType == ESM::Spell::ST_Power)
continue; continue;
ESM::ActiveSpells::ActiveSpellParams params; ESM::ActiveSpells::ActiveSpellParams params;
params.mId = id; params.mSourceSpellId = id;
params.mDisplayName = spell->mName; params.mDisplayName = spell->mName;
params.mCasterActorId = creatureStats.mActorId; params.mCasterActorId = creatureStats.mActorId;
if (spell->mData.mType == ESM::Spell::ST_Ability) if (spell->mData.mType == ESM::Spell::ST_Ability)
params.mType = ESM::ActiveSpells::Type_Ability; params.mFlags = ESM::Compatibility::ActiveSpells::Type_Ability_Flags;
else else
params.mType = ESM::ActiveSpells::Type_Permanent; params.mFlags = ESM::Compatibility::ActiveSpells::Type_Permanent_Flags;
params.mWorsenings = -1; params.mWorsenings = -1;
params.mNextWorsening = ESM::TimeStamp(); params.mNextWorsening = ESM::TimeStamp();
int effectIndex = 0;
for (const auto& enam : spell->mEffects.mList) 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; ESM::ActiveEffect effect;
effect.mEffectId = enam.mEffectID; effect.mEffectId = enam.mData.mEffectID;
effect.mArg = MWMechanics::EffectKey(enam).mArg; effect.mArg = MWMechanics::EffectKey(enam.mData).mArg;
effect.mDuration = -1; effect.mDuration = -1;
effect.mTimeLeft = -1; effect.mTimeLeft = -1;
effect.mEffectIndex = effectIndex; effect.mEffectIndex = enam.mIndex;
auto rand = oldParams.mEffectRands.find(effectIndex); auto rand = oldParams.mEffectRands.find(enam.mIndex);
if (rand != oldParams.mEffectRands.end()) 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.mMagnitude = magnitude;
effect.mMinMagnitude = magnitude; effect.mMinMagnitude = magnitude;
effect.mMaxMagnitude = magnitude; effect.mMaxMagnitude = magnitude;
@ -92,13 +92,12 @@ namespace MWWorld
else else
{ {
effect.mMagnitude = 0.f; effect.mMagnitude = 0.f;
effect.mMinMagnitude = enam.mMagnMin; effect.mMinMagnitude = enam.mData.mMagnMin;
effect.mMaxMagnitude = enam.mMagnMax; effect.mMaxMagnitude = enam.mData.mMagnMax;
effect.mFlags = ESM::ActiveEffect::Flag_None; effect.mFlags = ESM::ActiveEffect::Flag_None;
} }
params.mEffects.emplace_back(effect); params.mEffects.emplace_back(effect);
} }
effectIndex++;
} }
creatureStats.mActiveSpells.mSpells.emplace_back(params); creatureStats.mActiveSpells.mSpells.emplace_back(params);
} }
@ -132,30 +131,28 @@ namespace MWWorld
if (!enchantment) if (!enchantment)
continue; continue;
ESM::ActiveSpells::ActiveSpellParams params; ESM::ActiveSpells::ActiveSpellParams params;
params.mId = id; params.mSourceSpellId = id;
params.mDisplayName = std::move(name); params.mDisplayName = std::move(name);
params.mCasterActorId = creatureStats.mActorId; params.mCasterActorId = creatureStats.mActorId;
params.mType = ESM::ActiveSpells::Type_Enchantment; params.mFlags = ESM::Compatibility::ActiveSpells::Type_Enchantment_Flags;
params.mWorsenings = -1; params.mWorsenings = -1;
params.mNextWorsening = ESM::TimeStamp(); params.mNextWorsening = ESM::TimeStamp();
for (std::size_t effectIndex = 0; for (const auto& enam : enchantment->mEffects.mList)
effectIndex < oldMagnitudes.size() && effectIndex < enchantment->mEffects.mList.size(); ++effectIndex)
{ {
const auto& enam = enchantment->mEffects.mList[effectIndex]; auto [random, multiplier] = oldMagnitudes[enam.mIndex];
auto [random, multiplier] = oldMagnitudes[effectIndex]; float magnitude = (enam.mData.mMagnMax - enam.mData.mMagnMin) * random + enam.mData.mMagnMin;
float magnitude = (enam.mMagnMax - enam.mMagnMin) * random + enam.mMagnMin;
magnitude *= multiplier; magnitude *= multiplier;
if (magnitude <= 0) if (magnitude <= 0)
continue; continue;
ESM::ActiveEffect effect; ESM::ActiveEffect effect;
effect.mEffectId = enam.mEffectID; effect.mEffectId = enam.mData.mEffectID;
effect.mMagnitude = magnitude; effect.mMagnitude = magnitude;
effect.mMinMagnitude = magnitude; effect.mMinMagnitude = magnitude;
effect.mMaxMagnitude = magnitude; effect.mMaxMagnitude = magnitude;
effect.mArg = MWMechanics::EffectKey(enam).mArg; effect.mArg = MWMechanics::EffectKey(enam.mData).mArg;
effect.mDuration = -1; effect.mDuration = -1;
effect.mTimeLeft = -1; effect.mTimeLeft = -1;
effect.mEffectIndex = static_cast<int>(effectIndex); effect.mEffectIndex = enam.mIndex;
// Prevent recalculation of resistances and don't reflect or absorb the effect // Prevent recalculation of resistances and don't reflect or absorb the effect
effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances | ESM::ActiveEffect::Flag_Ignore_Reflect effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances | ESM::ActiveEffect::Flag_Ignore_Reflect
| ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption;
@ -172,7 +169,7 @@ namespace MWWorld
{ {
auto it auto it
= std::find_if(creatureStats.mActiveSpells.mSpells.begin(), creatureStats.mActiveSpells.mSpells.end(), = 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()) if (it != creatureStats.mActiveSpells.mSpells.end())
{ {
it->mNextWorsening = spell.second.mNextWorsening; it->mNextWorsening = spell.second.mNextWorsening;
@ -188,7 +185,7 @@ namespace MWWorld
continue; continue;
for (auto& params : creatureStats.mActiveSpells.mSpells) for (auto& params : creatureStats.mActiveSpells.mSpells)
{ {
if (params.mId == key.mSourceId) if (params.mSourceSpellId == key.mSourceId)
{ {
bool found = false; bool found = false;
for (auto& effect : params.mEffects) for (auto& effect : params.mEffects)

@ -79,18 +79,17 @@ namespace
int count = 0; int count = 0;
speed = 0.0f; speed = 0.0f;
ESM::EffectList projectileEffects; ESM::EffectList projectileEffects;
for (std::vector<ESM::ENAMstruct>::const_iterator iter(effects->mList.begin()); iter != effects->mList.end(); for (const ESM::IndexedENAMstruct& effect : effects->mList)
++iter)
{ {
const ESM::MagicEffect* magicEffect const ESM::MagicEffect* magicEffect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(iter->mEffectID); = MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(effect.mData.mEffectID);
// Speed of multi-effect projectiles should be the average of the constituent effects, // Speed of multi-effect projectiles should be the average of the constituent effects,
// based on observation of the original engine. // based on observation of the original engine.
speed += magicEffect->mData.mSpeed; speed += magicEffect->mData.mSpeed;
count++; count++;
if (iter->mRange != ESM::RT_Target) if (effect.mData.mRange != ESM::RT_Target)
continue; continue;
if (magicEffect->mBolt.empty()) if (magicEffect->mBolt.empty())
@ -106,7 +105,7 @@ namespace
->get<ESM::Skill>() ->get<ESM::Skill>()
.find(magicEffect->mData.mSchool) .find(magicEffect->mData.mSchool)
->mSchool->mBoltSound); ->mSchool->mBoltSound);
projectileEffects.mList.push_back(*iter); projectileEffects.mList.push_back(effect);
} }
if (count != 0) if (count != 0)
@ -117,7 +116,7 @@ namespace
{ {
const ESM::MagicEffect* magicEffect const ESM::MagicEffect* magicEffect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find( = MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(
effects->mList.begin()->mEffectID); effects->mList.begin()->mData.mEffectID);
texture = magicEffect->mParticle; texture = magicEffect->mParticle;
} }
@ -136,10 +135,10 @@ namespace
{ {
// Calculate combined light diffuse color from magical effects // Calculate combined light diffuse color from magical effects
osg::Vec4 lightDiffuseColor; osg::Vec4 lightDiffuseColor;
for (const ESM::ENAMstruct& enam : effects.mList) for (const ESM::IndexedENAMstruct& enam : effects.mList)
{ {
const ESM::MagicEffect* magicEffect const ESM::MagicEffect* magicEffect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(enam.mEffectID); = MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(enam.mData.mEffectID);
lightDiffuseColor += magicEffect->getColor(); lightDiffuseColor += magicEffect->getColor();
} }
int numberOfEffects = effects.mList.size(); int numberOfEffects = effects.mList.size();

@ -2940,14 +2940,14 @@ namespace MWWorld
return result; 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); MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor);
const bool casterIsPlayer = actor == MWMechanics::getPlayer(); const bool casterIsPlayer = actor == MWMechanics::getPlayer();
MWWorld::Ptr target; MWWorld::Ptr target;
// For scripted spells we should not use hit contact // For scripted spells we should not use hit contact
if (manualSpell) if (scriptedSpell)
{ {
if (!casterIsPlayer) if (!casterIsPlayer)
{ {
@ -3015,7 +3015,7 @@ namespace MWWorld
const ESM::RefId& selectedSpell = stats.getSpells().getSelectedSpell(); const ESM::RefId& selectedSpell = stats.getSpells().getSelectedSpell();
MWMechanics::CastSpell cast(actor, target, false, manualSpell); MWMechanics::CastSpell cast(actor, target, false, scriptedSpell);
cast.mHitPosition = hitPosition; cast.mHitPosition = hitPosition;
if (!selectedSpell.empty()) if (!selectedSpell.empty())
@ -3703,22 +3703,22 @@ namespace MWWorld
void World::preloadEffects(const ESM::EffectList* effectList) 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<ESM::MagicEffect>().find(effectInfo.mEffectID); const ESM::MagicEffect* effect = mStore.get<ESM::MagicEffect>().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, 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->mCasting);
preload(mWorldScene.get(), mStore, effect->mHit); preload(mWorldScene.get(), mStore, effect->mHit);
if (effectInfo.mArea > 0) if (effectInfo.mData.mArea > 0)
preload(mWorldScene.get(), mStore, effect->mArea); 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); preload(mWorldScene.get(), mStore, effect->mBolt);
} }
} }

@ -530,29 +530,30 @@ namespace ESM
TEST_P(Esm3SaveLoadRecordTest, enamShouldNotChange) TEST_P(Esm3SaveLoadRecordTest, enamShouldNotChange)
{ {
EffectList record; EffectList record;
record.mList.emplace_back(ENAMstruct{ record.mList.emplace_back(IndexedENAMstruct{ {
.mEffectID = 1, .mEffectID = 1,
.mSkill = 2, .mSkill = 2,
.mAttribute = 3, .mAttribute = 3,
.mRange = 4, .mRange = 4,
.mArea = 5, .mArea = 5,
.mDuration = 6, .mDuration = 6,
.mMagnMin = 7, .mMagnMin = 7,
.mMagnMax = 8, .mMagnMax = 8,
}); },
0 });
EffectList result; EffectList result;
saveAndLoadRecord(record, GetParam(), result); saveAndLoadRecord(record, GetParam(), result);
EXPECT_EQ(result.mList.size(), record.mList.size()); EXPECT_EQ(result.mList.size(), record.mList.size());
EXPECT_EQ(result.mList[0].mEffectID, record.mList[0].mEffectID); EXPECT_EQ(result.mList[0].mData.mEffectID, record.mList[0].mData.mEffectID);
EXPECT_EQ(result.mList[0].mSkill, record.mList[0].mSkill); EXPECT_EQ(result.mList[0].mData.mSkill, record.mList[0].mData.mSkill);
EXPECT_EQ(result.mList[0].mAttribute, record.mList[0].mAttribute); EXPECT_EQ(result.mList[0].mData.mAttribute, record.mList[0].mData.mAttribute);
EXPECT_EQ(result.mList[0].mRange, record.mList[0].mRange); EXPECT_EQ(result.mList[0].mData.mRange, record.mList[0].mData.mRange);
EXPECT_EQ(result.mList[0].mArea, record.mList[0].mArea); EXPECT_EQ(result.mList[0].mData.mArea, record.mList[0].mData.mArea);
EXPECT_EQ(result.mList[0].mDuration, record.mList[0].mDuration); EXPECT_EQ(result.mList[0].mData.mDuration, record.mList[0].mData.mDuration);
EXPECT_EQ(result.mList[0].mMagnMin, record.mList[0].mMagnMin); EXPECT_EQ(result.mList[0].mData.mMagnMin, record.mList[0].mData.mMagnMin);
EXPECT_EQ(result.mList[0].mMagnMax, record.mList[0].mMagnMax); EXPECT_EQ(result.mList[0].mData.mMagnMax, record.mList[0].mData.mMagnMax);
} }
TEST_P(Esm3SaveLoadRecordTest, weaponShouldNotChange) TEST_P(Esm3SaveLoadRecordTest, weaponShouldNotChange)

@ -93,11 +93,12 @@ namespace ESM
{ {
for (const auto& params : spells) 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.writeHNT("CAST", params.mCasterActorId);
esm.writeHNString("DISP", params.mDisplayName); esm.writeHNString("DISP", params.mDisplayName);
esm.writeHNT("TYPE", params.mType); esm.writeHNT("FLAG", params.mFlags);
if (params.mItem.isSet()) if (params.mItem.isSet())
esm.writeFormId(params.mItem, true, "ITEM"); esm.writeFormId(params.mItem, true, "ITEM");
if (params.mWorsenings >= 0) if (params.mWorsenings >= 0)
@ -130,14 +131,42 @@ namespace ESM
while (esm.isNextSub(tag)) while (esm.isNextSub(tag))
{ {
ActiveSpells::ActiveSpellParams params; ActiveSpells::ActiveSpellParams params;
params.mId = esm.getRefId(); params.mSourceSpellId = esm.getRefId();
if (format > MaxActiveSpellTypeVersion)
params.mActiveSpellId = esm.getHNRefId("SPID");
esm.getHNT(params.mCasterActorId, "CAST"); esm.getHNT(params.mCasterActorId, "CAST");
params.mDisplayName = esm.getHNString("DISP"); params.mDisplayName = esm.getHNString("DISP");
if (format <= MaxClearModifiersFormatVersion) if (format <= MaxClearModifiersFormatVersion)
params.mType = ActiveSpells::Type_Temporary; params.mFlags = Compatibility::ActiveSpells::Type_Temporary_Flags;
else 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 (esm.peekNextSub("ITEM"))
{ {
if (format <= MaxActiveSpellSlotIndexFormatVersion) if (format <= MaxActiveSpellSlotIndexFormatVersion)

@ -46,23 +46,28 @@ namespace ESM
// format 0, saved games only // format 0, saved games only
struct ActiveSpells struct ActiveSpells
{ {
enum EffectType enum Flags : uint32_t
{ {
Type_Temporary, Flag_Temporary = 1 << 0, //!< Effect will end automatically once its duration ends.
Type_Ability, Flag_Equipment = 1 << 1, //!< Effect will end automatically if item is unequipped.
Type_Enchantment, Flag_SpellStore = 1 << 2, //!< Effect will end automatically if removed from the actor's spell store.
Type_Permanent, Flag_AffectsBaseValues = 1 << 3, //!< Effects will affect base values instead of current values.
Type_Consumable 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 struct ActiveSpellParams
{ {
RefId mId; RefId mActiveSpellId;
RefId mSourceSpellId;
std::vector<ActiveEffect> mEffects; std::vector<ActiveEffect> mEffects;
std::string mDisplayName; std::string mDisplayName;
int32_t mCasterActorId; int32_t mCasterActorId;
RefNum mItem; RefNum mItem;
EffectType mType; Flags mFlags;
bool mStackable;
int32_t mWorsenings; int32_t mWorsenings;
TimeStamp mNextWorsening; TimeStamp mNextWorsening;
}; };
@ -73,6 +78,29 @@ namespace ESM
void load(ESMReader& esm); void load(ESMReader& esm);
void save(ESMWriter& esm) const; 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>(Flags::Flag_Temporary | Flags::Flag_Stackable);
constexpr Flags Type_Permanent_Flags = Flags::Flag_SpellStore;
constexpr Flags Type_Ability_Flags
= static_cast<Flags>(Flags::Flag_SpellStore | Flags::Flag_AffectsBaseValues);
constexpr Flags Type_Enchantment_Flags = Flags::Flag_Equipment;
}
}
} }
#endif #endif

@ -22,19 +22,40 @@ namespace ESM
} }
} }
void EffectList::populate(const std::vector<ENAMstruct>& effects)
{
mList.clear();
for (size_t i = 0; i < effects.size(); i++)
mList.push_back({ effects[i], static_cast<uint32_t>(i) });
}
void EffectList::updateIndexes()
{
for (size_t i = 0; i < mList.size(); i++)
mList[i].mIndex = i;
}
void EffectList::add(ESMReader& esm) void EffectList::add(ESMReader& esm)
{ {
ENAMstruct s; ENAMstruct s;
esm.getSubComposite(s); esm.getSubComposite(s);
mList.push_back(s); mList.push_back({ s, static_cast<uint32_t>(mList.size()) });
} }
void EffectList::save(ESMWriter& esm) const 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 } // end namespace

@ -26,10 +26,21 @@ namespace ESM
int32_t mArea, mDuration, mMagnMin, mMagnMax; 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 /// EffectList, ENAM subrecord
struct EffectList struct EffectList
{ {
std::vector<ENAMstruct> mList; std::vector<IndexedENAMstruct> mList;
void populate(const std::vector<ENAMstruct>& effects);
void updateIndexes();
/// Load one effect, assumes subrecord name was already read /// Load one effect, assumes subrecord name was already read
void add(ESMReader& esm); void add(ESMReader& esm);

@ -26,7 +26,8 @@ namespace ESM
inline constexpr FormatVersion MaxUseEsmCellIdFormatVersion = 26; inline constexpr FormatVersion MaxUseEsmCellIdFormatVersion = 26;
inline constexpr FormatVersion MaxActiveSpellSlotIndexFormatVersion = 27; inline constexpr FormatVersion MaxActiveSpellSlotIndexFormatVersion = 27;
inline constexpr FormatVersion MaxOldCountFormatVersion = 30; 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 MinSupportedSaveGameFormatVersion = 5;
inline constexpr FormatVersion OpenMW0_48SaveGameFormatVersion = 21; inline constexpr FormatVersion OpenMW0_48SaveGameFormatVersion = 21;

@ -339,6 +339,11 @@
-- @field #string id Record id of the spell or item used to cast the spell -- @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 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 #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. -- @field #list<#ActiveSpellEffect> effects The active effects (@{#ActiveSpellEffect}) of this spell.
--- ---
@ -369,7 +374,7 @@
-- @type Enchantment -- @type Enchantment
-- @field #string id Enchantment id -- @field #string id Enchantment id
-- @field #number type @{#EnchantmentType} -- @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 cost
-- @field #number charge Charge capacity. Should not be confused with current charge. -- @field #number charge Charge capacity. Should not be confused with current charge.
-- @field #list<#MagicEffectWithParams> effects The effects (@{#MagicEffectWithParams}) of the enchantment -- @field #list<#MagicEffectWithParams> effects The effects (@{#MagicEffectWithParams}) of the enchantment
@ -671,6 +676,8 @@
-- @field #number type @{#SpellType} -- @field #number type @{#SpellType}
-- @field #number cost -- @field #number cost
-- @field #list<#MagicEffectWithParams> effects The effects (@{#MagicEffectWithParams}) of the spell -- @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 -- @type MagicEffect
@ -680,16 +687,28 @@
-- @field #string school Skill ID that is this effect's school -- @field #string school Skill ID that is this effect's school
-- @field #number baseCost -- @field #number baseCost
-- @field openmw.util#Color color -- @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 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 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 hitStatic Identifier of the vfx static used on hit
-- @field #string areaStatic Identifier of the vfx static used for AOE spells -- @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 -- @type MagicEffectWithParams
-- @field #MagicEffect effect @{#MagicEffect} -- @field #MagicEffect effect @{#MagicEffect}
-- @field #string id ID of the associated @{#MagicEffect}
-- @field #string affectedSkill Optional skill ID -- @field #string affectedSkill Optional skill ID
-- @field #string affectedAttribute Optional attribute ID -- @field #string affectedAttribute Optional attribute ID
-- @field #number range -- @field #number range
@ -697,6 +716,7 @@
-- @field #number magnitudeMin -- @field #number magnitudeMin
-- @field #number magnitudeMax -- @field #number magnitudeMax
-- @field #number duration -- @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 -- @type ActiveEffect
@ -708,6 +728,7 @@
-- @field #number magnitude current magnitude of the effect. Will be set to 0 when effect is removed or expires. -- @field #number magnitude current magnitude of the effect. Will be set to 0 when effect is removed or expires.
-- @field #number magnitudeBase -- @field #number magnitudeBase
-- @field #number magnitudeModifier -- @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 --- @{#Sound}: Sounds and Speech
-- @field [parent=#core] #Sound sound -- @field [parent=#core] #Sound sound

@ -302,17 +302,59 @@
-- end -- 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 -- @function [parent=#ActorActiveSpells] isSpellActive
-- @param self -- @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 -- @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 -- @function [parent=#ActorActiveSpells] remove
-- @param self -- @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. -- Return the spells (@{#ActorSpells}) of the given actor.

Loading…
Cancel
Save