Merge branch 'noitsnotdehardcoded' into 'master'

Add magic effects to openmw.content

Closes #8962

See merge request OpenMW/openmw!5248
This commit is contained in:
Alexei Kotov
2026-04-03 21:46:06 +03:00
12 changed files with 241 additions and 79 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ message(STATUS "Configuring OpenMW...")
set(OPENMW_VERSION_MAJOR 0)
set(OPENMW_VERSION_MINOR 51)
set(OPENMW_VERSION_RELEASE 0)
set(OPENMW_LUA_API_REVISION 123)
set(OPENMW_LUA_API_REVISION 124)
set(OPENMW_POSTPROCESSING_API_REVISION 5)
set(OPENMW_VERSION_COMMITHASH "")
+22
View File
@@ -297,6 +297,27 @@ namespace MWLua
return LuaUtil::makeReadOnly(api);
}
sol::table initMagicEffectBindings(sol::state_view& lua, MWWorld::Store<ESM::MagicEffect>& store)
{
addRecordStoreBindings<ESM::MagicEffect>(lua, &MWLua::tableToMagicEffect);
addMutableMagicEffectType(lua);
sol::table api(lua, sol::create);
api["records"] = MutableStore<ESM::MagicEffect>{ store };
// We can't get rid of the GMST table engine side because mwscript needs it, so intead of copying it into a
// Lua file we've got this hidden function to generate it
api["_getGMSTs"] = [](sol::this_state state) {
sol::table gmsts(state, sol::create);
for (int i = 0; i < ESM::MagicEffect::Length; ++i)
{
const ESM::RefId effect = ESM::MagicEffect::indexToRefId(i);
const std::string_view gmst = ESM::MagicEffect::refIdToGmstString(effect);
gmsts[effect] = gmst;
}
return gmsts;
};
return LuaUtil::makeReadOnly(api);
}
sol::table initMiscBindings(sol::state_view& lua, MWWorld::Store<ESM::Miscellaneous>& store)
{
addRecordStoreBindings<ESM::Miscellaneous>(lua, &MWLua::tableToMisc);
@@ -364,6 +385,7 @@ namespace MWLua
api["gameSettings"] = initGameSettingBindings(lua, esmStore.getWritable<ESM::GameSetting>());
api["globals"] = initGlobalVariableBindings(lua, esmStore.getWritable<ESM::Global>());
api["ingredients"] = initIngredientBindings(lua, esmStore.getWritable<ESM::Ingredient>());
api["magicEffects"] = initMagicEffectBindings(lua, esmStore.getWritable<ESM::MagicEffect>());
api["miscs"] = initMiscBindings(lua, esmStore.getWritable<ESM::Miscellaneous>());
api["potions"] = initPotionBindings(lua, esmStore.getWritable<ESM::Potion>());
api["spells"] = initSpellBindings(lua, esmStore.getWritable<ESM::Spell>());
+1 -61
View File
@@ -142,10 +142,6 @@ namespace MWLua
namespace sol
{
template <>
struct is_automagical<ESM::MagicEffect> : std::false_type
{
};
template <typename T>
struct is_automagical<MWLua::ActorStore<T>> : std::false_type
{
@@ -266,63 +262,7 @@ namespace MWLua
addEffectParamsBindings(state);
// MagicEffect record
auto magicEffectT = state.new_usertype<ESM::MagicEffect>("ESM3_MagicEffect");
magicEffectT[sol::meta_function::to_string]
= [](const ESM::MagicEffect& rec) { return std::format("ESM3_MagicEffect[{}]", rec.mId.toDebugString()); };
magicEffectT["id"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> ESM::RefId { return rec.mId; });
magicEffectT["icon"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> std::string {
auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS();
return Misc::ResourceHelpers::correctIconPath(VFS::Path::toNormalized(rec.mIcon), *vfs);
});
magicEffectT["particle"]
= sol::readonly_property([](const ESM::MagicEffect& rec) -> std::string_view { return rec.mParticle; });
magicEffectT["continuousVfx"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> bool {
return (rec.mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0;
});
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["bolt"] = 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(); });
magicEffectT["hitStatic"] = sol::readonly_property(
[](const ESM::MagicEffect& rec) -> std::string { return rec.mHit.serializeText(); });
magicEffectT["name"]
= sol::readonly_property([](const ESM::MagicEffect& rec) -> std::string_view { return rec.mName; });
magicEffectT["school"] = sol::readonly_property(
[](const ESM::MagicEffect& rec) -> std::string { return rec.mData.mSchool.serializeText(); });
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["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(
[](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?
// magicEffectT["projectileSpeed"]
// = sol::readonly_property([](const ESM::MagicEffect& rec) -> float { return rec.mData.mSpeed; });
addMagicEffectType(state);
auto activeSpellEffectT = state.new_usertype<ESM::ActiveEffect>("ActiveSpellEffect");
activeSpellEffectT[sol::meta_function::to_string] = [](const ESM::ActiveEffect& self) {
+156 -1
View File
@@ -5,6 +5,7 @@
#include <components/esm3/loadmgef.hpp>
#include <components/esm3/loadspel.hpp>
#include <components/lua/util.hpp>
#include <components/misc/color.hpp>
#include "../mwbase/environment.hpp"
#include "../mwworld/esmstore.hpp"
@@ -55,6 +56,10 @@ namespace sol
{
};
template <>
struct is_automagical<ESM::MagicEffect> : std::false_type
{
};
template <>
struct is_automagical<ESM::Potion> : std::false_type
{
};
@@ -132,7 +137,7 @@ namespace MWLua
addEffectsProperty(lua, record);
}
void addPropertyFromTable(const sol::lua_table& rec, std::string_view key, ESM::RefId& value)
void addPropertyFromTable(const sol::table& rec, std::string_view key, ESM::RefId& value)
{
if (rec[key] != sol::nil)
{
@@ -293,6 +298,100 @@ namespace MWLua
flags &= ~flag;
}
}
void setReverseFlagProperty(const sol::table& rec, std::string_view key, int32_t& flags, int flag)
{
if (rec[key] != sol::nil)
{
if (!rec[key])
flags |= flag;
else
flags &= ~flag;
}
}
void assignColor(ESM::MagicEffect& effect, const Misc::Color& color)
{
effect.mData.mRed = std::clamp(static_cast<int32_t>(color.r() * 255), 0, 255);
effect.mData.mGreen = std::clamp(static_cast<int32_t>(color.g() * 255), 0, 255);
effect.mData.mBlue = std::clamp(static_cast<int32_t>(color.b() * 255), 0, 255);
}
template <class T>
void addMagicEffectType(sol::state_view& lua, std::string_view name)
{
sol::usertype<T> record = lua.new_usertype<T>(name);
record[sol::meta_function::to_string]
= [](const T& rec) -> std::string { return "ESM3_MagicEffect[" + rec.mId.toDebugString() + "]"; };
record["id"] = sol::readonly_property([](const T& rec) -> ESM::RefId { return rec.mId; });
Types::addIconProperty(record);
Types::addProperty(record, "particle", &ESM::MagicEffect::mParticle);
Types::addFlagProperty(record, "continuousVfx", ESM::MagicEffect::ContinuousVfx, &ESM::MagicEffect::mData,
&ESM::MagicEffect::MEDTstruct::mFlags);
Types::addProperty(record, "areaSound", &ESM::MagicEffect::mAreaSound);
Types::addProperty(record, "boltSound", &ESM::MagicEffect::mBoltSound);
Types::addProperty(record, "castSound", &ESM::MagicEffect::mCastSound);
Types::addProperty(record, "hitSound", &ESM::MagicEffect::mHitSound);
Types::addProperty(record, "areaStatic", &ESM::MagicEffect::mArea);
Types::addProperty(record, "bolt", &ESM::MagicEffect::mBolt);
Types::addProperty(record, "castStatic", &ESM::MagicEffect::mCasting);
Types::addProperty(record, "hitStatic", &ESM::MagicEffect::mHit);
Types::addProperty(record, "name", &ESM::MagicEffect::mName);
Types::addProperty(record, "school", &ESM::MagicEffect::mData, &ESM::MagicEffect::MEDTstruct::mSchool);
Types::addProperty(record, "baseCost", &ESM::MagicEffect::mData, &ESM::MagicEffect::MEDTstruct::mBaseCost);
const auto getColor = [](const T& rec) -> Misc::Color {
const ESM::MagicEffect& effect = Types::RecordType<T>::asRecord(rec);
return Misc::Color(
effect.mData.mRed / 255.f, effect.mData.mGreen / 255.f, effect.mData.mBlue / 255.f, 1.f);
};
if constexpr (Types::RecordType<T>::isMutable)
{
record["color"] = sol::property(std::move(getColor), [](T& rec, const Misc::Color& color) {
ESM::MagicEffect& effect = rec.find();
assignColor(effect, color);
});
}
else
{
record["color"] = sol::readonly_property(std::move(getColor));
}
Types::addReverseFlagProperty(record, "hasDuration", ESM::MagicEffect::NoDuration, &ESM::MagicEffect::mData,
&ESM::MagicEffect::MEDTstruct::mFlags);
Types::addReverseFlagProperty(record, "hasMagnitude", ESM::MagicEffect::NoMagnitude,
&ESM::MagicEffect::mData, &ESM::MagicEffect::MEDTstruct::mFlags);
// TODO: Not self-explanatory. Needs either a better name or documentation. The description in
// loadmgef.hpp is uninformative.
Types::addFlagProperty(record, "isAppliedOnce", ESM::MagicEffect::AppliedOnce, &ESM::MagicEffect::mData,
&ESM::MagicEffect::MEDTstruct::mFlags);
Types::addFlagProperty(record, "harmful", ESM::MagicEffect::Harmful, &ESM::MagicEffect::mData,
&ESM::MagicEffect::MEDTstruct::mFlags);
Types::addFlagProperty(record, "casterLinked", ESM::MagicEffect::CasterLinked, &ESM::MagicEffect::mData,
&ESM::MagicEffect::MEDTstruct::mFlags);
Types::addFlagProperty(record, "nonRecastable", ESM::MagicEffect::NonRecastable, &ESM::MagicEffect::mData,
&ESM::MagicEffect::MEDTstruct::mFlags);
Types::addFlagProperty(record, "hasAttribute", ESM::MagicEffect::TargetAttribute, &ESM::MagicEffect::mData,
&ESM::MagicEffect::MEDTstruct::mFlags);
Types::addFlagProperty(record, "hasSkill", ESM::MagicEffect::TargetSkill, &ESM::MagicEffect::mData,
&ESM::MagicEffect::MEDTstruct::mFlags);
Types::addFlagProperty(record, "onSelf", ESM::MagicEffect::CastSelf, &ESM::MagicEffect::mData,
&ESM::MagicEffect::MEDTstruct::mFlags);
Types::addFlagProperty(record, "onTouch", ESM::MagicEffect::CastTouch, &ESM::MagicEffect::mData,
&ESM::MagicEffect::MEDTstruct::mFlags);
Types::addFlagProperty(record, "onTarget", ESM::MagicEffect::CastTarget, &ESM::MagicEffect::mData,
&ESM::MagicEffect::MEDTstruct::mFlags);
Types::addFlagProperty(record, "unreflectable", ESM::MagicEffect::Unreflectable, &ESM::MagicEffect::mData,
&ESM::MagicEffect::MEDTstruct::mFlags);
Types::addFlagProperty(record, "allowsSpellmaking", ESM::MagicEffect::AllowSpellmaking,
&ESM::MagicEffect::mData, &ESM::MagicEffect::MEDTstruct::mFlags);
Types::addFlagProperty(record, "allowsEnchanting", ESM::MagicEffect::AllowEnchanting,
&ESM::MagicEffect::mData, &ESM::MagicEffect::MEDTstruct::mFlags);
Types::addFlagProperty(record, "negativeLight", ESM::MagicEffect::NegativeLight, &ESM::MagicEffect::mData,
&ESM::MagicEffect::MEDTstruct::mFlags);
Types::addProperty(record, "speed", &ESM::MagicEffect::mData, &ESM::MagicEffect::MEDTstruct::mSpeed);
}
}
void addSpellBindings(sol::state_view& state)
@@ -382,4 +481,60 @@ namespace MWLua
out.updateIndexes();
return out;
}
void addMagicEffectType(sol::state_view& lua)
{
addMagicEffectType<ESM::MagicEffect>(lua, "ESM3_MagicEffect");
}
void addMutableMagicEffectType(sol::state_view& lua)
{
addMagicEffectType<MutableRecord<ESM::MagicEffect>>(lua, "ESM3_MutableMagicEffect");
}
ESM::MagicEffect tableToMagicEffect(const sol::table& rec)
{
auto effect = Types::initFromTemplate<ESM::MagicEffect>(rec);
if (rec["icon"] != sol::nil)
effect.mIcon = rec["icon"];
if (rec["particle"] != sol::nil)
effect.mParticle = rec["particle"];
setFlagProperty(rec, "continuousVfx", effect.mData.mFlags, ESM::MagicEffect::ContinuousVfx);
addPropertyFromTable(rec, "areaSound", effect.mAreaSound);
addPropertyFromTable(rec, "boltSound", effect.mBoltSound);
addPropertyFromTable(rec, "castSound", effect.mCastSound);
addPropertyFromTable(rec, "hitSound", effect.mHitSound);
addPropertyFromTable(rec, "areaStatic", effect.mArea);
addPropertyFromTable(rec, "bolt", effect.mBolt);
addPropertyFromTable(rec, "castStatic", effect.mCasting);
addPropertyFromTable(rec, "hitStatic", effect.mHit);
if (rec["name"] != sol::nil)
effect.mName = rec["name"];
addPropertyFromTable(rec, "school", effect.mData.mSchool);
if (rec["baseCost"] != sol::nil)
effect.mData.mBaseCost = rec["baseCost"];
if (rec["color"] != sol::nil)
{
const auto color = LuaUtil::cast<Misc::Color>(rec["color"]);
assignColor(effect, color);
}
setReverseFlagProperty(rec, "hasDuration", effect.mData.mFlags, ESM::MagicEffect::NoDuration);
setReverseFlagProperty(rec, "hasMagnitude", effect.mData.mFlags, ESM::MagicEffect::NoMagnitude);
setFlagProperty(rec, "isAppliedOnce", effect.mData.mFlags, ESM::MagicEffect::AppliedOnce);
setFlagProperty(rec, "harmful", effect.mData.mFlags, ESM::MagicEffect::Harmful);
setFlagProperty(rec, "casterLinked", effect.mData.mFlags, ESM::MagicEffect::CasterLinked);
setFlagProperty(rec, "nonRecastable", effect.mData.mFlags, ESM::MagicEffect::NonRecastable);
setFlagProperty(rec, "hasAttribute", effect.mData.mFlags, ESM::MagicEffect::TargetAttribute);
setFlagProperty(rec, "hasSkill", effect.mData.mFlags, ESM::MagicEffect::TargetSkill);
setFlagProperty(rec, "onSelf", effect.mData.mFlags, ESM::MagicEffect::CastSelf);
setFlagProperty(rec, "onTouch", effect.mData.mFlags, ESM::MagicEffect::CastTouch);
setFlagProperty(rec, "onTarget", effect.mData.mFlags, ESM::MagicEffect::CastTarget);
setFlagProperty(rec, "unreflectable", effect.mData.mFlags, ESM::MagicEffect::Unreflectable);
setFlagProperty(rec, "allowsSpellmaking", effect.mData.mFlags, ESM::MagicEffect::AllowSpellmaking);
setFlagProperty(rec, "allowsEnchanting", effect.mData.mFlags, ESM::MagicEffect::AllowEnchanting);
setFlagProperty(rec, "negativeLight", effect.mData.mFlags, ESM::MagicEffect::NegativeLight);
if (rec["speed"] != sol::nil)
effect.mData.mSpeed = rec["speed"];
return effect;
}
}
+5
View File
@@ -7,6 +7,7 @@ namespace ESM
{
struct EffectList;
struct Enchantment;
struct MagicEffect;
struct Potion;
struct Spell;
}
@@ -25,6 +26,10 @@ namespace MWLua
void addMutablePotionType(sol::state_view& lua);
ESM::EffectList tableToEffectList(const sol::table&);
void addMagicEffectType(sol::state_view&);
void addMutableMagicEffectType(sol::state_view& lua);
ESM::MagicEffect tableToMagicEffect(const sol::table&);
}
#endif // MWLUA_MAGICTYPEBINDINGS_H
+21
View File
@@ -111,6 +111,27 @@ namespace MWLua::Types
type[key] = sol::readonly_property(std::move(getter));
}
template <class Type, class Flag, class... Member>
void addReverseFlagProperty(sol::usertype<Type>& type, std::string_view key, Flag flag, Member... members)
{
using Record = RecordType<Type>::Record;
const auto getter = [=](const Type& rec) -> bool {
const Record& record = RecordType<Type>::asRecord(rec);
return !((record.*....*members) & flag);
};
if constexpr (RecordType<Type>::isMutable)
type[key] = sol::property(std::move(getter), [=](Type& rec, bool value) {
Record& record = rec.find();
auto& data = (record.*....*members);
if (value)
data &= ~flag;
else
data |= flag;
});
else
type[key] = sol::readonly_property(std::move(getter));
}
template <class T>
void addModelProperty(sol::usertype<T>& type)
{
-1
View File
@@ -525,7 +525,6 @@ namespace MWWorld
store->setUp();
getWritable<ESM::Skill>().setUp(get<ESM::GameSetting>());
getWritable<ESM::MagicEffect>().setUp(get<ESM::GameSetting>());
getWritable<ESM::Attribute>().setUp(get<ESM::GameSetting>());
getWritable<ESM4::Land>().updateLandPositions(get<ESM4::Cell>());
getWritable<ESM4::Reference>().preprocessReferences(get<ESM4::Cell>());
-13
View File
@@ -999,19 +999,6 @@ namespace MWWorld
.mWerewolfValue = getGMSTFloat(settings, "fWerewolfLuck") });
}
// Magic Effect
//=========================================================================
void Store<ESM::MagicEffect>::setUp(const MWWorld::Store<ESM::GameSetting>& settings)
{
for (ESM::MagicEffect* mgef : mShared)
{
std::string_view gmst = ESM::MagicEffect::refIdToGmstString(mgef->mId);
if (!gmst.empty())
mgef->mName = getGMSTString(settings, gmst);
}
}
// Dialogue
//=========================================================================
-2
View File
@@ -466,8 +466,6 @@ namespace MWWorld
public:
Store() = default;
void setUp(const MWWorld::Store<ESM::GameSetting>& settings);
};
template <>
@@ -82,12 +82,28 @@ local function generateDefaultDoors()
end
end
local function setMagicEffectNames()
local gmsts = content.gameSettings.records
local effects = content.magicEffects.records
for id, gmst in pairs(content.magicEffects._getGMSTs()) do
local effect = effects[id]
if effect ~= nil then
local name = gmsts[gmst]
if type(name) ~= 'string' or name == '' then
name = gmst
end
effect.name = name
end
end
end
return {
engineHandlers = {
onContentFilesLoaded = function()
generateDefaultDoors()
generateDefaultGMSTs()
generateDefaultStatics()
setMagicEffectNames()
end
}
}
+9
View File
@@ -76,6 +76,15 @@
--- @{#MiscContent}: Misc manipulation.
-- @field [parent=#content] #MiscContent miscs
--- @{#MagicEffectContent}: Magic effect manipulation.
-- @field [parent=#content] #MagicEffectContent magicEffects
---
-- A mutable list of all @{openmw.core#MagicEffect}s.
-- @field [parent=#MagicEffectContent] #list<openmw.core#MagicEffect> records
-- @usage
-- content.magicEffects.records.MyMagicEffect = { template = content.magicEffects.records['summonscamp'], name = 'Summon Nothing' }
---
-- A mutable list of all @{openmw.types#MiscellaneousRecord}s.
-- @field [parent=#MiscContent] #list<openmw.types#MiscellaneousRecord> records
+10
View File
@@ -772,6 +772,16 @@
-- @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
-- @field #boolean hasAttribute True if the effect requires an attribute parameter
-- @field #boolean hasSkill True if the effect requires a skill parameter
-- @field #boolean onSelf True if the effect can be cast on self
-- @field #boolean onTouch True if the effect can be cast on touch
-- @field #boolean onTarget True if the effect can be cast on target
-- @field #boolean unreflectable True if the effect cannot be reflected
-- @field #boolean allowsSpellmaking True if the effect is available for spellmaking
-- @field #boolean allowsEnchanting True if the effect is available for enchanting
-- @field #boolean negativeLight True if the effect casts negative light
-- @field #number speed Unused
---