diff --git a/apps/openmw/mwlua/magicbindings.cpp b/apps/openmw/mwlua/magicbindings.cpp index 3af9c4967a..395a2806e8 100644 --- a/apps/openmw/mwlua/magicbindings.cpp +++ b/apps/openmw/mwlua/magicbindings.cpp @@ -13,6 +13,7 @@ #include "../mwbase/world.hpp" #include "../mwmechanics/activespells.hpp" #include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/magiceffects.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/spellutil.hpp" #include "../mwworld/action.hpp" @@ -32,6 +33,41 @@ namespace MWLua { const ObjectVariant mActor; }; + + template + struct ActorStore + { + using Collection = Store::Collection; + using Iterator = Collection::const_iterator; + const ObjectVariant mActor; + Store* store = nullptr; + Iterator it; + int index = 0; + + void reset() + { + index = 0; + it = store->begin(); + } + + bool isEnd() { return it == store->end(); } + + void advance() + { + it++; + index++; + } + }; + + struct ActiveEffect + { + MWMechanics::EffectKey key; + MWMechanics::EffectParam param; + }; + // class returned via 'types.Actor.activeEffects(obj)' in Lua + using ActorActiveEffects = ActorStore; + // class returned via 'types.Actor.activeSpells(obj)' in Lua + using ActorActiveSpells = ActorStore; } namespace sol @@ -56,10 +92,33 @@ namespace sol struct is_automagical : std::false_type { }; + template <> + struct is_automagical : std::false_type + { + }; } namespace MWLua { + static ESM::RefId toSpellId(const sol::object& spellOrId) + { + if (spellOrId.is()) + return spellOrId.as()->mId; + else + return ESM::RefId::deserializeText(LuaUtil::cast(spellOrId)); + }; + static const ESM::Spell* toSpell(const sol::object& spellOrId) + { + if (spellOrId.is()) + return spellOrId.as(); + else + { + auto& store = MWBase::Environment::get().getWorld()->getStore(); + auto refId = ESM::RefId::deserializeText(LuaUtil::cast(spellOrId)); + return store.get().find(refId); + } + }; + sol::table initCoreMagicBindings(const Context& context) { sol::state_view& lua = context.mLua->sol(); @@ -89,6 +148,16 @@ namespace MWLua { "Power", ESM::Spell::ST_Power }, })); + sol::table effect(context.mLua->sol(), sol::create); + magicApi["EFFECT_TYPE"] = LuaUtil::makeStrictReadOnly(effect); + for (int id = 0; id < ESM::MagicEffect::Length; ++id) + { + auto name = ESM::MagicEffect::effectIdToString(id); + // Chop off the 'sEffect' prefix present on 100% of effect id strings + name = name.substr(7); + effect[name] = Misc::StringUtils::lowerCase(name); + } + // Spell store using SpellStore = MWWorld::Store; const SpellStore* spellStore = &MWBase::Environment::get().getWorld()->getStore().get(); @@ -191,7 +260,11 @@ namespace MWLua magicEffectT[sol::meta_function::to_string] = [](const ESM::MagicEffect& rec) { return "ESM3_MagicEffect[" + ESM::MagicEffect::effectIdToString(rec.mIndex) + "]"; }; - magicEffectT["id"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> int { return rec.mIndex; }); + magicEffectT["id"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> std::string_view { + auto gmstName = ESM::MagicEffect::effectIdToString(rec.mIndex); + auto name = gmstName.substr(7); // Remove the 'sEffect' prefix + return Misc::StringUtils::lowerCase(name); + }); magicEffectT["name"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> std::string_view { return MWBase::Environment::get() .getWorld() @@ -214,18 +287,50 @@ namespace MWLua // magicEffectT["projectileSpeed"] // = sol::readonly_property([](const ESM::MagicEffect& rec) -> float { return rec.mData.mSpeed; }); + auto activeEffectT = context.mLua->sol().new_usertype("ActiveEffect"); + + activeEffectT[sol::meta_function::to_string] = [](const ActiveEffect& effect) { + return "ActiveEffect[" + ESM::MagicEffect::effectIdToString(effect.key.mId) + "]"; + }; + activeEffectT["id"] = sol::readonly_property([](const ActiveEffect& effect) -> std::string_view { + auto gmstName = ESM::MagicEffect::effectIdToString(effect.key.mId); + auto name = gmstName.substr(7); // Remove the 'sEffect' prefix + return Misc::StringUtils::lowerCase(name); + }); + activeEffectT["name"] + = sol::readonly_property([](const ActiveEffect& effect) -> std::string { return effect.key.toString(); }); + + activeEffectT["affectedSkill"] + = sol::readonly_property([magicEffectStore](const ActiveEffect& effect) -> sol::optional { + auto* rec = magicEffectStore->find(effect.key.mId); + if ((rec->mData.mFlags & ESM::MagicEffect::TargetSkill) && effect.key.mArg >= 0 + && effect.key.mArg < ESM::Skill::Length) + return Misc::StringUtils::lowerCase(ESM::Skill::sSkillNames[effect.key.mArg]); + else + return sol::nullopt; + }); + activeEffectT["affectedAttribute"] + = sol::readonly_property([magicEffectStore](const ActiveEffect& effect) -> sol::optional { + auto* rec = magicEffectStore->find(effect.key.mId); + if ((rec->mData.mFlags & ESM::MagicEffect::TargetAttribute) && effect.key.mArg >= 0 + && effect.key.mArg < ESM::Attribute::Length) + return Misc::StringUtils::lowerCase(ESM::Attribute::sAttributeNames[effect.key.mArg]); + else + return sol::nullopt; + }); + + activeEffectT["magnitude"] + = sol::readonly_property([](const ActiveEffect& effect) { return effect.param.getMagnitude(); }); + activeEffectT["magnitudeBase"] + = sol::readonly_property([](const ActiveEffect& effect) { return effect.param.getBase(); }); + activeEffectT["magnitudeModifier"] + = sol::readonly_property([](const ActiveEffect& effect) { return effect.param.getModifier(); }); + return LuaUtil::makeReadOnly(magicApi); } void addActorMagicBindings(sol::table& actor, const Context& context) { - auto toSpellId = [](const sol::object& spellOrId) -> ESM::RefId { - if (spellOrId.is()) - return spellOrId.as()->mId; - else - return ESM::RefId::deserializeText(LuaUtil::cast(spellOrId)); - }; - const MWWorld::Store* spellStore = &MWBase::Environment::get().getWorld()->getStore().get(); @@ -233,7 +338,35 @@ namespace MWLua actor["spells"] = [](const sol::object actor) { return ActorSpells{ ObjectVariant(actor) }; }; auto spellsT = context.mLua->sol().new_usertype("ActorSpells"); spellsT[sol::meta_function::to_string] - = [](const ActorSpells& spells) { return "ActorSpells[" + spells.mActor.object().toString(); }; + = [](const ActorSpells& spells) { return "ActorSpells[" + spells.mActor.object().toString() + "]"; }; + + actor["activeSpells"] = [](const sol::object actor) { + ActorActiveSpells store = { ObjectVariant(actor) }; + auto ptr = store.mActor.ptr(); + if (!ptr.isEmpty()) + { + store.store = &ptr.getClass().getCreatureStats(ptr).getActiveSpells(); + } + return store; + }; + auto activeSpellsT = context.mLua->sol().new_usertype("ActorActiveSpells"); + activeSpellsT[sol::meta_function::to_string] = [](const ActorActiveSpells& spells) { + return "ActorActiveSpells[" + spells.mActor.object().toString() + "]"; + }; + + actor["activeEffects"] = [](const sol::object actor) { + ActorActiveEffects store = { ObjectVariant(actor) }; + auto ptr = store.mActor.ptr(); + if (!ptr.isEmpty()) + { + store.store = &ptr.getClass().getCreatureStats(ptr).getMagicEffects(); + } + return store; + }; + auto activeEffectsT = context.mLua->sol().new_usertype("ActorActiveEffects"); + activeEffectsT[sol::meta_function::to_string] = [](const ActorActiveEffects& effects) { + return "ActorActiveEffects[" + effects.mActor.object().toString() + "]"; + }; actor["getSelectedSpell"] = [spellStore](const Object& o) -> sol::optional { const MWWorld::Ptr& ptr = o.ptr(); @@ -250,36 +383,35 @@ namespace MWLua else return spellStore->find(spellId); }; - actor["setSelectedSpell"] - = [context, spellStore, toSpellId](const SelfObject& o, const sol::object& spellOrId) { - const MWWorld::Ptr& ptr = o.ptr(); - const MWWorld::Class& cls = ptr.getClass(); - if (!cls.isActor()) - throw std::runtime_error("Actor expected"); - ESM::RefId spellId; - if (spellOrId != sol::nil) - { - spellId = toSpellId(spellOrId); - const ESM::Spell* spell = spellStore->find(spellId); - if (spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power) - throw std::runtime_error("Ability or disease can not be casted: " + spellId.toDebugString()); - } - context.mLuaManager->addAction([obj = Object(ptr), spellId]() { - const MWWorld::Ptr& ptr = obj.ptr(); - auto& stats = ptr.getClass().getCreatureStats(ptr); - if (!stats.getSpells().hasSpell(spellId)) - throw std::runtime_error("Actor doesn't know spell " + spellId.toDebugString()); - if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) - { - int chance = 0; - if (!spellId.empty()) - chance = MWMechanics::getSpellSuccessChance(spellId, ptr); - MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, chance); - } - else - ptr.getClass().getCreatureStats(ptr).getSpells().setSelectedSpell(spellId); - }); - }; + actor["setSelectedSpell"] = [context, spellStore](const SelfObject& o, const sol::object& spellOrId) { + const MWWorld::Ptr& ptr = o.ptr(); + const MWWorld::Class& cls = ptr.getClass(); + if (!cls.isActor()) + throw std::runtime_error("Actor expected"); + ESM::RefId spellId; + if (spellOrId != sol::nil) + { + spellId = toSpellId(spellOrId); + const ESM::Spell* spell = spellStore->find(spellId); + if (spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power) + throw std::runtime_error("Ability or disease can not be casted: " + spellId.toDebugString()); + } + context.mLuaManager->addAction([obj = Object(ptr), spellId]() { + const MWWorld::Ptr& ptr = obj.ptr(); + auto& stats = ptr.getClass().getCreatureStats(ptr); + if (!stats.getSpells().hasSpell(spellId)) + throw std::runtime_error("Actor doesn't know spell " + spellId.toDebugString()); + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + { + int chance = 0; + if (!spellId.empty()) + chance = MWMechanics::getSpellSuccessChance(spellId, ptr); + MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, chance); + } + else + ptr.getClass().getCreatureStats(ptr).getSpells().setSelectedSpell(spellId); + }); + }; // #(types.Actor.spells(o)) spellsT[sol::meta_function::length] = [](const ActorSpells& spells) -> size_t { @@ -309,7 +441,7 @@ namespace MWLua spellsT[sol::meta_function::ipairs] = context.mLua->sol()["ipairsForArray"].template get(); // types.Actor.spells(o):add(id) - spellsT["add"] = [context, toSpellId](const ActorSpells& spells, const sol::object& spellOrId) { + spellsT["add"] = [context](const ActorSpells& spells, const sol::object& spellOrId) { if (spells.mActor.isLObject()) throw std::runtime_error("Local scripts can modify only spells of the actor they are attached to."); context.mLuaManager->addAction([obj = spells.mActor.object(), id = toSpellId(spellOrId)]() { @@ -319,7 +451,7 @@ namespace MWLua }; // types.Actor.spells(o):remove(id) - spellsT["remove"] = [context, toSpellId](const ActorSpells& spells, const sol::object& spellOrId) { + spellsT["remove"] = [context](const ActorSpells& spells, const sol::object& spellOrId) { if (spells.mActor.isLObject()) throw std::runtime_error("Local scripts can modify only spells of the actor they are attached to."); context.mLuaManager->addAction([obj = spells.mActor.object(), id = toSpellId(spellOrId)]() { @@ -337,5 +469,82 @@ namespace MWLua ptr.getClass().getCreatureStats(ptr).getSpells().clear(); }); }; + + // 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 { + if (!self.isEnd()) + { + auto result = sol::make_object(lua, self.it->getId()); + auto index = sol::make_object(lua, self.index); + self.advance(); + return { index, result }; + } + else + { + return { sol::lua_nil, sol::lua_nil }; + } + }); + }; + + // types.Actor.activeSpells(o):isSpellActive(id) + activeSpellsT["isSpellActive"] = [](const ActorActiveSpells& spells, const sol::object& spellOrId) -> bool { + auto id = toSpellId(spellOrId); + const MWWorld::Ptr& ptr = spells.mActor.object().ptr(); + auto& activeSpells = ptr.getClass().getCreatureStats(ptr).getActiveSpells(); + return activeSpells.isSpellActive(id); + }; + + // pairs(types.Actor.activeEffects(o)) + // Note that the indexes are fake, and only for consistency with other lua pairs interfaces. You can't use them + // for anything. + activeEffectsT["__pairs"] = [](sol::this_state ts, ActorActiveEffects& self) { + sol::state_view lua(ts); + self.reset(); + return sol::as_function([lua, &self]() mutable -> std::pair { + if (!self.isEnd()) + { + ActiveEffect effect = ActiveEffect{ self.it->first, self.it->second }; + auto result = sol::make_object(lua, effect); + + auto key = sol::make_object(lua, self.it->first.toString()); + self.advance(); + return { key, result }; + } + else + { + return { sol::lua_nil, sol::lua_nil }; + } + }); + }; + + // types.Actor.activeEffects(o):getEffect(id, ?arg) + activeEffectsT["getEffect"] = [](const ActorActiveEffects& self, std::string_view idStr, + sol::optional argStr) -> sol::optional { + auto id = ESM::MagicEffect::effectStringToId("sEffect" + std::string(idStr)); + auto* rec = MWBase::Environment::get().getWorld()->getStore().get().find(id); + + MWMechanics::EffectKey key = MWMechanics::EffectKey(id); + + if (argStr.has_value() + && (rec->mData.mFlags & (ESM::MagicEffect::TargetAttribute | ESM::MagicEffect::TargetSkill))) + { + // MWLua exposes attributes and skills as strings, so we have to convert them back to IDs here + if (rec->mData.mFlags & ESM::MagicEffect::TargetAttribute) + key = MWMechanics::EffectKey(id, ESM::Attribute::stringToAttributeId(argStr.value())); + + if (rec->mData.mFlags & ESM::MagicEffect::TargetSkill) + key = MWMechanics::EffectKey(id, ESM::Skill::stringToSkillId(argStr.value())); + } + + MWMechanics::EffectParam param; + if (self.store->get(key, param)) + return ActiveEffect{ key, param }; + return sol::nullopt; + }; } } diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index b70d45634c..0531c35f88 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -79,7 +79,8 @@ namespace MWMechanics void resetWorsenings(); }; - typedef std::list::const_iterator TIterator; + typedef std::list Collection; + typedef Collection::const_iterator TIterator; void readState(const ESM::ActiveSpells& state); void writeState(ESM::ActiveSpells& state) const; diff --git a/apps/openmw/mwmechanics/magiceffects.cpp b/apps/openmw/mwmechanics/magiceffects.cpp index d44d43947a..61589490e7 100644 --- a/apps/openmw/mwmechanics/magiceffects.cpp +++ b/apps/openmw/mwmechanics/magiceffects.cpp @@ -157,17 +157,22 @@ namespace MWMechanics } EffectParam MagicEffects::get(const EffectKey& key) const + { + EffectParam param = EffectParam(); + get(key, param); + return param; + } + + bool MagicEffects::get(const EffectKey& key, EffectParam& param) const { Collection::const_iterator iter = mCollection.find(key); - if (iter == mCollection.end()) + if (iter != mCollection.end()) { - return EffectParam(); - } - else - { - return iter->second; + param = iter->second; + return true; } + return false; } MagicEffects MagicEffects::diff(const MagicEffects& prev, const MagicEffects& now) diff --git a/apps/openmw/mwmechanics/magiceffects.hpp b/apps/openmw/mwmechanics/magiceffects.hpp index 7d05e7b66a..4f3f3a1d48 100644 --- a/apps/openmw/mwmechanics/magiceffects.hpp +++ b/apps/openmw/mwmechanics/magiceffects.hpp @@ -105,6 +105,7 @@ namespace MWMechanics void setModifiers(const MagicEffects& effects); EffectParam get(const EffectKey& key) const; + bool get(const EffectKey& key, EffectParam& param) const; ///< This function can safely be used for keys that are not present. static MagicEffects diff(const MagicEffects& prev, const MagicEffects& now); diff --git a/components/esm/attr.cpp b/components/esm/attr.cpp index 5e419405dd..3828392722 100644 --- a/components/esm/attr.cpp +++ b/components/esm/attr.cpp @@ -1,4 +1,5 @@ #include "attr.hpp" +#include using namespace ESM; @@ -56,3 +57,12 @@ const std::string Attribute::sAttributeIcons[Attribute::Length] = { "icons\\k\\attribute_personality.dds", "icons\\k\\attribute_luck.dds", }; + +Attribute::AttributeID Attribute::stringToAttributeId(std::string_view attribute) +{ + for (auto id : ESM::Attribute::sAttributeIds) + if (Misc::StringUtils::ciEqual(ESM::Attribute::sAttributeNames[id], attribute)) + return id; + + throw std::logic_error("No such attribute: " + std::string(attribute)); +} diff --git a/components/esm/attr.hpp b/components/esm/attr.hpp index b49f68c874..dd8321c5be 100644 --- a/components/esm/attr.hpp +++ b/components/esm/attr.hpp @@ -33,6 +33,8 @@ namespace ESM static const std::string sGmstAttributeIds[Length]; static const std::string sGmstAttributeDescIds[Length]; static const std::string sAttributeIcons[Length]; + + static AttributeID stringToAttributeId(std::string_view attribute); }; } #endif diff --git a/components/esm3/loadskil.cpp b/components/esm3/loadskil.cpp index c2776bae74..fa7f7ec7b7 100644 --- a/components/esm3/loadskil.cpp +++ b/components/esm3/loadskil.cpp @@ -3,6 +3,8 @@ #include "esmreader.hpp" #include "esmwriter.hpp" +#include + namespace ESM { const std::string Skill::sSkillNames[Length] = { @@ -97,6 +99,15 @@ namespace ESM Destruction, Alteration, Illusion, Conjuration, Mysticism, Restoration, Alchemy, Unarmored, Security, Sneak, Acrobatics, LightArmor, ShortBlade, Marksman, Mercantile, Speechcraft, HandToHand } }; + Skill::SkillEnum Skill::stringToSkillId(std::string_view skill) + { + for (auto id : ESM::Skill::sSkillIds) + if (Misc::StringUtils::ciEqual(ESM::Skill::sSkillNames[id], skill)) + return id; + + throw std::logic_error("No such skill: " + std::string(skill)); + } + void Skill::load(ESMReader& esm, bool& isDeleted) { isDeleted = false; // Skill record can't be deleted now (may be changed in the future) diff --git a/components/esm3/loadskil.hpp b/components/esm3/loadskil.hpp index bacc8534ed..d472706bcf 100644 --- a/components/esm3/loadskil.hpp +++ b/components/esm3/loadskil.hpp @@ -81,6 +81,8 @@ namespace ESM static const std::string sIconNames[Length]; static const std::array sSkillIds; + static SkillEnum stringToSkillId(std::string_view skill); + void load(ESMReader& esm, bool& isDeleted); void save(ESMWriter& esm, bool isDeleted = false) const;