#include "stats.hpp" #include #include #include #include #include #include #include #include #include #include "context.hpp" #include "localscripts.hpp" #include "luamanagerimp.hpp" #include "../mwbase/environment.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "objectvariant.hpp" #include "types/types.hpp" namespace { using SelfObject = MWLua::SelfObject; using ObjectVariant = MWLua::ObjectVariant; using Index = const SelfObject::CachedStat::Index&; template auto addIndexedAccessor(Index index) { return [index](const sol::object& o) { return T::create(ObjectVariant(o), index); }; } template void addProp(const MWLua::Context& context, sol::usertype& type, std::string_view prop, G getter) { type[prop] = sol::property([=](const T& stat) { return stat.get(context, prop, getter); }, [=](const T& stat, const sol::object& value) { stat.cache(context, prop, value); }); } template sol::object getValue(const MWLua::Context& context, const ObjectVariant& obj, SelfObject::CachedStat::Setter setter, Index index, std::string_view prop, G getter) { if (obj.isSelfObject()) { SelfObject* self = obj.asSelfObject(); auto it = self->mStatsCache.find({ setter, index, prop }); if (it != self->mStatsCache.end()) return it->second; } return sol::make_object(context.mLua->sol(), getter(obj.ptr())); } } namespace MWLua { static void addStatUpdateAction(MWLua::LuaManager* manager, const SelfObject& obj) { if (!obj.mStatsCache.empty()) return; // was already added before manager->addAction( [obj = Object(obj)] { LocalScripts* scripts = obj.ptr().getRefData().getLuaScripts(); if (scripts) scripts->applyStatsCache(); }, "StatUpdateAction"); } static void setCreatureValue(Index, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) { auto& stats = ptr.getClass().getCreatureStats(ptr); if (prop == "current") stats.setLevel(LuaUtil::cast(value)); } static void setNpcValue(Index index, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) { auto& stats = ptr.getClass().getNpcStats(ptr); if (prop == "progress") stats.setLevelProgress(LuaUtil::cast(value)); else if (prop == "skillIncreasesForAttribute") stats.setSkillIncreasesForAttribute( *std::get(index).getIf(), LuaUtil::cast(value)); else if (prop == "skillIncreasesForSpecialization") stats.setSkillIncreasesForSpecialization(std::get(index), LuaUtil::cast(value)); } class SkillIncreasesForAttributeStats { ObjectVariant mObject; public: SkillIncreasesForAttributeStats(ObjectVariant object) : mObject(std::move(object)) { } sol::object get(const Context& context, ESM::StringRefId attributeId) const { const auto& ptr = mObject.ptr(); if (!ptr.getClass().isNpc()) return sol::nil; return getValue(context, mObject, &setNpcValue, attributeId, "skillIncreasesForAttribute", [attributeId](const MWWorld::Ptr& ptr) { return ptr.getClass().getNpcStats(ptr).getSkillIncreasesForAttribute(attributeId); }); } void set(const Context& context, ESM::StringRefId attributeId, const sol::object& value) const { const auto& ptr = mObject.ptr(); if (!ptr.getClass().isNpc()) return; SelfObject* obj = mObject.asSelfObject(); addStatUpdateAction(context.mLuaManager, *obj); obj->mStatsCache[SelfObject::CachedStat{ &setNpcValue, attributeId, "skillIncreasesForAttribute" }] = value; } }; class SkillIncreasesForSpecializationStats { ObjectVariant mObject; public: SkillIncreasesForSpecializationStats(ObjectVariant object) : mObject(std::move(object)) { } sol::object get(const Context& context, int specialization) const { const auto& ptr = mObject.ptr(); if (!ptr.getClass().isNpc()) return sol::nil; return getValue(context, mObject, &setNpcValue, specialization, "skillIncreasesForSpecialization", [specialization](const MWWorld::Ptr& ptr) { return ptr.getClass().getNpcStats(ptr).getSkillIncreasesForSpecialization(specialization); }); } void set(const Context& context, int specialization, const sol::object& value) const { const auto& ptr = mObject.ptr(); if (!ptr.getClass().isNpc()) return; SelfObject* obj = mObject.asSelfObject(); addStatUpdateAction(context.mLuaManager, *obj); obj->mStatsCache[SelfObject::CachedStat{ &setNpcValue, specialization, "skillIncreasesForSpecialization" }] = value; } }; class LevelStat { ObjectVariant mObject; LevelStat(ObjectVariant object) : mObject(std::move(object)) { } public: sol::object getCurrent(const Context& context) const { return getValue(context, mObject, &setCreatureValue, std::monostate{}, "current", [](const MWWorld::Ptr& ptr) { return ptr.getClass().getCreatureStats(ptr).getLevel(); }); } void setCurrent(const Context& context, const sol::object& value) const { SelfObject* obj = mObject.asSelfObject(); addStatUpdateAction(context.mLuaManager, *obj); obj->mStatsCache[SelfObject::CachedStat{ &setCreatureValue, std::monostate{}, "current" }] = value; } sol::object getProgress(const Context& context) const { const auto& ptr = mObject.ptr(); if (!ptr.getClass().isNpc()) return sol::nil; return getValue(context, mObject, &setNpcValue, std::monostate{}, "progress", [](const MWWorld::Ptr& ptr) { return ptr.getClass().getNpcStats(ptr).getLevelProgress(); }); } void setProgress(const Context& context, const sol::object& value) const { const auto& ptr = mObject.ptr(); if (!ptr.getClass().isNpc()) return; SelfObject* obj = mObject.asSelfObject(); addStatUpdateAction(context.mLuaManager, *obj); obj->mStatsCache[SelfObject::CachedStat{ &setNpcValue, std::monostate{}, "progress" }] = value; } SkillIncreasesForAttributeStats getSkillIncreasesForAttributeStats() const { return SkillIncreasesForAttributeStats{ mObject }; } SkillIncreasesForSpecializationStats getSkillIncreasesForSpecializationStats() const { return SkillIncreasesForSpecializationStats{ mObject }; } static std::optional create(ObjectVariant object, Index) { if (!object.ptr().getClass().isActor()) return {}; return LevelStat{ std::move(object) }; } }; class DynamicStat { ObjectVariant mObject; int mIndex; DynamicStat(ObjectVariant object, int index) : mObject(std::move(object)) , mIndex(index) { } public: template sol::object get(const Context& context, std::string_view prop, G getter) const { return getValue( context, mObject, &DynamicStat::setValue, mIndex, prop, [this, getter](const MWWorld::Ptr& ptr) { return (ptr.getClass().getCreatureStats(ptr).getDynamic(mIndex).*getter)(); }); } static std::optional create(ObjectVariant object, Index i) { if (!object.ptr().getClass().isActor()) return {}; int index = std::get(i); return DynamicStat{ std::move(object), index }; } void cache(const Context& context, std::string_view prop, const sol::object& value) const { SelfObject* obj = mObject.asSelfObject(); addStatUpdateAction(context.mLuaManager, *obj); obj->mStatsCache[SelfObject::CachedStat{ &DynamicStat::setValue, mIndex, prop }] = value; } static void setValue(Index i, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) { int index = std::get(i); auto& stats = ptr.getClass().getCreatureStats(ptr); auto stat = stats.getDynamic(index); float floatValue = LuaUtil::cast(value); if (prop == "base") stat.setBase(floatValue); else if (prop == "current") stat.setCurrent(floatValue, true, true); else if (prop == "modifier") stat.setModifier(floatValue); stats.setDynamic(index, stat); } }; class AttributeStat { ObjectVariant mObject; ESM::RefId mId; AttributeStat(ObjectVariant object, ESM::RefId id) : mObject(std::move(object)) , mId(id) { } public: template sol::object get(const Context& context, std::string_view prop, G getter) const { return getValue( context, mObject, &AttributeStat::setValue, mId, prop, [this, getter](const MWWorld::Ptr& ptr) { return (ptr.getClass().getCreatureStats(ptr).getAttribute(mId).*getter)(); }); } float getModified(const Context& context) const { auto base = LuaUtil::cast(get(context, "base", &MWMechanics::AttributeValue::getBase)); auto damage = LuaUtil::cast(get(context, "damage", &MWMechanics::AttributeValue::getDamage)); auto modifier = LuaUtil::cast(get(context, "modifier", &MWMechanics::AttributeValue::getModifier)); return std::max(0.f, base - damage + modifier); // Should match AttributeValue::getModified } static std::optional create(ObjectVariant object, Index i) { if (!object.ptr().getClass().isActor()) return {}; ESM::RefId id = std::get(i); return AttributeStat{ std::move(object), id }; } void cache(const Context& context, std::string_view prop, const sol::object& value) const { SelfObject* obj = mObject.asSelfObject(); addStatUpdateAction(context.mLuaManager, *obj); obj->mStatsCache[SelfObject::CachedStat{ &AttributeStat::setValue, mId, prop }] = value; } static void setValue(Index i, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) { ESM::RefId id = std::get(i); auto& stats = ptr.getClass().getCreatureStats(ptr); auto stat = stats.getAttribute(id); float floatValue = LuaUtil::cast(value); if (prop == "base") stat.setBase(floatValue); else if (prop == "damage") { stat.restore(stat.getDamage()); stat.damage(floatValue); } else if (prop == "modifier") stat.setModifier(floatValue); stats.setAttribute(id, stat); } }; class SkillStat { ObjectVariant mObject; ESM::RefId mId; SkillStat(ObjectVariant object, ESM::RefId id) : mObject(std::move(object)) , mId(id) { } static float getProgress(const MWWorld::Ptr& ptr, ESM::RefId id, const MWMechanics::SkillValue& stat) { float progress = stat.getProgress(); if (progress != 0.f) progress /= getMaxProgress(ptr, id, stat); return progress; } static float getMaxProgress(const MWWorld::Ptr& ptr, ESM::RefId id, const MWMechanics::SkillValue& stat) { const auto& store = *MWBase::Environment::get().getESMStore(); const auto cl = store.get().find(ptr.get()->mBase->mClass); return ptr.getClass().getNpcStats(ptr).getSkillProgressRequirement(id, *cl); } public: template sol::object get(const Context& context, std::string_view prop, G getter) const { return getValue(context, mObject, &SkillStat::setValue, mId, prop, [this, getter](const MWWorld::Ptr& ptr) { return (ptr.getClass().getNpcStats(ptr).getSkill(mId).*getter)(); }); } float getModified(const Context& context) const { auto base = LuaUtil::cast(get(context, "base", &MWMechanics::SkillValue::getBase)); auto damage = LuaUtil::cast(get(context, "damage", &MWMechanics::SkillValue::getDamage)); auto modifier = LuaUtil::cast(get(context, "modifier", &MWMechanics::SkillValue::getModifier)); return std::max(0.f, base - damage + modifier); // Should match SkillValue::getModified } sol::object getProgress(const Context& context) const { return getValue(context, mObject, &SkillStat::setValue, mId, "progress", [this](const MWWorld::Ptr& ptr) { return getProgress(ptr, mId, ptr.getClass().getNpcStats(ptr).getSkill(mId)); }); } static std::optional create(ObjectVariant object, Index index) { if (!object.ptr().getClass().isNpc()) return {}; ESM::RefId id = std::get(index); return SkillStat{ std::move(object), id }; } void cache(const Context& context, std::string_view prop, const sol::object& value) const { SelfObject* obj = mObject.asSelfObject(); addStatUpdateAction(context.mLuaManager, *obj); obj->mStatsCache[SelfObject::CachedStat{ &SkillStat::setValue, mId, prop }] = value; } static void setValue(Index index, std::string_view prop, const MWWorld::Ptr& ptr, const sol::object& value) { ESM::RefId id = std::get(index); auto& stats = ptr.getClass().getNpcStats(ptr); auto stat = stats.getSkill(id); float floatValue = LuaUtil::cast(value); if (prop == "base") stat.setBase(floatValue); else if (prop == "damage") { stat.restore(stat.getDamage()); stat.damage(floatValue); } else if (prop == "modifier") stat.setModifier(floatValue); else if (prop == "progress") stat.setProgress(floatValue * getMaxProgress(ptr, id, stat)); stats.setSkill(id, stat); } }; } 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 { }; template <> struct is_automagical : std::false_type { }; template <> struct is_automagical : std::false_type { }; template <> struct is_automagical : std::false_type { }; } namespace MWLua { void addActorStatsBindings(sol::table& actor, const Context& context) { sol::table stats(context.mLua->sol(), sol::create); actor["stats"] = LuaUtil::makeReadOnly(stats); auto skillIncreasesForAttributeStatsT = context.mLua->sol().new_usertype("SkillIncreasesForAttributeStats"); for (auto attribute : MWBase::Environment::get().getESMStore()->get()) { skillIncreasesForAttributeStatsT[ESM::RefId(attribute.mId).serializeText()] = sol::property( [=](const SkillIncreasesForAttributeStats& stat) { return stat.get(context, attribute.mId); }, [=](const SkillIncreasesForAttributeStats& stat, const sol::object& value) { stat.set(context, attribute.mId, value); }); } // ESM::Class::specializationIndexToLuaId.at(rec.mData.mSpecialization) auto skillIncreasesForSpecializationStatsT = context.mLua->sol().new_usertype( "skillIncreasesForSpecializationStats"); for (int i = 0; i < 3; i++) { std::string_view index = ESM::Class::specializationIndexToLuaId.at(i); skillIncreasesForSpecializationStatsT[index] = sol::property([=](const SkillIncreasesForSpecializationStats& stat) { return stat.get(context, i); }, [=](const SkillIncreasesForSpecializationStats& stat, const sol::object& value) { stat.set(context, i, value); }); } auto levelStatT = context.mLua->sol().new_usertype("LevelStat"); levelStatT["current"] = sol::property([context](const LevelStat& stat) { return stat.getCurrent(context); }, [context](const LevelStat& stat, const sol::object& value) { stat.setCurrent(context, value); }); levelStatT["progress"] = sol::property([context](const LevelStat& stat) { return stat.getProgress(context); }, [context](const LevelStat& stat, const sol::object& value) { stat.setProgress(context, value); }); levelStatT["skillIncreasesForAttribute"] = sol::readonly_property([](const LevelStat& stat) { return stat.getSkillIncreasesForAttributeStats(); }); levelStatT["skillIncreasesForSpecialization"] = sol::readonly_property( [](const LevelStat& stat) { return stat.getSkillIncreasesForSpecializationStats(); }); stats["level"] = addIndexedAccessor(0); auto dynamicStatT = context.mLua->sol().new_usertype("DynamicStat"); addProp(context, dynamicStatT, "base", &MWMechanics::DynamicStat::getBase); addProp(context, dynamicStatT, "current", &MWMechanics::DynamicStat::getCurrent); addProp(context, dynamicStatT, "modifier", &MWMechanics::DynamicStat::getModifier); sol::table dynamic(context.mLua->sol(), sol::create); stats["dynamic"] = LuaUtil::makeReadOnly(dynamic); dynamic["health"] = addIndexedAccessor(0); dynamic["magicka"] = addIndexedAccessor(1); dynamic["fatigue"] = addIndexedAccessor(2); auto attributeStatT = context.mLua->sol().new_usertype("AttributeStat"); addProp(context, attributeStatT, "base", &MWMechanics::AttributeValue::getBase); addProp(context, attributeStatT, "damage", &MWMechanics::AttributeValue::getDamage); attributeStatT["modified"] = sol::readonly_property([=](const AttributeStat& stat) { return stat.getModified(context); }); addProp(context, attributeStatT, "modifier", &MWMechanics::AttributeValue::getModifier); sol::table attributes(context.mLua->sol(), sol::create); stats["attributes"] = LuaUtil::makeReadOnly(attributes); for (const ESM::Attribute& attribute : MWBase::Environment::get().getESMStore()->get()) attributes[ESM::RefId(attribute.mId).serializeText()] = addIndexedAccessor(attribute.mId); } void addNpcStatsBindings(sol::table& npc, const Context& context) { sol::table npcStats(context.mLua->sol(), sol::create); sol::table baseMeta(context.mLua->sol(), sol::create); baseMeta[sol::meta_function::index] = LuaUtil::getMutableFromReadOnly(npc["baseType"]["stats"]); npcStats[sol::metatable_key] = baseMeta; npc["stats"] = LuaUtil::makeReadOnly(npcStats); auto skillStatT = context.mLua->sol().new_usertype("SkillStat"); addProp(context, skillStatT, "base", &MWMechanics::SkillValue::getBase); addProp(context, skillStatT, "damage", &MWMechanics::SkillValue::getDamage); skillStatT["modified"] = sol::readonly_property([=](const SkillStat& stat) { return stat.getModified(context); }); addProp(context, skillStatT, "modifier", &MWMechanics::SkillValue::getModifier); skillStatT["progress"] = sol::property([context](const SkillStat& stat) { return stat.getProgress(context); }, [context](const SkillStat& stat, const sol::object& value) { stat.cache(context, "progress", value); }); sol::table skills(context.mLua->sol(), sol::create); npcStats["skills"] = LuaUtil::makeReadOnly(skills); for (const ESM::Skill& skill : MWBase::Environment::get().getESMStore()->get()) skills[ESM::RefId(skill.mId).serializeText()] = addIndexedAccessor(skill.mId); } sol::table initCoreStatsBindings(const Context& context) { sol::state_view& lua = context.mLua->sol(); sol::table statsApi(lua, sol::create); auto* vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); sol::table attributes(lua, sol::create); addRecordFunctionBinding(attributes, context); statsApi["Attribute"] = LuaUtil::makeReadOnly(attributes); statsApi["Attribute"][sol::metatable_key][sol::meta_function::to_string] = ESM::Attribute::getRecordType; auto attributeT = context.mLua->sol().new_usertype("Attribute"); attributeT[sol::meta_function::to_string] = [](const ESM::Attribute& rec) { return "ESM3_Attribute[" + rec.mId.toDebugString() + "]"; }; attributeT["id"] = sol::readonly_property( [](const ESM::Attribute& rec) -> std::string { return ESM::RefId{ rec.mId }.serializeText(); }); attributeT["name"] = sol::readonly_property([](const ESM::Attribute& rec) -> std::string_view { return rec.mName; }); attributeT["description"] = sol::readonly_property([](const ESM::Attribute& rec) -> std::string_view { return rec.mDescription; }); attributeT["icon"] = sol::readonly_property([vfs](const ESM::Attribute& rec) -> std::string { return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); sol::table skills(lua, sol::create); addRecordFunctionBinding(skills, context); statsApi["Skill"] = LuaUtil::makeReadOnly(skills); statsApi["Skill"][sol::metatable_key][sol::meta_function::to_string] = ESM::Skill::getRecordType; auto skillT = context.mLua->sol().new_usertype("Skill"); skillT[sol::meta_function::to_string] = [](const ESM::Skill& rec) { return "ESM3_Skill[" + rec.mId.toDebugString() + "]"; }; skillT["id"] = sol::readonly_property( [](const ESM::Skill& rec) -> std::string { return ESM::RefId{ rec.mId }.serializeText(); }); skillT["name"] = sol::readonly_property([](const ESM::Skill& rec) -> std::string_view { return rec.mName; }); skillT["description"] = sol::readonly_property([](const ESM::Skill& rec) -> std::string_view { return rec.mDescription; }); skillT["specialization"] = sol::readonly_property([](const ESM::Skill& rec) -> std::string_view { return ESM::Class::specializationIndexToLuaId.at(rec.mData.mSpecialization); }); skillT["icon"] = sol::readonly_property([vfs](const ESM::Skill& rec) -> std::string { return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); skillT["school"] = sol::readonly_property([](const ESM::Skill& rec) -> const ESM::MagicSchool* { if (!rec.mSchool) return nullptr; return &*rec.mSchool; }); skillT["attribute"] = sol::readonly_property([](const ESM::Skill& rec) -> std::string { return ESM::Attribute::indexToRefId(rec.mData.mAttribute).serializeText(); }); skillT["skillGain1"] = sol::readonly_property([](const ESM::Skill& rec) -> float { return rec.mData.mUseValue[0]; }); skillT["skillGain2"] = sol::readonly_property([](const ESM::Skill& rec) -> float { return rec.mData.mUseValue[1]; }); skillT["skillGain3"] = sol::readonly_property([](const ESM::Skill& rec) -> float { return rec.mData.mUseValue[2]; }); skillT["skillGain4"] = sol::readonly_property([](const ESM::Skill& rec) -> float { return rec.mData.mUseValue[3]; }); auto schoolT = context.mLua->sol().new_usertype("MagicSchool"); schoolT[sol::meta_function::to_string] = [](const ESM::MagicSchool& rec) { return "ESM3_MagicSchool[" + rec.mName + "]"; }; schoolT["name"] = sol::readonly_property([](const ESM::MagicSchool& rec) -> std::string_view { return rec.mName; }); schoolT["areaSound"] = sol::readonly_property( [](const ESM::MagicSchool& rec) -> std::string { return rec.mAreaSound.serializeText(); }); schoolT["boltSound"] = sol::readonly_property( [](const ESM::MagicSchool& rec) -> std::string { return rec.mBoltSound.serializeText(); }); schoolT["castSound"] = sol::readonly_property( [](const ESM::MagicSchool& rec) -> std::string { return rec.mCastSound.serializeText(); }); schoolT["failureSound"] = sol::readonly_property( [](const ESM::MagicSchool& rec) -> std::string { return rec.mFailureSound.serializeText(); }); schoolT["hitSound"] = sol::readonly_property( [](const ESM::MagicSchool& rec) -> std::string { return rec.mHitSound.serializeText(); }); return LuaUtil::makeReadOnly(statsApi); } }