#include "objectbindings.hpp" #include #include #include #include #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/player.hpp" #include "../mwworld/scene.hpp" #include "../mwrender/renderingmanager.hpp" #include "../mwrender/vismask.hpp" #include "../mwmechanics/creaturestats.hpp" #include "luaevents.hpp" #include "luamanagerimp.hpp" #include "types/types.hpp" 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 { namespace { MWWorld::CellStore* findCell(const sol::object& cellOrName, const osg::Vec3f& pos) { MWWorld::WorldModel* wm = MWBase::Environment::get().getWorldModel(); MWWorld::CellStore* cell; if (cellOrName.is()) cell = cellOrName.as().mStore; else { std::string_view name = LuaUtil::cast(cellOrName); if (name.empty()) cell = nullptr; // default exterior worldspace else cell = &wm->getCell(name); } if (cell != nullptr && !cell->isExterior()) return cell; const ESM::RefId worldspace = cell == nullptr ? ESM::Cell::sDefaultWorldspaceId : cell->getCell()->getWorldSpace(); return &wm->getExterior(ESM::positionToExteriorCellLocation(pos.x(), pos.y(), worldspace)); } void teleportPlayer( MWWorld::CellStore* destCell, const osg::Vec3f& pos, const osg::Vec3f& rot, bool placeOnGround) { MWBase::World* world = MWBase::Environment::get().getWorld(); ESM::Position esmPos; static_assert(sizeof(esmPos) == sizeof(osg::Vec3f) * 2); std::memcpy(esmPos.pos, &pos, sizeof(osg::Vec3f)); std::memcpy(esmPos.rot, &rot, sizeof(osg::Vec3f)); MWWorld::Ptr ptr = world->getPlayerPtr(); auto& stats = ptr.getClass().getCreatureStats(ptr); stats.land(true); stats.setTeleported(true); world->getPlayer().setTeleported(true); world->changeToCell(destCell->getCell()->getId(), esmPos, false); MWWorld::Ptr newPtr = world->getPlayerPtr(); world->moveObject(newPtr, pos); world->rotateObject(newPtr, rot); if (placeOnGround) world->adjustPosition(newPtr, true); } void teleportNotPlayer(const MWWorld::Ptr& ptr, MWWorld::CellStore* destCell, const osg::Vec3f& pos, const osg::Vec3f& rot, bool placeOnGround) { MWBase::World* world = MWBase::Environment::get().getWorld(); const MWWorld::Class& cls = ptr.getClass(); if (cls.isActor()) { auto& stats = ptr.getClass().getCreatureStats(ptr); stats.land(false); stats.setTeleported(true); } MWWorld::Ptr newPtr = world->moveObject(ptr, destCell, pos); world->rotateObject(newPtr, rot, MWBase::RotationFlag_none); if (placeOnGround) world->adjustPosition(newPtr, true); if (cls.isDoor()) { // Change "original position and rotation" because without it teleported animated doors don't work // properly. newPtr.getCellRef().setPosition(newPtr.getRefData().getPosition()); } if (!newPtr.getRefData().isEnabled()) world->enable(newPtr); } template using Cell = std::conditional_t, LCell, GCell>; template void registerObjectList(const std::string& prefix, const Context& context) { using ListT = ObjectList; sol::state_view& lua = context.mLua->sol(); sol::usertype listT = lua.new_usertype(prefix + "ObjectList"); listT[sol::meta_function::to_string] = [](const ListT& list) { return "{" + std::to_string(list.mIds->size()) + " objects}"; }; listT[sol::meta_function::length] = [](const ListT& list) { return list.mIds->size(); }; listT[sol::meta_function::index] = [](const ListT& list, size_t index) { if (index > 0 && index <= list.mIds->size()) return ObjectT((*list.mIds)[index - 1]); else throw std::runtime_error("Index out of range"); }; listT[sol::meta_function::pairs] = lua["ipairsForArray"].template get(); listT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); } template void addBasicBindings(sol::usertype& objectT, const Context& context) { objectT["id"] = sol::readonly_property([](const ObjectT& o) -> std::string { return o.id().toString(); }); objectT["contentFile"] = sol::readonly_property([](const ObjectT& o) -> sol::optional { int contentFileIndex = o.id().mContentFile; const std::vector& contentList = MWBase::Environment::get().getWorld()->getContentFiles(); if (contentFileIndex < 0 || contentFileIndex >= static_cast(contentList.size())) return sol::nullopt; return Misc::StringUtils::lowerCase(contentList[contentFileIndex]); }); objectT["isValid"] = [](const ObjectT& o) { return !o.ptrOrEmpty().isEmpty(); }; objectT["recordId"] = sol::readonly_property( [](const ObjectT& o) -> std::string { return o.ptr().getCellRef().getRefId().serializeText(); }); objectT["cell"] = sol::readonly_property([](const ObjectT& o) -> sol::optional> { const MWWorld::Ptr& ptr = o.ptr(); if (ptr.isInCell()) return Cell{ ptr.getCell() }; else return sol::nullopt; }); objectT["position"] = sol::readonly_property( [](const ObjectT& o) -> osg::Vec3f { return o.ptr().getRefData().getPosition().asVec3(); }); objectT["scale"] = sol::readonly_property([](const ObjectT& o) -> float { return o.ptr().getCellRef().getScale(); }); objectT["rotation"] = sol::readonly_property( [](const ObjectT& o) -> osg::Vec3f { return o.ptr().getRefData().getPosition().asRotationVec3(); }); objectT["startingPosition"] = sol::readonly_property( [](const ObjectT& o) -> osg::Vec3f { return o.ptr().getCellRef().getPosition().asVec3(); }); objectT["startingRotation"] = sol::readonly_property( [](const ObjectT& o) -> osg::Vec3f { return o.ptr().getCellRef().getPosition().asRotationVec3(); }); objectT["getBoundingBox"] = [](const ObjectT& o) { MWRender::RenderingManager* renderingManager = MWBase::Environment::get().getWorld()->getRenderingManager(); osg::BoundingBox bb = renderingManager->getCullSafeBoundingBox(o.ptr()); return LuaUtil::Box{ bb.center(), bb._max - bb.center() }; }; objectT["type"] = sol::readonly_property( [types = getTypeToPackageTable(context.mLua->sol())]( const ObjectT& o) mutable { return types[getLiveCellRefType(o.ptr().mRef)]; }); objectT["count"] = sol::readonly_property([](const ObjectT& o) { return o.ptr().getRefData().getCount(); }); objectT[sol::meta_function::equal_to] = [](const ObjectT& a, const ObjectT& b) { return a.id() == b.id(); }; objectT[sol::meta_function::to_string] = &ObjectT::toString; objectT["sendEvent"] = [context](const ObjectT& dest, std::string eventName, const sol::object& eventData) { context.mLuaEvents->addLocalEvent( { dest.id(), std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer) }); }; auto getOwnerRecordId = [](const ObjectT& o) -> sol::optional { ESM::RefId owner = o.ptr().getCellRef().getOwner(); if (owner.empty()) return sol::nullopt; else return owner.serializeText(); }; auto setOwnerRecordId = [](const ObjectT& obj, sol::optional ownerId) { if (std::is_same_v && !dynamic_cast(&obj)) throw std::runtime_error("Local scripts can set an owner only on self"); const MWWorld::Ptr& ptr = obj.ptr(); if (!ownerId) { ptr.getCellRef().setOwner(ESM::RefId()); return; } ESM::RefId owner = ESM::RefId::deserializeText(*ownerId); const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); if (!store.get().search(owner)) throw std::runtime_error("Invalid owner record id"); ptr.getCellRef().setOwner(owner); }; objectT["ownerRecordId"] = sol::property(getOwnerRecordId, setOwnerRecordId); auto getOwnerFactionId = [](const ObjectT& o) -> sol::optional { ESM::RefId owner = o.ptr().getCellRef().getFaction(); if (owner.empty()) return sol::nullopt; else return owner.serializeText(); }; auto setOwnerFactionId = [](const ObjectT& object, sol::optional ownerId) { ESM::RefId ownerFac; if (std::is_same_v && !dynamic_cast(&object)) throw std::runtime_error("Local scripts can set an owner faction only on self"); if (!ownerId) { object.ptr().getCellRef().setFaction(ESM::RefId()); return; } ownerFac = ESM::RefId::deserializeText(*ownerId); const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); if (!store.get().search(ownerFac)) throw std::runtime_error("Invalid owner faction id"); object.ptr().getCellRef().setFaction(ownerFac); }; objectT["ownerFactionId"] = sol::property(getOwnerFactionId, setOwnerFactionId); auto getOwnerFactionRank = [](const ObjectT& o) -> int { return o.ptr().getCellRef().getFactionRank(); }; auto setOwnerFactionRank = [](const ObjectT& object, int factionRank) { if (std::is_same_v && !dynamic_cast(&object)) throw std::runtime_error("Local scripts can set an owner faction rank only on self"); object.ptr().getCellRef().setFactionRank(factionRank); }; objectT["ownerFactionRank"] = sol::property(getOwnerFactionRank, setOwnerFactionRank); objectT["activateBy"] = [](const ObjectT& object, const ObjectT& actor) { const MWWorld::Ptr& objPtr = object.ptr(); const MWWorld::Ptr& actorPtr = actor.ptr(); uint32_t esmRecordType = actorPtr.getType(); if (esmRecordType != ESM::REC_CREA && esmRecordType != ESM::REC_NPC_) throw std::runtime_error( "The argument of `activateBy` must be an actor who activates the object. Got: " + actor.toString()); if (objPtr.getRefData().activate()) MWBase::Environment::get().getLuaManager()->objectActivated(objPtr, actorPtr); }; auto isEnabled = [](const ObjectT& o) { return o.ptr().getRefData().isEnabled(); }; auto setEnabled = [context](const GObject& object, bool enable) { if (enable && object.ptr().getRefData().isDeleted()) throw std::runtime_error("Object is removed"); context.mLuaManager->addAction([object, enable] { if (object.ptr().isInCell()) { if (enable) MWBase::Environment::get().getWorld()->enable(object.ptr()); else MWBase::Environment::get().getWorld()->disable(object.ptr()); } else { if (enable) object.ptr().getRefData().enable(); else throw std::runtime_error("Objects in containers can't be disabled"); } }); }; if constexpr (std::is_same_v) objectT["enabled"] = sol::property(isEnabled, setEnabled); else objectT["enabled"] = sol::readonly_property(isEnabled); if constexpr (std::is_same_v) { // Only for global scripts objectT["setScale"] = [context](const GObject& object, float scale) { context.mLuaManager->addAction( [object, scale] { MWBase::Environment::get().getWorld()->scaleObject(object.ptr(), scale); }); }; objectT["addScript"] = [context](const GObject& object, std::string_view path, sol::object initData) { const LuaUtil::ScriptsConfiguration& cfg = context.mLua->getConfiguration(); std::optional scriptId = cfg.findId(path); if (!scriptId) throw std::runtime_error("Unknown script: " + std::string(path)); if (!(cfg[*scriptId].mFlags & ESM::LuaScriptCfg::sCustom)) throw std::runtime_error( "Script without CUSTOM tag can not be added dynamically: " + std::string(path)); if (object.ptr().getType() == ESM::REC_STAT) throw std::runtime_error("Attaching scripts to Static is not allowed: " + std::string(path)); if (initData != sol::nil) context.mLuaManager->addCustomLocalScript(object.ptr(), *scriptId, LuaUtil::serialize(LuaUtil::cast(initData), context.mSerializer)); else context.mLuaManager->addCustomLocalScript( object.ptr(), *scriptId, cfg[*scriptId].mInitializationData); }; objectT["hasScript"] = [lua = context.mLua](const GObject& object, std::string_view path) { const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); std::optional scriptId = cfg.findId(path); if (!scriptId) return false; MWWorld::Ptr ptr = object.ptr(); LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); if (localScripts) return localScripts->hasScript(*scriptId); else return false; }; objectT["removeScript"] = [lua = context.mLua](const GObject& object, std::string_view path) { const LuaUtil::ScriptsConfiguration& cfg = lua->getConfiguration(); std::optional scriptId = cfg.findId(path); if (!scriptId) throw std::runtime_error("Unknown script: " + std::string(path)); MWWorld::Ptr ptr = object.ptr(); LocalScripts* localScripts = ptr.getRefData().getLuaScripts(); if (!localScripts || !localScripts->hasScript(*scriptId)) throw std::runtime_error("There is no script " + std::string(path) + " on " + ptr.toString()); if (localScripts->getAutoStartConf().count(*scriptId) > 0) throw std::runtime_error("Autostarted script can not be removed: " + std::string(path)); localScripts->removeScript(*scriptId); }; using DelayedRemovalFn = std::function; auto removeFn = [](const MWWorld::Ptr ptr, int countToRemove) -> std::optional { int currentCount = ptr.getRefData().getCount(); if (countToRemove <= 0 || countToRemove > currentCount) throw std::runtime_error("Can't remove " + std::to_string(countToRemove) + " of " + std::to_string(currentCount) + " items"); ptr.getRefData().setCount(currentCount - countToRemove); // Immediately change count if (!ptr.getContainerStore() && currentCount > countToRemove) return std::nullopt; // Delayed action to trigger side effects return [countToRemove](MWWorld::Ptr ptr) { // Restore the original count ptr.getRefData().setCount(ptr.getRefData().getCount() + countToRemove); // And now remove properly if (ptr.getContainerStore()) ptr.getContainerStore()->remove(ptr, countToRemove); else { MWBase::Environment::get().getWorld()->disable(ptr); MWBase::Environment::get().getWorld()->deleteObject(ptr); } }; }; objectT["remove"] = [removeFn, context](const GObject& object, sol::optional count) { std::optional delayed = removeFn(object.ptr(), count.value_or(object.ptr().getRefData().getCount())); if (delayed.has_value()) context.mLuaManager->addAction([fn = *delayed, object] { fn(object.ptr()); }); }; objectT["split"] = [removeFn, context](const GObject& object, int count) -> GObject { // Doesn't matter which cell to use because the new instance will be in disabled state. MWWorld::CellStore* cell = MWBase::Environment::get().getWorldScene()->getCurrentCell(); const MWWorld::Ptr& ptr = object.ptr(); MWWorld::Ptr splitted = ptr.getClass().copyToCell(ptr, *cell, count); splitted.getRefData().disable(); std::optional delayedRemovalFn = removeFn(ptr, count); if (delayedRemovalFn.has_value()) context.mLuaManager->addAction([fn = *delayedRemovalFn, object] { fn(object.ptr()); }); return GObject(splitted); }; objectT["moveInto"] = [removeFn, context](const GObject& object, const Inventory& inventory) { const MWWorld::Ptr& ptr = object.ptr(); int count = ptr.getRefData().getCount(); std::optional delayedRemovalFn = removeFn(ptr, count); context.mLuaManager->addAction([item = object, count, cont = inventory.mObj, delayedRemovalFn] { const MWWorld::Ptr& oldPtr = item.ptr(); auto& refData = oldPtr.getRefData(); refData.setCount(count); // temporarily undo removal to run ContainerStore::add refData.enable(); cont.ptr().getClass().getContainerStore(cont.ptr()).add(oldPtr, count, false); refData.setCount(0); if (delayedRemovalFn.has_value()) (*delayedRemovalFn)(oldPtr); }); }; objectT["teleport"] = [removeFn, context](const GObject& object, const sol::object& cellOrName, const osg::Vec3f& pos, const sol::object& options) { MWWorld::CellStore* cell = findCell(cellOrName, pos); MWWorld::Ptr ptr = object.ptr(); int count = ptr.getRefData().getCount(); if (count == 0) throw std::runtime_error("Object is either removed or already in the process of teleporting"); osg::Vec3f rot = ptr.getRefData().getPosition().asRotationVec3(); bool placeOnGround = false; if (options.is()) rot = options.as(); else if (options != sol::nil) { sol::table t = LuaUtil::cast(options); rot = LuaUtil::getValueOrDefault(t["rotation"], rot); placeOnGround = LuaUtil::getValueOrDefault(t["onGround"], placeOnGround); } if (ptr.getContainerStore()) { DelayedRemovalFn delayedRemovalFn = *removeFn(ptr, count); context.mLuaManager->addAction( [object, cell, pos, rot, count, delayedRemovalFn, placeOnGround] { MWWorld::Ptr oldPtr = object.ptr(); oldPtr.getRefData().setCount(count); MWWorld::Ptr newPtr = oldPtr.getClass().moveToCell(oldPtr, *cell); oldPtr.getRefData().setCount(0); newPtr.getRefData().disable(); teleportNotPlayer(newPtr, cell, pos, rot, placeOnGround); delayedRemovalFn(oldPtr); }, "TeleportFromContainerAction"); } else if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) context.mLuaManager->addTeleportPlayerAction( [cell, pos, rot, placeOnGround] { teleportPlayer(cell, pos, rot, placeOnGround); }); else { ptr.getRefData().setCount(0); context.mLuaManager->addAction( [object, cell, pos, rot, count, placeOnGround] { object.ptr().getRefData().setCount(count); teleportNotPlayer(object.ptr(), cell, pos, rot, placeOnGround); }, "TeleportAction"); } }; } } template void addInventoryBindings(sol::usertype& objectT, const std::string& prefix, const Context& context) { using InventoryT = Inventory; sol::usertype inventoryT = context.mLua->sol().new_usertype(prefix + "Inventory"); inventoryT[sol::meta_function::to_string] = [](const InventoryT& inv) { return "Inventory[" + inv.mObj.toString() + "]"; }; inventoryT["getAll"] = [ids = getPackageToTypeTable(context.mLua->sol())]( const InventoryT& inventory, sol::optional type) { int mask = -1; sol::optional typeId = sol::nullopt; if (type.has_value()) typeId = ids[*type]; else mask = MWWorld::ContainerStore::Type_All; if (typeId.has_value()) { switch (*typeId) { case ESM::REC_ALCH: mask = MWWorld::ContainerStore::Type_Potion; break; case ESM::REC_ARMO: mask = MWWorld::ContainerStore::Type_Armor; break; case ESM::REC_BOOK: mask = MWWorld::ContainerStore::Type_Book; break; case ESM::REC_CLOT: mask = MWWorld::ContainerStore::Type_Clothing; break; case ESM::REC_INGR: mask = MWWorld::ContainerStore::Type_Ingredient; break; case ESM::REC_LIGH: mask = MWWorld::ContainerStore::Type_Light; break; case ESM::REC_MISC: mask = MWWorld::ContainerStore::Type_Miscellaneous; break; case ESM::REC_WEAP: mask = MWWorld::ContainerStore::Type_Weapon; break; case ESM::REC_APPA: mask = MWWorld::ContainerStore::Type_Apparatus; break; case ESM::REC_LOCK: mask = MWWorld::ContainerStore::Type_Lockpick; break; case ESM::REC_PROB: mask = MWWorld::ContainerStore::Type_Probe; break; case ESM::REC_REPA: mask = MWWorld::ContainerStore::Type_Repair; break; default:; } } if (mask == -1) throw std::runtime_error( std::string("Incorrect type argument in inventory:getAll: " + LuaUtil::toString(*type))); const MWWorld::Ptr& ptr = inventory.mObj.ptr(); MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); ObjectIdList list = std::make_shared>(); auto it = store.begin(mask); while (it.getType() != -1) { const MWWorld::Ptr& item = *(it++); MWBase::Environment::get().getWorldModel()->registerPtr(item); list->push_back(getId(item)); } return ObjectList{ list }; }; inventoryT["countOf"] = [](const InventoryT& inventory, std::string_view recordId) { const MWWorld::Ptr& ptr = inventory.mObj.ptr(); MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); return store.count(ESM::RefId::stringRefId(recordId)); }; inventoryT["find"] = [](const InventoryT& inventory, std::string_view recordId) -> sol::optional { const MWWorld::Ptr& ptr = inventory.mObj.ptr(); MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); auto itemId = ESM::RefId::stringRefId(recordId); for (const MWWorld::Ptr& item : store) { if (item.getCellRef().getRefId() == itemId) { MWBase::Environment::get().getWorldModel()->registerPtr(item); return ObjectT(getId(item)); } } return sol::nullopt; }; inventoryT["findAll"] = [](const InventoryT& inventory, std::string_view recordId) { const MWWorld::Ptr& ptr = inventory.mObj.ptr(); MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); auto itemId = ESM::RefId::stringRefId(recordId); ObjectIdList list = std::make_shared>(); for (const MWWorld::Ptr& item : store) { if (item.getCellRef().getRefId() == itemId) { MWBase::Environment::get().getWorldModel()->registerPtr(item); list->push_back(getId(item)); } } return ObjectList{ list }; }; } template void initObjectBindings(const std::string& prefix, const Context& context) { sol::usertype objectT = context.mLua->sol().new_usertype(prefix + "Object", sol::base_classes, sol::bases()); addBasicBindings(objectT, context); addInventoryBindings(objectT, prefix, context); registerObjectList(prefix, context); } } // namespace void initObjectBindingsForLocalScripts(const Context& context) { initObjectBindings("L", context); } void initObjectBindingsForGlobalScripts(const Context& context) { initObjectBindings("G", context); } }