diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index d527b73a90..997eea667b 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -62,7 +62,7 @@ add_openmw_dir (mwlua luamanagerimp object worldview userdataserializer luaevents engineevents objectvariant context globalscripts localscripts playerscripts luabindings objectbindings cellbindings camerabindings uibindings inputbindings nearbybindings postprocessingbindings stats debugbindings - types/types types/door types/actor types/container types/lockable types/weapon types/npc types/creature types/player types/activator types/book types/lockpick types/probe types/apparatus types/potion types/ingredient types/misc types/repair types/armor types/light types/static types/clothing types/levelledlist + types/types types/door types/item types/actor types/container types/lockable types/weapon types/npc types/creature types/player types/activator types/book types/lockpick types/probe types/apparatus types/potion types/ingredient types/misc types/repair types/armor types/light types/static types/clothing types/levelledlist worker magicbindings ) diff --git a/apps/openmw/mwlua/magicbindings.cpp b/apps/openmw/mwlua/magicbindings.cpp index 28755a69de..75440ce7e4 100644 --- a/apps/openmw/mwlua/magicbindings.cpp +++ b/apps/openmw/mwlua/magicbindings.cpp @@ -1,6 +1,7 @@ #include "magicbindings.hpp" #include +#include #include #include #include @@ -184,6 +185,13 @@ namespace MWLua { "Curse", ESM::Spell::ST_Curse }, { "Power", ESM::Spell::ST_Power }, })); + magicApi["ENCHANTMENT_TYPE"] + = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ + { "CastOnce", ESM::Enchantment::Type::CastOnce }, + { "CastOnStrike", ESM::Enchantment::Type::WhenStrikes }, + { "CastOnUse", ESM::Enchantment::Type::WhenUsed }, + { "ConstantEffect", ESM::Enchantment::Type::ConstantEffect }, + })); sol::table effect(context.mLua->sol(), sol::create); magicApi["EFFECT_TYPE"] = LuaUtil::makeStrictReadOnly(effect); @@ -209,6 +217,25 @@ namespace MWLua magicApi["spells"] = spellStore; + // Enchantment store + using EnchantmentStore = MWWorld::Store; + const EnchantmentStore* enchantmentStore + = &MWBase::Environment::get().getWorld()->getStore().get(); + sol::usertype enchantmentStoreT = lua.new_usertype("ESM3_EnchantmentStore"); + enchantmentStoreT[sol::meta_function::to_string] = [](const EnchantmentStore& store) { + return "ESM3_EnchantmentStore{" + std::to_string(store.getSize()) + " enchantments}"; + }; + enchantmentStoreT[sol::meta_function::length] = [](const EnchantmentStore& store) { return store.getSize(); }; + enchantmentStoreT[sol::meta_function::index] = sol::overload( + [](const EnchantmentStore& store, size_t index) -> const ESM::Enchantment* { return store.at(index - 1); }, + [](const EnchantmentStore& store, std::string_view enchantmentId) -> const ESM::Enchantment* { + return store.find(ESM::RefId::deserializeText(enchantmentId)); + }); + enchantmentStoreT[sol::meta_function::pairs] = lua["ipairsForArray"].template get(); + enchantmentStoreT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); + + magicApi["enchantments"] = enchantmentStore; + // MagicEffect store using MagicEffectStore = MWWorld::Store; const MagicEffectStore* magicEffectStore @@ -255,6 +282,25 @@ namespace MWLua return res; }); + // Enchantment record + auto enchantT = lua.new_usertype("ESM3_Enchantment"); + enchantT[sol::meta_function::to_string] = [](const ESM::Enchantment& rec) -> std::string { + return "ESM3_Enchantment[" + rec.mId.toDebugString() + "]"; + }; + enchantT["id"] = sol::readonly_property([](const ESM::Enchantment& rec) { return rec.mId.serializeText(); }); + enchantT["type"] = sol::readonly_property([](const ESM::Enchantment& rec) -> int { return rec.mData.mType; }); + enchantT["autocalcFlag"] = sol::readonly_property( + [](const ESM::Enchantment& rec) -> bool { return !!(rec.mData.mFlags & ESM::Enchantment::Autocalc); }); + enchantT["cost"] = sol::readonly_property([](const ESM::Enchantment& rec) -> int { return rec.mData.mCost; }); + enchantT["charge"] + = sol::readonly_property([](const ESM::Enchantment& rec) -> int { return rec.mData.mCharge; }); + enchantT["effects"] = sol::readonly_property([&lua](const ESM::Enchantment& 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) { diff --git a/apps/openmw/mwlua/types/actor.cpp b/apps/openmw/mwlua/types/actor.cpp index bf0866bc0f..42d25789f6 100644 --- a/apps/openmw/mwlua/types/actor.cpp +++ b/apps/openmw/mwlua/types/actor.cpp @@ -19,6 +19,46 @@ namespace MWLua { using EquipmentItem = std::variant; using Equipment = std::map; + static constexpr int sAnySlot = -1; + + static std::pair findInInventory( + MWWorld::InventoryStore& store, const EquipmentItem& item, int slot = sAnySlot) + { + auto old_it = slot != sAnySlot ? store.getSlot(slot) : store.end(); + MWWorld::Ptr itemPtr; + + if (std::holds_alternative(item)) + { + itemPtr = MWBase::Environment::get().getWorldModel()->getPtr(std::get(item)); + if (old_it != store.end() && *old_it == itemPtr) + return { old_it, true }; // already equipped + if (itemPtr.isEmpty() || itemPtr.getRefData().getCount() == 0 + || itemPtr.getContainerStore() != static_cast(&store)) + { + Log(Debug::Warning) << "Object" << std::get(item).toString() << " is not in inventory"; + return { store.end(), false }; + } + } + else + { + ESM::RefId recordId = ESM::RefId::deserializeText(std::get(item)); + if (old_it != store.end() && old_it->getCellRef().getRefId() == recordId) + return { old_it, true }; // already equipped + itemPtr = store.search(recordId); + if (itemPtr.isEmpty() || itemPtr.getRefData().getCount() == 0) + { + Log(Debug::Warning) << "There is no object with recordId='" << recordId << "' in inventory"; + return { store.end(), false }; + } + } + + // TODO: Refactor InventoryStore to accept Ptr and get rid of this linear search. + MWWorld::ContainerStoreIterator it = std::find(store.begin(), store.end(), itemPtr); + if (it == store.end()) // should never happen + throw std::logic_error("Item not found in container"); + + return { it, false }; + } static void setEquipment(const MWWorld::Ptr& actor, const Equipment& equipment) { @@ -26,34 +66,13 @@ namespace MWLua std::array usedSlots; std::fill(usedSlots.begin(), usedSlots.end(), false); - static constexpr int anySlot = -1; auto tryEquipToSlot = [&store, &usedSlots](int slot, const EquipmentItem& item) -> bool { - auto old_it = slot != anySlot ? store.getSlot(slot) : store.end(); - MWWorld::Ptr itemPtr; - if (std::holds_alternative(item)) - { - itemPtr = MWBase::Environment::get().getWorldModel()->getPtr(std::get(item)); - if (old_it != store.end() && *old_it == itemPtr) - return true; // already equipped - if (itemPtr.isEmpty() || itemPtr.getRefData().getCount() == 0 - || itemPtr.getContainerStore() != static_cast(&store)) - { - Log(Debug::Warning) << "Object" << std::get(item).toString() << " is not in inventory"; - return false; - } - } - else - { - const ESM::RefId& recordId = ESM::RefId::stringRefId(std::get(item)); - if (old_it != store.end() && old_it->getCellRef().getRefId() == recordId) - return true; // already equipped - itemPtr = store.search(recordId); - if (itemPtr.isEmpty() || itemPtr.getRefData().getCount() == 0) - { - Log(Debug::Warning) << "There is no object with recordId='" << recordId << "' in inventory"; - return false; - } - } + auto [it, alreadyEquipped] = findInInventory(store, item, slot); + if (alreadyEquipped) + return true; + if (it == store.end()) + return false; + MWWorld::Ptr itemPtr = *it; auto [allowedSlots, _] = itemPtr.getClass().getEquipmentSlots(itemPtr); bool requestedSlotIsAllowed @@ -70,11 +89,6 @@ namespace MWLua slot = *firstAllowed; } - // TODO: Refactor InventoryStore to accept Ptr and get rid of this linear search. - MWWorld::ContainerStoreIterator it = std::find(store.begin(), store.end(), itemPtr); - if (it == store.end()) // should never happen - throw std::logic_error("Item not found in container"); - store.equip(slot, it); return requestedSlotIsAllowed; // return true if equipped to requested slot and false if slot was changed }; @@ -94,7 +108,42 @@ namespace MWLua } for (const auto& [slot, item] : equipment) if (slot >= MWWorld::InventoryStore::Slots) - tryEquipToSlot(anySlot, item); + tryEquipToSlot(sAnySlot, item); + } + + static void setSelectedEnchantedItem(const MWWorld::Ptr& actor, const EquipmentItem& item) + { + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + // We're not passing in a specific slot, so ignore the already equipped return value + auto [it, _] = findInInventory(store, item, sAnySlot); + if (it == store.end()) + return; + + MWWorld::Ptr itemPtr = *it; + + // Equip the item if applicable + auto slots = itemPtr.getClass().getEquipmentSlots(itemPtr); + if (!slots.first.empty()) + { + bool alreadyEquipped = false; + for (auto slot : slots.first) + { + if (store.getSlot(slot) == it) + alreadyEquipped = true; + } + + if (!alreadyEquipped) + { + MWBase::Environment::get().getWindowManager()->useItem(itemPtr); + // make sure that item was successfully equipped + if (!store.isEquipped(itemPtr)) + return; + } + } + + store.setSelectedEnchantItem(it); + // to reset WindowManager::mSelectedSpell immediately + MWBase::Environment::get().getWindowManager()->setSelectedEnchantItem(*it); } void addActorBindings(sol::table actor, const Context& context) @@ -173,8 +222,37 @@ namespace MWLua stats.setDrawState(newDrawState); }; - // TODO - // getSelectedEnchantedItem, setSelectedEnchantedItem + actor["getSelectedEnchantedItem"] = [](sol::this_state lua, const Object& o) -> sol::object { + const MWWorld::Ptr& ptr = o.ptr(); + if (!ptr.getClass().hasInventoryStore(ptr)) + return sol::nil; + MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); + auto it = store.getSelectedEnchantItem(); + if (it == store.end()) + return sol::nil; + MWBase::Environment::get().getWorldModel()->registerPtr(*it); + if (dynamic_cast(&o)) + return sol::make_object(lua, GObject(*it)); + else + return sol::make_object(lua, LObject(*it)); + }; + actor["setSelectedEnchantedItem"] = [context](const SelfObject& obj, const sol::object& item) { + const MWWorld::Ptr& ptr = obj.ptr(); + if (!ptr.getClass().hasInventoryStore(ptr)) + return; + + EquipmentItem ei; + if (item.is()) + { + ei = LuaUtil::cast(item).id(); + } + else + { + ei = LuaUtil::cast(item); + } + context.mLuaManager->addAction([obj = Object(ptr), ei = ei] { setSelectedEnchantedItem(obj.ptr(), ei); }, + "setSelectedEnchantedItemAction"); + }; actor["canMove"] = [](const Object& o) { const MWWorld::Class& cls = o.ptr().getClass(); diff --git a/apps/openmw/mwlua/types/item.cpp b/apps/openmw/mwlua/types/item.cpp new file mode 100644 index 0000000000..cc3695786b --- /dev/null +++ b/apps/openmw/mwlua/types/item.cpp @@ -0,0 +1,15 @@ +#include "../luabindings.hpp" +#include "../worldview.hpp" + +#include "types.hpp" + +namespace MWLua +{ + void addItemBindings(sol::table item) + { + item["getEnchantmentCharge"] + = [](const Object& object) { return object.ptr().getCellRef().getEnchantmentCharge(); }; + item["setEnchantmentCharge"] + = [](const GObject& object, float charge) { object.ptr().getCellRef().setEnchantmentCharge(charge); }; + } +} diff --git a/apps/openmw/mwlua/types/types.cpp b/apps/openmw/mwlua/types/types.cpp index 053c882a3f..3f9680e44b 100644 --- a/apps/openmw/mwlua/types/types.cpp +++ b/apps/openmw/mwlua/types/types.cpp @@ -183,9 +183,9 @@ namespace MWLua addActorBindings( addType(ObjectTypeName::Actor, { ESM::REC_INTERNAL_PLAYER, ESM::REC_CREA, ESM::REC_NPC_ }), context); - addType(ObjectTypeName::Item, + addItemBindings(addType(ObjectTypeName::Item, { ESM::REC_ARMO, ESM::REC_BOOK, ESM::REC_CLOT, ESM::REC_INGR, ESM::REC_LIGH, ESM::REC_MISC, ESM::REC_ALCH, - ESM::REC_WEAP, ESM::REC_APPA, ESM::REC_LOCK, ESM::REC_PROB, ESM::REC_REPA }); + ESM::REC_WEAP, ESM::REC_APPA, ESM::REC_LOCK, ESM::REC_PROB, ESM::REC_REPA })); addLockableBindings( addType(ObjectTypeName::Lockable, { ESM::REC_CONT, ESM::REC_DOOR, ESM::REC_CONT4, ESM::REC_DOOR4 })); diff --git a/apps/openmw/mwlua/types/types.hpp b/apps/openmw/mwlua/types/types.hpp index 34b4991ac5..9f179a78e3 100644 --- a/apps/openmw/mwlua/types/types.hpp +++ b/apps/openmw/mwlua/types/types.hpp @@ -47,6 +47,7 @@ namespace MWLua void addBookBindings(sol::table book, const Context& context); void addContainerBindings(sol::table container, const Context& context); void addDoorBindings(sol::table door, const Context& context); + void addItemBindings(sol::table item); void addActorBindings(sol::table actor, const Context& context); void addWeaponBindings(sol::table weapon, const Context& context); void addNpcBindings(sol::table npc, const Context& context); diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index c5b7c3deba..bcb172bd62 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -317,6 +317,41 @@ -- local weapons = cell:getAll(types.Weapon) +--- Possible @{#EnchantmentType} values +-- @field [parent=#Magic] #EnchantmentType ENCHANTMENT_TYPE + +--- `core.magic.ENCHANTMENT_TYPE` +-- @type EnchantmentType +-- @field #number CastOnce Enchantment can be cast once, destroying the enchanted item. +-- @field #number CastOnStrike Enchantment is cast on strike, if there is enough charge. +-- @field #number CastOnUse Enchantment is cast when used, if there is enough charge. +-- @field #number ConstantEffect Enchantment is always active when equipped. + + +--- +-- @type Enchantment +-- @field #string id Enchantment id +-- @field #number type @{#EnchantmentType} +-- @field #number autocalcFlag If set, the casting cost should be computer rather than reading the cost field +-- @field #number cost +-- @field #number charge Charge capacity. Should not be confused with current charge. +-- @field #list<#MagicEffectWithParams> effects The effects (@{#MagicEffectWithParams}) of the enchantment +-- @usage -- Getting the enchantment of an arbitrary item, if it has one +-- local function getRecord(item) +-- if item.type and item.type.record then +-- return item.type.record(item) +-- end +-- return nil +-- end +-- local function getEnchantment(item) +-- local record = getRecord(item) +-- if record and record.enchant then +-- return core.magic.enchantments[record.enchant] +-- end +-- return nil +-- end + + --- -- Inventory of a player/NPC or a content of a container. -- @type Inventory @@ -583,7 +618,6 @@ -- @field #number SummonCreature04 "summoncreature04" -- @field #number SummonCreature05 "summoncreature05" - --- Possible @{#SpellType} values -- @field [parent=#Magic] #SpellType SPELL_TYPE @@ -617,6 +651,17 @@ -- end -- end +--- List of all @{#Enchantment}s. +-- @field [parent=#Magic] #list<#Enchantment> enchantments +-- @usage local enchantment = core.magic.enchantments['marara's boon'] -- get by id +-- @usage local enchantment = core.magic.enchantments[1] -- get by index +-- @usage -- Print all enchantments with constant effect +-- for _, ench in pairs(core.magic.enchantments) do +-- if ench.type == core.magic.ENCHANTMENT_TYPE.ConstantEffect then +-- print(ench.id) +-- end +-- end + --- -- @type Spell -- @field #string id Spell id diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index fe195f123c..30237deec5 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -161,6 +161,18 @@ -- @param openmw.core#GameObject actor -- @param openmw.core#Spell spell Spell (can be nil) +--- +-- Get currently selected enchanted item +-- @function [parent=#Actor] getSelectedEnchantedItem +-- @param openmw.core#GameObject actor +-- @return openmw.core#GameObject, nil enchanted item or nil + +--- +-- Set currently selected enchanted item, equipping it if applicable +-- @function [parent=#Actor] setSelectedEnchantedItem +-- @param openmw.core#GameObject actor +-- @param openmw.core#GameObject item enchanted item + --- -- Return the active magic effects (@{#ActorActiveEffects}) currently affecting the given actor. -- @function [parent=#Actor] activeEffects @@ -592,7 +604,17 @@ -- @param openmw.core#GameObject object -- @return #boolean +--- +-- Get this item's current enchantment charge. +-- @function [parent=#Item] getEnchantmentCharge +-- @param #Item item +-- @return #number The charge remaining. -1 if the enchantment has never been used, implying the charge is full. Unenchanted items will always return a value of -1. +--- +-- Set this item's enchantment charge. +-- @function [parent=#Item] setEnchantmentCharge +-- @param #Item item +-- @param #number charge --- @{#Creature} functions -- @field [parent=#types] #Creature Creature