diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index de55c36475..0cf609652b 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -56,8 +56,8 @@ add_openmw_dir (mwscript ) add_openmw_dir (mwlua - luamanagerimp localscripts object worldview userdataserializer eventqueue query - luabindings objectbindings asyncbindings camerabindings uibindings + luamanagerimp actions object worldview userdataserializer eventqueue query + luabindings localscripts objectbindings asyncbindings camerabindings uibindings ) add_openmw_dir (mwsound diff --git a/apps/openmw/mwlua/actions.cpp b/apps/openmw/mwlua/actions.cpp new file mode 100644 index 0000000000..bc9cdc3aad --- /dev/null +++ b/apps/openmw/mwlua/actions.cpp @@ -0,0 +1,140 @@ +#include "actions.hpp" + +#include + +#include "../mwworld/class.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/player.hpp" + +namespace MWLua +{ + + void TeleportAction::apply(WorldView& worldView) const + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + bool exterior = mCell.empty() || world->getExterior(mCell); + MWWorld::CellStore* cell; + if (exterior) + { + int cellX, cellY; + world->positionToIndex(mPos.x(), mPos.y(), cellX, cellY); + cell = world->getExterior(cellX, cellY); + } + else + cell = world->getInterior(mCell); + if (!cell) + { + Log(Debug::Error) << "LuaManager::applyTeleport -> cell not found: '" << mCell << "'"; + return; + } + + MWWorld::Ptr obj = worldView.getObjectRegistry()->getPtr(mObject, false); + const MWWorld::Class& cls = obj.getClass(); + bool isPlayer = obj == world->getPlayerPtr(); + if (cls.isActor()) + cls.getCreatureStats(obj).land(isPlayer); + if (isPlayer) + { + ESM::Position esmPos; + static_assert(sizeof(esmPos) == sizeof(osg::Vec3f) * 2); + std::memcpy(esmPos.pos, &mPos, sizeof(osg::Vec3f)); + std::memcpy(esmPos.rot, &mRot, sizeof(osg::Vec3f)); + world->getPlayer().setTeleported(true); + if (exterior) + world->changeToExteriorCell(esmPos, true); + else + world->changeToInteriorCell(mCell, esmPos, true); + } + else + { + MWWorld::Ptr newObj = world->moveObject(obj, cell, mPos.x(), mPos.y(), mPos.z()); + world->rotateObject(newObj, mRot.x(), mRot.y(), mRot.z()); + worldView.getObjectRegistry()->registerPtr(newObj); + } + } + + void SetEquipmentAction::apply(WorldView& worldView) const + { + MWWorld::Ptr actor = worldView.getObjectRegistry()->getPtr(mActor, false); + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + std::array usedSlots; + std::fill(usedSlots.begin(), usedSlots.end(), false); + + constexpr int anySlot = -1; + auto tryEquipToSlot = [&](int slot, const Item& item) -> bool + { + auto old_it = slot != anySlot ? store.getSlot(slot) : store.end(); + MWWorld::Ptr itemPtr; + if (std::holds_alternative(item)) + { + itemPtr = worldView.getObjectRegistry()->getPtr(std::get(item), false); + 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" << idToString(std::get(item)) << " is not in inventory"; + return false; + } + } + else + { + const std::string& recordId = std::get(item); + if (old_it != store.end() && *old_it->getCellRef().getRefIdPtr() == 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 [allowedSlots, _] = itemPtr.getClass().getEquipmentSlots(itemPtr); + bool requestedSlotIsAllowed = false; + for (int allowedSlot : allowedSlots) + requestedSlotIsAllowed = requestedSlotIsAllowed || allowedSlot == slot; + if (!requestedSlotIsAllowed) + { + slot = anySlot; + for (int allowedSlot : allowedSlots) + if (!usedSlots[allowedSlot]) + { + slot = allowedSlot; + break; + } + if (slot == anySlot) + { + Log(Debug::Warning) << "No suitable slot for " << ptrToString(itemPtr); + return 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"); + + store.equip(slot, it, actor); + return requestedSlotIsAllowed; // return true if equipped to requested slot and false if slot was changed + }; + + for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) + { + auto old_it = store.getSlot(slot); + auto new_it = mEquipment.find(slot); + if (new_it == mEquipment.end()) + { + if (old_it != store.end()) + store.unequipSlot(slot, actor); + continue; + } + if (tryEquipToSlot(slot, new_it->second)) + usedSlots[slot] = true; + } + for (auto [slot, item] : mEquipment) + if (slot >= MWWorld::InventoryStore::Slots) + tryEquipToSlot(anySlot, item); + } + +} diff --git a/apps/openmw/mwlua/actions.hpp b/apps/openmw/mwlua/actions.hpp new file mode 100644 index 0000000000..900b175320 --- /dev/null +++ b/apps/openmw/mwlua/actions.hpp @@ -0,0 +1,55 @@ +#ifndef MWLUA_ACTIONS_H +#define MWLUA_ACTIONS_H + +#include + +#include "object.hpp" +#include "worldview.hpp" + +namespace MWLua +{ + + // Some changes to the game world can not be done from the scripting thread (because it runs in parallel with OSG Cull), + // so we need to queue it and apply from the main thread. All such changes should be implemented as classes inherited + // from MWLua::Action. + + class Action + { + public: + virtual ~Action() {} + virtual void apply(WorldView&) const = 0; + }; + + class TeleportAction final : public Action + { + public: + TeleportAction(ObjectId object, std::string cell, const osg::Vec3f& pos, const osg::Vec3f& rot) + : mObject(object), mCell(std::move(cell)), mPos(pos), mRot(rot) {} + + void apply(WorldView&) const override; + + private: + ObjectId mObject; + std::string mCell; + osg::Vec3f mPos; + osg::Vec3f mRot; + }; + + class SetEquipmentAction final : public Action + { + public: + using Item = std::variant; // recordId or ObjectId + using Equipment = std::map; // slot to item + + SetEquipmentAction(ObjectId actor, Equipment equipment) : mActor(actor), mEquipment(std::move(equipment)) {} + + void apply(WorldView&) const override; + + private: + ObjectId mActor; + Equipment mEquipment; + }; + +} + +#endif // MWLUA_ACTIONS_H diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp index 5b65c3f984..473ffd3fef 100644 --- a/apps/openmw/mwlua/localscripts.cpp +++ b/apps/openmw/mwlua/localscripts.cpp @@ -5,6 +5,8 @@ #include "../mwmechanics/aisequence.hpp" #include "../mwmechanics/aicombat.hpp" +#include "luamanagerimp.hpp" + namespace sol { template <> @@ -32,9 +34,24 @@ namespace MWLua selfAPI["object"] = sol::readonly_property([](SelfObject& self) -> LObject { return LObject(self); }); selfAPI["controls"] = sol::readonly_property([](SelfObject& self) { return &self.mControls; }); selfAPI["setDirectControl"] = [](SelfObject& self, bool v) { self.mControls.controlledFromLua = v; }; - selfAPI["setEquipment"] = [](const GObject& obj, const sol::table& equipment) + selfAPI["setEquipment"] = [manager=context.mLuaManager](const SelfObject& obj, sol::table equipment) { - throw std::logic_error("Not implemented"); + if (!obj.ptr().getClass().hasInventoryStore(obj.ptr())) + { + if (!equipment.empty()) + throw std::runtime_error(ptrToString(obj.ptr()) + " has no equipment slots"); + return; + } + SetEquipmentAction::Equipment eqp; + for (auto& [key, value] : equipment) + { + int slot = key.as(); + if (value.is()) + eqp[slot] = value.as().id(); + else + eqp[slot] = value.as(); + } + manager->addAction(std::make_unique(obj.id(), std::move(eqp))); }; selfAPI["getCombatTarget"] = [worldView=context.mWorldView](SelfObject& self) -> sol::optional { diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 4e7dcf2b24..fa885c88f5 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -98,6 +98,17 @@ namespace MWLua void LuaManager::update(bool paused, float dt) { + if (!mPlayer.isEmpty()) + { + MWWorld::Ptr newPlayerPtr = MWBase::Environment::get().getWorld()->getPlayerPtr(); + if (!(getId(mPlayer) == getId(newPlayerPtr))) + throw std::logic_error("Player Refnum was changed unexpectedly"); + if (!mPlayer.isInCell() || !newPlayerPtr.isInCell() || mPlayer.getCell() != newPlayerPtr.getCell()) + { + mPlayer = newPlayerPtr; + mWorldView.getObjectRegistry()->registerPtr(mPlayer); + } + } mWorldView.update(); if (paused) @@ -162,6 +173,14 @@ namespace MWLua for (const std::string& message : mUIMessages) windowManager->messageBox(message); mUIMessages.clear(); + + for (std::unique_ptr& action : mActionQueue) + action->apply(mWorldView); + mActionQueue.clear(); + + if (mTeleportPlayerAction) + mTeleportPlayerAction->apply(mWorldView); + mTeleportPlayerAction.reset(); } void LuaManager::clear() @@ -314,4 +333,5 @@ namespace MWLua scripts->load(data, true); scripts->setSerializer(mLocalSerializer.get()); } + } diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index e8babc9695..d9934135ec 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -8,6 +8,7 @@ #include "../mwbase/luamanager.hpp" +#include "actions.hpp" #include "object.hpp" #include "eventqueue.hpp" #include "globalscripts.hpp" @@ -46,8 +47,10 @@ namespace MWLua void clear() override; // should be called before loading game or starting a new game to reset internal state. void setupPlayer(const MWWorld::Ptr& ptr) override; // Should be called once after each "clear". - // Used only in luabindings.cpp + // Used only in luabindings void addLocalScript(const MWWorld::Ptr&, const std::string& scriptPath); + void addAction(std::unique_ptr&& action) { mActionQueue.push_back(std::move(action)); } + void addTeleportPlayerAction(std::unique_ptr&& action) { mTeleportPlayerAction = std::move(action); } void addUIMessage(std::string_view message) { mUIMessages.emplace_back(message); } // Saving @@ -90,6 +93,8 @@ namespace MWLua std::vector mActorAddedEvents; // Queued actions that should be done in main thread. Processed by applyQueuedChanges(). + std::vector> mActionQueue; + std::unique_ptr mTeleportPlayerAction; std::vector mUIMessages; }; diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index 3684521ceb..ca6fbae31f 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -95,6 +95,11 @@ namespace MWLua { return o.ptr().getCellRef().getRefId(); }); + objectT["cell"] = sol::readonly_property([](const ObjectT& o) + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + return world->getCellName(o.ptr().getCell()); + }); objectT["position"] = sol::readonly_property([](const ObjectT& o) -> osg::Vec3f { return o.ptr().getRefData().getPosition().asVec3(); @@ -120,10 +125,15 @@ namespace MWLua }; objectT["teleport"] = [luaManager=context.mLuaManager](const GObject& object, std::string_view cell, - const osg::Vec3f& pos, const sol::optional& rot) + const osg::Vec3f& pos, const sol::optional& optRot) { - // TODO - throw std::logic_error("Not implemented"); + MWWorld::Ptr ptr = object.ptr(); + osg::Vec3f rot = optRot ? *optRot : ptr.getRefData().getPosition().asRotationVec3(); + auto action = std::make_unique(object.id(), std::string(cell), pos, rot); + if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) + luaManager->addTeleportPlayerAction(std::move(action)); + else + luaManager->addAction(std::move(action)); }; } } @@ -151,6 +161,20 @@ namespace MWLua }); } + static SetEquipmentAction::Equipment parseEquipmentTable(sol::table equipment) + { + SetEquipmentAction::Equipment eqp; + for (auto& [key, value] : equipment) + { + int slot = key.as(); + if (value.is()) + eqp[slot] = value.as().id(); + else + eqp[slot] = value.as(); + } + return eqp; + } + template static void addInventoryBindings(sol::usertype& objectT, const std::string& prefix, const Context& context) { @@ -160,8 +184,11 @@ namespace MWLua objectT["getEquipment"] = [context](const ObjectT& o) { const MWWorld::Ptr& ptr = o.ptr(); - MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); sol::table equipment(context.mLua->sol(), sol::create); + if (!ptr.getClass().hasInventoryStore(ptr)) + return equipment; + + MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { auto it = store.getSlot(slot); @@ -175,6 +202,8 @@ namespace MWLua objectT["isEquipped"] = [](const ObjectT& actor, const ObjectT& item) { const MWWorld::Ptr& ptr = actor.ptr(); + if (!ptr.getClass().hasInventoryStore(ptr)) + return false; MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); return store.isEquipped(item.ptr()); }; @@ -234,17 +263,26 @@ namespace MWLua if constexpr (std::is_same_v) { // Only for global scripts - // TODO - objectT["moveInto"] = [](const GObject& obj, const InventoryT& inventory) {}; - objectT["setEquipment"] = [](const GObject& obj, const sol::table& equipment) {}; + objectT["setEquipment"] = [manager=context.mLuaManager](const GObject& obj, sol::table equipment) + { + if (!obj.ptr().getClass().hasInventoryStore(obj.ptr())) + { + if (!equipment.empty()) + throw std::runtime_error(ptrToString(obj.ptr()) + " has no equipment slots"); + return; + } + manager->addAction(std::make_unique(obj.id(), parseEquipmentTable(equipment))); + }; + // TODO // obj.inventory:drop(obj2, [count]) // obj.inventory:drop(recordId, [count]) // obj.inventory:addNew(recordId, [count]) // obj.inventory:remove(obj/recordId, [count]) + /*objectT["moveInto"] = [](const GObject& obj, const InventoryT& inventory) {}; inventoryT["drop"] = [](const InventoryT& inventory) {}; inventoryT["addNew"] = [](const InventoryT& inventory) {}; - inventoryT["remove"] = [](const InventoryT& inventory) {}; + inventoryT["remove"] = [](const InventoryT& inventory) {};*/ } }