diff --git a/apps/openmw/mwlua/magicbindings.cpp b/apps/openmw/mwlua/magicbindings.cpp index 2048060f86..bf0f7f35f7 100644 --- a/apps/openmw/mwlua/magicbindings.cpp +++ b/apps/openmw/mwlua/magicbindings.cpp @@ -1,9 +1,15 @@ #include "magicbindings.hpp" #include +#include +#include +#include +#include #include +#include #include #include +#include #include #include #include @@ -111,6 +117,12 @@ namespace MWLua MWMechanics::EffectKey key; MWMechanics::EffectParam param; }; + struct ActiveSpell + { + ObjectVariant mActor; + MWMechanics::ActiveSpells::ActiveSpellParams mParams; + }; + // class returned via 'types.Actor.spells(obj)' in Lua using ActorSpells = ActorStore; // class returned via 'types.Actor.activeEffects(obj)' in Lua @@ -141,6 +153,10 @@ namespace sol struct is_automagical> : std::false_type { }; + template <> + struct is_automagical : std::false_type + { + }; } namespace MWLua @@ -152,6 +168,27 @@ namespace MWLua else return ESM::RefId::deserializeText(LuaUtil::cast(spellOrId)); } + static ESM::RefId toRecordId(const sol::object& recordOrId) + { + if (recordOrId.is()) + return recordOrId.as()->mId; + else if (recordOrId.is()) + return recordOrId.as()->mId; + else if (recordOrId.is()) + return recordOrId.as()->mId; + else if (recordOrId.is()) + return recordOrId.as()->mId; + else if (recordOrId.is()) + return recordOrId.as()->mId; + else if (recordOrId.is()) + return recordOrId.as()->mId; + else if (recordOrId.is()) + return recordOrId.as()->mId; + else if (recordOrId.is()) + return recordOrId.as()->mId; + else + return ESM::RefId::deserializeText(LuaUtil::cast(recordOrId)); + } sol::table initCoreMagicBindings(const Context& context) { @@ -372,6 +409,122 @@ namespace MWLua // magicEffectT["projectileSpeed"] // = sol::readonly_property([](const ESM::MagicEffect& rec) -> float { return rec.mData.mSpeed; }); + auto activeSpellEffectT = context.mLua->sol().new_usertype("ActiveSpellEffect"); + activeSpellEffectT[sol::meta_function::to_string] = [](const ESM::ActiveEffect& effect) { + return "ActiveSpellEffect[" + ESM::MagicEffect::indexToGmstString(effect.mEffectId) + "]"; + }; + activeSpellEffectT["id"] = sol::readonly_property([](const ESM::ActiveEffect& effect) -> std::string { + auto name = ESM::MagicEffect::indexToName(effect.mEffectId); + return Misc::StringUtils::lowerCase(name); + }); + activeSpellEffectT["name"] = sol::readonly_property([](const ESM::ActiveEffect& effect) -> std::string { + return MWMechanics::EffectKey(effect.mEffectId, effect.mArg).toString(); + }); + activeSpellEffectT["affectedSkill"] + = sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional { + auto* rec = magicEffectStore->find(effect.mEffectId); + if ((rec->mData.mFlags & ESM::MagicEffect::TargetSkill) && effect.mArg >= 0 + && effect.mArg < ESM::Skill::Length) + return ESM::Skill::indexToRefId(effect.mArg).serializeText(); + else + return sol::nullopt; + }); + activeSpellEffectT["affectedAttribute"] + = sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional { + auto* rec = magicEffectStore->find(effect.mEffectId); + if ((rec->mData.mFlags & ESM::MagicEffect::TargetAttribute) && effect.mArg >= 0 + && effect.mArg < ESM::Attribute::Length) + return Misc::StringUtils::lowerCase(ESM::Attribute::sAttributeNames[effect.mArg]); + else + return sol::nullopt; + }); + activeSpellEffectT["magnitudeThisFrame"] + = sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional { + auto* rec = magicEffectStore->find(effect.mEffectId); + if (rec->mData.mFlags & ESM::MagicEffect::Flags::NoMagnitude) + return sol::nullopt; + return effect.mMagnitude; + }); + activeSpellEffectT["minMagnitude"] + = sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional { + auto* rec = magicEffectStore->find(effect.mEffectId); + if (rec->mData.mFlags & ESM::MagicEffect::Flags::NoMagnitude) + return sol::nullopt; + return effect.mMinMagnitude; + }); + activeSpellEffectT["maxMagnitude"] + = sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional { + auto* rec = magicEffectStore->find(effect.mEffectId); + if (rec->mData.mFlags & ESM::MagicEffect::Flags::NoMagnitude) + return sol::nullopt; + return effect.mMaxMagnitude; + }); + activeSpellEffectT["durationLeft"] + = sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional { + // Permanent/constant effects, abilities, etc. will have a negative duration + if (effect.mDuration < 0) + return sol::nullopt; + auto* rec = magicEffectStore->find(effect.mEffectId); + if (rec->mData.mFlags & ESM::MagicEffect::Flags::NoDuration) + return sol::nullopt; + return effect.mTimeLeft; + }); + activeSpellEffectT["duration"] + = sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional { + // Permanent/constant effects, abilities, etc. will have a negative duration + if (effect.mDuration < 0) + return sol::nullopt; + auto* rec = magicEffectStore->find(effect.mEffectId); + if (rec->mData.mFlags & ESM::MagicEffect::Flags::NoDuration) + return sol::nullopt; + return effect.mDuration; + }); + + auto activeSpellT = context.mLua->sol().new_usertype("ActiveSpellParams"); + activeSpellT[sol::meta_function::to_string] = [](const ActiveSpell& activeSpell) { + return "ActiveSpellParams[" + activeSpell.mParams.getId().serializeText() + "]"; + }; + activeSpellT["name"] = sol::readonly_property( + [](const ActiveSpell& activeSpell) -> std::string_view { return activeSpell.mParams.getDisplayName(); }); + activeSpellT["id"] = sol::readonly_property( + [](const ActiveSpell& activeSpell) -> std::string { return activeSpell.mParams.getId().serializeText(); }); + activeSpellT["item"] = sol::readonly_property([&lua](const ActiveSpell& activeSpell) -> sol::object { + auto item = activeSpell.mParams.getItem(); + if (!item.isSet()) + return sol::nil; + auto itemPtr = MWBase::Environment::get().getWorldModel()->getPtr(item); + if (itemPtr.isEmpty()) + return sol::nil; + if (activeSpell.mActor.isGObject()) + return sol::make_object(lua, GObject(itemPtr)); + else + return sol::make_object(lua, LObject(itemPtr)); + }); + activeSpellT["caster"] = sol::readonly_property([&lua](const ActiveSpell& activeSpell) -> sol::object { + auto caster + = MWBase::Environment::get().getWorld()->searchPtrViaActorId(activeSpell.mParams.getCasterActorId()); + if (caster.isEmpty()) + return sol::nil; + else + { + if (activeSpell.mActor.isGObject()) + return sol::make_object(lua, GObject(getId(caster))); + else + return sol::make_object(lua, LObject(getId(caster))); + } + }); + activeSpellT["effects"] = sol::readonly_property([&lua](const ActiveSpell& activeSpell) -> sol::table { + sol::table res(lua, sol::create); + size_t tableIndex = 0; + for (const ESM::ActiveEffect& effect : activeSpell.mParams.getEffects()) + { + if (!(effect.mFlags & ESM::ActiveEffect::Flag_Applied)) + continue; + res[++tableIndex] = effect; // ESM::ActiveEffect (effect params) + } + return res; + }); + auto activeEffectT = context.mLua->sol().new_usertype("ActiveEffect"); activeEffectT[sol::meta_function::to_string] = [](const ActiveEffect& effect) { @@ -547,18 +700,16 @@ namespace MWLua }; // pairs(types.Actor.activeSpells(o)) - // Note that the indexes are fake, and only for consistency with other lua pairs interfaces. You can't use them - // for anything. activeSpellsT["__pairs"] = [](sol::this_state ts, ActorActiveSpells& self) { sol::state_view lua(ts); self.reset(); - return sol::as_function([lua, &self]() mutable -> std::pair { + return sol::as_function([lua, self]() mutable -> std::pair { if (!self.isEnd()) { - auto result = sol::make_object(lua, self.mIterator->getId()); - auto index = sol::make_object(lua, self.mIndex + 1); + auto id = sol::make_object(lua, self.mIterator->getId().serializeText()); + auto params = sol::make_object(lua, ActiveSpell{ self.mActor, *self.mIterator }); self.advance(); - return { index, result }; + return { params, params }; } else { @@ -568,10 +719,13 @@ namespace MWLua }; // types.Actor.activeSpells(o):isSpellActive(id) - activeSpellsT["isSpellActive"] = [](const ActorActiveSpells& spells, const sol::object& spellOrId) -> bool { - auto id = toSpellId(spellOrId); - if (auto* store = spells.getStore()) - return store->isSpellActive(id); + activeSpellsT["isSpellActive"] + = [](const ActorActiveSpells& activeSpells, const sol::object& recordOrId) -> bool { + if (auto* store = activeSpells.getStore()) + { + auto id = toRecordId(recordOrId); + return store->isSpellActive(id) || store->isEnchantmentActive(id); + } return false; }; @@ -593,7 +747,7 @@ namespace MWLua activeEffectsT["__pairs"] = [](sol::this_state ts, ActorActiveEffects& self) { sol::state_view lua(ts); self.reset(); - return sol::as_function([lua, &self]() mutable -> std::pair { + return sol::as_function([lua, self]() mutable -> std::pair { if (!self.isEnd()) { ActiveEffect effect = ActiveEffect{ self.mIterator->first, self.mIterator->second }; diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index 55477bef0c..8294f915e8 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -28,6 +28,7 @@ #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" +#include "../mwworld/manualref.hpp" namespace { @@ -423,6 +424,30 @@ namespace MWMechanics != mSpells.end(); } + bool ActiveSpells::isEnchantmentActive(const ESM::RefId& id) const + { + const auto& store = MWBase::Environment::get().getESMStore(); + if (store->get().search(id) == nullptr) + return false; + + // Enchantment id is not stored directly. Instead the enchanted item is stored. + return std::find_if(mSpells.begin(), mSpells.end(), [&](const auto& spell) { + switch (store->find(spell.mId)) + { + case ESM::REC_ARMO: + return store->get().find(spell.mId)->mEnchant == id; + case ESM::REC_BOOK: + return store->get().find(spell.mId)->mEnchant == id; + case ESM::REC_CLOT: + return store->get().find(spell.mId)->mEnchant == id; + case ESM::REC_WEAP: + return store->get().find(spell.mId)->mEnchant == id; + default: + return false; + } + }) != mSpells.end(); + } + void ActiveSpells::addSpell(const ActiveSpellParams& params) { mQueue.emplace_back(params); diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index 8184567a5a..c804e0d1da 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -72,6 +72,8 @@ namespace MWMechanics const std::string& getDisplayName() const { return mDisplayName; } + ESM::RefNum getItem() const { return mItem; } + // Increments worsenings count and sets the next timestamp void worsen(); @@ -143,8 +145,12 @@ namespace MWMechanics /// Remove all spells void clear(const MWWorld::Ptr& ptr); + /// True if a spell associated with this id is active + /// \note For enchantments, this is the id of the enchanted item, not the enchantment itself bool isSpellActive(const ESM::RefId& id) const; - ///< case insensitive + + /// True if the enchantment is active + bool isEnchantmentActive(const ESM::RefId& id) const; void skipWorsenings(double hours); diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index 4f9adc3b08..6901940edd 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -316,6 +316,26 @@ -- local all = cell:getAll() -- local weapons = cell:getAll(types.Weapon) +--- +-- @type ActiveSpell +-- @field #string name The spell or item display name +-- @field #string id Record id of the spell or item used to cast the spell +-- @field openmw.core#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 openmw.core#GameObject caster The caster object, or nil if the spell has no defined caster +-- @field #list<#ActiveSpellEffect> effects The active effects (@{#ActiveSpellEffect}) of this spell. + +--- +-- @type ActiveSpellEffect +-- @field #string affectedSkill @{#SKILL} or nil +-- @field #string affectedAttribute @{#ATTRIBUTE} or nil +-- @field #string id Magic effect id +-- @field #string name Localized name of the effect +-- @field #number magnitudeThisFrame The magnitude of the effect in the current frame. This will be a new random number between minMagnitude and maxMagnitude every frame. Or nil if the effect has no magnitude. +-- @field #number minMagnitude The minimum magnitude of this effect, or nil if the effect has no magnitude. +-- @field #number maxMagnitude The maximum magnitude of this effect, or nil if the effect has no magnitude. +-- @field #number duration Total duration in seconds of this spell effect, should not be confused with remaining duration. Or nil if the effect is not temporary. +-- @field #number durationLeft Remaining duration in seconds of this spell effect, or nil if the effect is not temporary. + --- Possible @{#EnchantmentType} values -- @field [parent=#Magic] #EnchantmentType ENCHANTMENT_TYPE @@ -699,8 +719,8 @@ --- -- @type MagicEffectWithParams -- @field #MagicEffect effect @{#MagicEffect} --- @field #any affectedSkill @{#SKILL} or nil --- @field #any affectedAttribute @{#ATTRIBUTE} or nil +-- @field #string affectedSkill @{#SKILL} or nil +-- @field #string affectedAttribute @{#ATTRIBUTE} or nil -- @field #number range -- @field #number area -- @field #number magnitudeMin @@ -709,8 +729,8 @@ --- -- @type ActiveEffect --- @field #any affectedSkill @{#SKILL} or nil --- @field #any affectedAttribute @{#ATTRIBUTE} or nil +-- @field #string affectedSkill @{#SKILL} or nil +-- @field #string affectedAttribute @{#ATTRIBUTE} or nil -- @field #string id Effect id string -- @field #string name Localized name of the effect -- @field #number magnitude diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index 36d63358df..7ba2b5d5bb 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -239,7 +239,7 @@ -- @param openmw.core#GameObject actor -- @return #ActorActiveSpells ---- Read-only list of spells currently affecting the actor. +--- Read-only list of spells currently affecting the actor. Can be iterated over for a list of @{openmw.core#ActiveSpell} -- @type ActorActiveSpells -- @usage -- print active spells -- for _, spell in pairs(Actor.activeSpells(self)) do @@ -251,12 +251,33 @@ -- else -- print('Player does not have bound longbow') -- end +-- @usage -- Print all information about active spells +-- for id, params in pairs(Actor.activeSpells(self)) do +-- print('active spell '..tostring(id)..':') +-- print(' name: '..tostring(params.name)) +-- print(' id: '..tostring(params.id)) +-- print(' item: '..tostring(params.item)) +-- print(' caster: '..tostring(params.caster)) +-- print(' effects: '..tostring(params.effects)) +-- for _, effect in pairs(params.effects) do +-- print(' -> effects['..tostring(effect)..']:') +-- print(' id: '..tostring(effect.id)) +-- print(' name: '..tostring(effect.name)) +-- print(' affectedSkill: '..tostring(effect.affectedSkill)) +-- print(' affectedAttribute: '..tostring(effect.affectedAttribute)) +-- print(' magnitudeThisFrame: '..tostring(effect.magnitudeThisFrame)) +-- print(' minMagnitude: '..tostring(effect.minMagnitude)) +-- print(' maxMagnitude: '..tostring(effect.maxMagnitude)) +-- print(' duration: '..tostring(effect.duration)) +-- print(' durationLeft: '..tostring(effect.durationLeft)) +-- end +-- end --- -- Get whether a specific spell is active on the actor. -- @function [parent=#ActorActiveSpells] isSpellActive -- @param self --- @param #any spellOrId @{openmw.core#Spell} or string spell id +-- @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 -- @return true if spell is active, false otherwise ---