#include "magicbindings.hpp" #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #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" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "localscripts.hpp" #include "luamanagerimp.hpp" #include "object.hpp" #include "objectvariant.hpp" #include "worldview.hpp" namespace MWLua { // class returned via 'types.Actor.spells(obj)' in Lua struct ActorSpells { bool isActor() const { return !mActor.ptr().isEmpty() && mActor.ptr().getClass().isActor(); } MWMechanics::Spells* getStore() const { if (!isActor()) return nullptr; const MWWorld::Ptr& ptr = mActor.ptr(); return &ptr.getClass().getCreatureStats(ptr).getSpells(); } ObjectVariant mActor; }; template struct ActorStore { using Collection = typename Store::Collection; using Iterator = typename Collection::const_iterator; ActorStore(ObjectVariant actor) : mActor(actor) , mIterator() , mIndex(0) { reset(); } bool isActor() const { return !mActor.ptr().isEmpty() && mActor.ptr().getClass().isActor(); } void reset() { mIndex = 0; auto* store = getStore(); if (store) mIterator = store->begin(); } bool isEnd() const { auto* store = getStore(); if (store) return mIterator == store->end(); return true; } void advance() { auto* store = getStore(); if (store) { mIterator++; mIndex++; } } Store* getStore() const; ObjectVariant mActor; Iterator mIterator; int mIndex; }; template <> MWMechanics::MagicEffects* ActorStore::getStore() const { if (!isActor()) return nullptr; const MWWorld::Ptr& ptr = mActor.ptr(); return &ptr.getClass().getCreatureStats(ptr).getMagicEffects(); } template <> MWMechanics::ActiveSpells* ActorStore::getStore() const { if (!isActor()) return nullptr; const MWWorld::Ptr& ptr = mActor.ptr(); return &ptr.getClass().getCreatureStats(ptr).getActiveSpells(); } 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 { template struct is_automagical> : std::false_type { }; template <> struct is_automagical : std::false_type { }; template <> struct is_automagical : std::false_type { }; template <> struct is_automagical : std::false_type { }; template <> 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)); } sol::table initCoreMagicBindings(const Context& context) { sol::state_view& lua = context.mLua->sol(); sol::table magicApi(lua, sol::create); // Constants magicApi["RANGE"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ { "Self", ESM::RT_Self }, { "Touch", ESM::RT_Touch }, { "Target", ESM::RT_Target }, })); magicApi["SCHOOL"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ { "Alteration", 0 }, { "Conjuration", 1 }, { "Destruction", 2 }, { "Illusion", 3 }, { "Mysticism", 4 }, { "Restoration", 5 }, })); magicApi["SPELL_TYPE"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ { "Spell", ESM::Spell::ST_Spell }, { "Ability", ESM::Spell::ST_Ability }, { "Blight", ESM::Spell::ST_Blight }, { "Disease", ESM::Spell::ST_Disease }, { "Curse", ESM::Spell::ST_Curse }, { "Power", ESM::Spell::ST_Power }, })); sol::table effect(context.mLua->sol(), sol::create); magicApi["EFFECT_TYPE"] = LuaUtil::makeStrictReadOnly(effect); for (const auto& it : ESM::MagicEffect::sEffectNames) { effect[it.second] = Misc::StringUtils::lowerCase(it.second); } // Spell store using SpellStore = MWWorld::Store; const SpellStore* spellStore = &MWBase::Environment::get().getWorld()->getStore().get(); sol::usertype spellStoreT = lua.new_usertype("ESM3_SpellStore"); spellStoreT[sol::meta_function::to_string] = [](const SpellStore& store) { return "ESM3_SpellStore{" + std::to_string(store.getSize()) + " spells}"; }; spellStoreT[sol::meta_function::length] = [](const SpellStore& store) { return store.getSize(); }; spellStoreT[sol::meta_function::index] = sol::overload( [](const SpellStore& store, size_t index) -> const ESM::Spell* { return store.at(index - 1); }, [](const SpellStore& store, std::string_view spellId) -> const ESM::Spell* { return store.find(ESM::RefId::deserializeText(spellId)); }); spellStoreT[sol::meta_function::pairs] = lua["ipairsForArray"].template get(); spellStoreT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); magicApi["spells"] = spellStore; // MagicEffect store using MagicEffectStore = MWWorld::Store; const MagicEffectStore* magicEffectStore = &MWBase::Environment::get().getWorld()->getStore().get(); auto magicEffectStoreT = lua.new_usertype("ESM3_MagicEffectStore"); magicEffectStoreT[sol::meta_function::to_string] = [](const MagicEffectStore& store) { return "ESM3_MagicEffectStore{" + std::to_string(store.getSize()) + " effects}"; }; magicEffectStoreT[sol::meta_function::index] = [](const MagicEffectStore& store, int id) -> const ESM::MagicEffect* { return store.find(id); }; auto magicEffectsIter = [magicEffectStore](sol::this_state lua, const sol::object& /*store*/, sol::optional id) -> std::tuple { MagicEffectStore::iterator iter; if (id.has_value()) { iter = magicEffectStore->findIter(*id); if (iter != magicEffectStore->end()) iter++; } else iter = magicEffectStore->begin(); if (iter != magicEffectStore->end()) return std::make_tuple(sol::make_object(lua, iter->first), sol::make_object(lua, &iter->second)); else return std::make_tuple(sol::nil, sol::nil); }; magicEffectStoreT[sol::meta_function::pairs] = [iter = sol::make_object(lua, magicEffectsIter)] { return iter; }; magicApi["effects"] = magicEffectStore; // Spell record auto spellT = lua.new_usertype("ESM3_Spell"); spellT[sol::meta_function::to_string] = [](const ESM::Spell& rec) -> std::string { return "ESM3_Spell[" + rec.mId.toDebugString() + "]"; }; spellT["id"] = sol::readonly_property([](const ESM::Spell& rec) { return rec.mId.serializeText(); }); spellT["name"] = sol::readonly_property([](const ESM::Spell& rec) -> std::string_view { return rec.mName; }); spellT["type"] = sol::readonly_property([](const ESM::Spell& rec) -> int { return rec.mData.mType; }); spellT["cost"] = sol::readonly_property([](const ESM::Spell& rec) -> int { return rec.mData.mCost; }); spellT["effects"] = sol::readonly_property([&lua](const ESM::Spell& rec) -> sol::table { sol::table res(lua, sol::create); for (size_t i = 0; i < rec.mEffects.mList.size(); ++i) res[i + 1] = rec.mEffects.mList[i]; // ESM::ENAMstruct (effect params) return res; }); // Effect params auto effectParamsT = lua.new_usertype("ESM3_EffectParams"); effectParamsT[sol::meta_function::to_string] = [magicEffectStore](const ESM::ENAMstruct& params) { const ESM::MagicEffect* const rec = magicEffectStore->find(params.mEffectID); return "ESM3_EffectParams[" + std::string(ESM::MagicEffect::effectIdToGmstString(rec->mIndex)) + "]"; }; effectParamsT["effect"] = sol::readonly_property([magicEffectStore](const ESM::ENAMstruct& params) -> const ESM::MagicEffect* { return magicEffectStore->find(params.mEffectID); }); effectParamsT["affectedSkill"] = sol::readonly_property([](const ESM::ENAMstruct& params) -> sol::optional { if (params.mSkill >= 0 && params.mSkill < ESM::Skill::Length) return Misc::StringUtils::lowerCase(ESM::Skill::sSkillNames[params.mSkill]); else return sol::nullopt; }); effectParamsT["affectedAttribute"] = sol::readonly_property([](const ESM::ENAMstruct& params) -> sol::optional { if (params.mAttribute >= 0 && params.mAttribute < ESM::Attribute::Length) return Misc::StringUtils::lowerCase(ESM::Attribute::sAttributeNames[params.mAttribute]); else return sol::nullopt; }); effectParamsT["range"] = sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mRange; }); effectParamsT["area"] = sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mArea; }); effectParamsT["magnitudeMin"] = sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mMagnMin; }); effectParamsT["magnitudeMax"] = sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mMagnMax; }); // MagicEffect record auto magicEffectT = context.mLua->sol().new_usertype("ESM3_MagicEffect"); magicEffectT[sol::meta_function::to_string] = [](const ESM::MagicEffect& rec) { return "ESM3_MagicEffect[" + std::string(ESM::MagicEffect::effectIdToGmstString(rec.mIndex)) + "]"; }; magicEffectT["id"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> std::string { auto name = ESM::MagicEffect::effectIdToName(rec.mIndex); return Misc::StringUtils::lowerCase(name); }); magicEffectT["name"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> std::string_view { return MWBase::Environment::get() .getWorld() ->getStore() .get() .find(ESM::MagicEffect::effectIdToGmstString(rec.mIndex)) ->mValue.getString(); }); magicEffectT["school"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> int { return rec.mData.mSchool; }); magicEffectT["baseCost"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> float { return rec.mData.mBaseCost; }); magicEffectT["color"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> Misc::Color { return Misc::Color(rec.mData.mRed / 255.f, rec.mData.mGreen / 255.f, rec.mData.mBlue / 255.f, 1.f); }); magicEffectT["harmful"] = sol::readonly_property( [](const ESM::MagicEffect& rec) -> bool { return rec.mData.mFlags & ESM::MagicEffect::Harmful; }); // TODO: Should we expose it? What happens if a spell has several effects with different projectileSpeed? // 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[" + std::string(ESM::MagicEffect::effectIdToGmstString(effect.key.mId)) + "]"; }; activeEffectT["id"] = sol::readonly_property([](const ActiveEffect& effect) -> std::string { auto name = ESM::MagicEffect::effectIdToName(effect.key.mId); 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) { const MWWorld::Store* spellStore = &MWBase::Environment::get().getWorld()->getStore().get(); // types.Actor.spells(o) actor["spells"] = [](const sol::object actor) { auto spells = ActorSpells{ ObjectVariant(actor) }; if (!spells.isActor()) throw std::runtime_error("Actor expected"); return spells; }; auto spellsT = context.mLua->sol().new_usertype("ActorSpells"); spellsT[sol::meta_function::to_string] = [](const ActorSpells& spells) { return "ActorSpells[" + spells.mActor.object().toString() + "]"; }; actor["activeSpells"] = [](const sol::object actor) { auto spells = ActorActiveSpells{ ObjectVariant(actor) }; if (!spells.isActor()) throw std::runtime_error("Actor expected"); return spells; }; 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) { auto effects = ActorActiveEffects{ ObjectVariant(actor) }; if (!effects.isActor()) throw std::runtime_error("Actor expected"); return effects; }; 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(); const MWWorld::Class& cls = ptr.getClass(); if (!cls.isActor()) throw std::runtime_error("Actor expected"); ESM::RefId spellId; if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) spellId = MWBase::Environment::get().getWindowManager()->getSelectedSpell(); else spellId = cls.getCreatureStats(ptr).getSpells().getSelectedSpell(); if (spellId.empty()) return sol::nullopt; else return spellStore->find(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 { if (auto* store = spells.getStore()) return store->count(); return 0; }; // types.Actor.spells(o)[i] spellsT[sol::meta_function::index] = sol::overload( [](const ActorSpells& spells, size_t index) -> sol::optional { if (auto* store = spells.getStore()) if (index <= store->count()) return store->at(index - 1); return sol::nullopt; }, [spellStore](const ActorSpells& spells, std::string_view spellId) -> sol::optional { if (auto* store = spells.getStore()) { const ESM::Spell* spell = spellStore->find(ESM::RefId::deserializeText(spellId)); if (store->hasSpell(spell)) return spell; } return sol::nullopt; }); // pairs(types.Actor.spells(o)) spellsT[sol::meta_function::pairs] = context.mLua->sol()["ipairsForArray"].template get(); // ipairs(types.Actor.spells(o)) spellsT[sol::meta_function::ipairs] = context.mLua->sol()["ipairsForArray"].template get(); // types.Actor.spells(o):add(id) 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)]() { const MWWorld::Ptr& ptr = obj.ptr(); if (ptr.getClass().isActor()) ptr.getClass().getCreatureStats(ptr).getSpells().add(id); }); }; // types.Actor.spells(o):remove(id) 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)]() { const MWWorld::Ptr& ptr = obj.ptr(); if (ptr.getClass().isActor()) ptr.getClass().getCreatureStats(ptr).getSpells().remove(id); }); }; // types.Actor.spells(o):clear() spellsT["clear"] = [context](const ActorSpells& spells) { 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()]() { const MWWorld::Ptr& ptr = obj.ptr(); if (ptr.getClass().isActor()) 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.mIterator->getId()); auto index = sol::make_object(lua, self.mIndex + 1); 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); if (auto* store = spells.getStore()) return store->isSpellActive(id); return false; }; // 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.mIterator->first, self.mIterator->second }; auto result = sol::make_object(lua, effect); auto key = sol::make_object(lua, self.mIterator->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& effects, std::string_view idStr, sol::optional argStr) -> sol::optional { if (!effects.isActor()) return sol::nullopt; auto id = ESM::MagicEffect::effectNameToId(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 (auto* store = effects.getStore()) if (store->get(key, param)) return ActiveEffect{ key, param }; return sol::nullopt; }; } }