diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ccb9c584a..1755b096b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Bug #6645: Enemy block sounds align with animation instead of blocked hits Bug #6661: Saved games that have no preview screenshot cause issues or crashes Bug #6807: Ultimate Galleon is not working properly + Bug #6893: Lua: Inconsistent behavior with actors affected by Disable and SetDelete commands Bug #6939: OpenMW-CS: ID columns are too short Bug #6949: Sun Damage effect doesn't work in quasi exteriors Bug #6964: Nerasa Dralor Won't Follow @@ -34,6 +35,7 @@ Bug #7088: Deleting last save game of last character doesn't clear character name/details Feature #5492: Let rain and snow collide with statics Feature #6447: Add LOD support to Object Paging + Feature #6726: Lua API for creating new objects Feature #6922: Improve launcher appearance Feature #6933: Support high-resolution cursor textures Feature #6945: Support S3TC-compressed and BGR/BGRA NiPixelData diff --git a/apps/openmw/mwclass/misc.cpp b/apps/openmw/mwclass/misc.cpp index 3b4c71fb42..76bb7f2c29 100644 --- a/apps/openmw/mwclass/misc.cpp +++ b/apps/openmw/mwclass/misc.cpp @@ -17,6 +17,7 @@ #include "../mwworld/esmstore.hpp" #include "../mwworld/manualref.hpp" #include "../mwworld/nullaction.hpp" +#include "../mwworld/worldmodel.hpp" #include "../mwgui/tooltips.hpp" #include "../mwgui/ustring.hpp" @@ -208,6 +209,7 @@ namespace MWClass newPtr.getRefData().setCount(count); } newPtr.getCellRef().unsetRefNum(); + MWBase::Environment::get().getWorldModel()->registerPtr(newPtr); return newPtr; } diff --git a/apps/openmw/mwlua/cellbindings.cpp b/apps/openmw/mwlua/cellbindings.cpp index bb98a45b46..931142d371 100644 --- a/apps/openmw/mwlua/cellbindings.cpp +++ b/apps/openmw/mwlua/cellbindings.cpp @@ -75,6 +75,8 @@ namespace MWLua const CellT& cell, sol::optional type) { ObjectIdList res = std::make_shared>(); auto visitor = [&](const MWWorld::Ptr& ptr) { + if (ptr.getRefData().isDeleted()) + return true; MWBase::Environment::get().getWorldModel()->registerPtr(ptr); if (getLiveCellRefType(ptr.mRef) == ptr.getType()) res->push_back(getId(ptr)); diff --git a/apps/openmw/mwlua/localscripts.hpp b/apps/openmw/mwlua/localscripts.hpp index c76c1be51a..26c8668b5a 100644 --- a/apps/openmw/mwlua/localscripts.hpp +++ b/apps/openmw/mwlua/localscripts.hpp @@ -23,6 +23,7 @@ namespace MWLua LocalScripts(LuaUtil::LuaState* lua, const LObject& obj); MWBase::LuaManager::ActorControls* getActorControls() { return &mData.mControls; } + const MWWorld::Ptr& getPtr() const { return mData.ptr(); } struct SelfObject : public LObject { diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index 9d59fe9705..24b47481b0 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -7,7 +7,10 @@ #include "../mwbase/environment.hpp" #include "../mwbase/statemanager.hpp" +#include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" +#include "../mwworld/manualref.hpp" +#include "../mwworld/scene.hpp" #include "../mwworld/store.hpp" #include "eventqueue.hpp" @@ -48,7 +51,7 @@ namespace MWLua { auto* lua = context.mLua; sol::table api(lua->sol(), sol::create); - api["API_REVISION"] = 32; + api["API_REVISION"] = 33; api["quit"] = [lua]() { Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback(); MWBase::Environment::get().getStateManager()->requestQuit(); @@ -83,7 +86,17 @@ namespace MWLua api["getExteriorCell"] = [](int x, int y) { return GCell{ MWBase::Environment::get().getWorldModel()->getExterior(x, y) }; }; api["activeActors"] = GObjectList{ worldView->getActorsInScene() }; - // TODO: add world.placeNewObject(recordId, cell, pos, [rot]) + api["createObject"] = [](std::string_view recordId, sol::optional count) -> GObject { + // Doesn't matter which cell to use because the new object will be in disabled state. + MWWorld::CellStore* cell = MWBase::Environment::get().getWorldScene()->getCurrentCell(); + + MWWorld::ManualRef mref( + MWBase::Environment::get().getWorld()->getStore(), ESM::RefId::stringRefId(recordId)); + const MWWorld::Ptr& ptr = mref.getPtr(); + ptr.getRefData().disable(); + MWWorld::Ptr newPtr = ptr.getClass().copyToCell(ptr, *cell, count.value_or(1)); + return GObject(getId(newPtr)); + }; return LuaUtil::makeReadOnly(api); } diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 129b9161a7..2dfb5dbc35 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -154,6 +154,9 @@ namespace MWLua mWorldView.update(); + std::erase_if(mActiveLocalScripts, + [](const LocalScripts* l) { return l->getPtr().isEmpty() || l->getPtr().getRefData().isDeleted(); }); + mGlobalScripts.statsNextFrame(); for (LocalScripts* scripts : mActiveLocalScripts) scripts->statsNextFrame(); diff --git a/apps/openmw/mwlua/object.cpp b/apps/openmw/mwlua/object.cpp index 446bf4443f..ed1a9dd9ec 100644 --- a/apps/openmw/mwlua/object.cpp +++ b/apps/openmw/mwlua/object.cpp @@ -20,6 +20,8 @@ namespace MWLua std::string ptrToString(const MWWorld::Ptr& ptr) { std::string res = "object"; + if (ptr.getRefData().isDeleted()) + res = "deleted object"; res.append(idToString(getId(ptr))); res.append(" ("); res.append(getLuaObjectTypeName(ptr)); diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index 8be7c6ff3f..5b67b9f718 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -7,6 +7,7 @@ #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/player.hpp" +#include "../mwworld/scene.hpp" #include "../mwmechanics/creaturestats.hpp" @@ -87,6 +88,8 @@ namespace MWLua { MWWorld::Ptr newObj = world->moveObject(obj, cell, mPos); world->rotateObject(newObj, mRot); + if (!newObj.getRefData().isEnabled()) + world->enable(newObj); } } @@ -198,6 +201,32 @@ namespace MWLua context.mLuaManager->addAction(std::make_unique(context.mLua, o.id(), actor.id())); }; + 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["addScript"] = [context](const GObject& object, std::string_view path, sol::object initData) { @@ -243,11 +272,74 @@ namespace MWLua localScripts->removeScript(*scriptId); }; - objectT["teleport"] = [context](const GObject& object, std::string_view cell, const osg::Vec3f& pos, - const sol::optional& optRot) { + auto removeFn = [context](const GObject& object, int countToRemove) { MWWorld::Ptr ptr = object.ptr(); + 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) + { + // Delayed action to trigger side effects + context.mLuaManager->addAction([object, countToRemove] { + MWWorld::Ptr ptr = object.ptr(); + // Restore 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(object.ptr()); + MWBase::Environment::get().getWorld()->deleteObject(ptr); + } + }); + } + }; + objectT["remove"] = [removeFn](const GObject& object, sol::optional count) { + removeFn(object, count.value_or(object.ptr().getRefData().getCount())); + }; + objectT["split"] = [removeFn](const GObject& object, int count) -> GObject { + removeFn(object, count); + + // 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(); + return GObject(getId(splitted)); + }; + objectT["moveInto"] = [removeFn, context](const GObject& object, const Inventory& inventory) { + // Currently moving to or from containers makes a copy and removes the original. + // TODO(#6148): actually move rather than copy and preserve RefNum + int count = object.ptr().getRefData().getCount(); + removeFn(object, count); + context.mLuaManager->addAction([item = object, count, cont = inventory.mObj] { + auto& refData = item.ptr().getRefData(); + refData.setCount(count); // temporarily undo removal to run ContainerStore::add + cont.ptr().getClass().getContainerStore(cont.ptr()).add(item.ptr(), count, false); + refData.setCount(0); + }); + }; + objectT["teleport"] = [removeFn, context](const GObject& object, std::string_view cell, + const osg::Vec3f& pos, const sol::optional& optRot) { + MWWorld::Ptr ptr = object.ptr(); + if (ptr.getRefData().isDeleted()) + throw std::runtime_error("Object is removed"); + if (ptr.getContainerStore()) + { + // Currently moving to or from containers makes a copy and removes the original. + // TODO(#6148): actually move rather than copy and preserve RefNum + auto* cellStore = MWBase::Environment::get().getWorldModel()->getCellByPosition(pos, cell); + MWWorld::Ptr newPtr = ptr.getClass().copyToCell(ptr, *cellStore, ptr.getRefData().getCount()); + newPtr.getRefData().disable(); + removeFn(object, ptr.getRefData().getCount()); + ptr = newPtr; + } osg::Vec3f rot = optRot ? *optRot : ptr.getRefData().getPosition().asRotationVec3(); - auto action = std::make_unique(context.mLua, object.id(), cell, pos, rot); + auto action = std::make_unique(context.mLua, getId(ptr), cell, pos, rot); if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) context.mLuaManager->addTeleportPlayerAction(std::move(action)); else @@ -335,24 +427,40 @@ namespace MWLua return ObjectList{ list }; }; - inventoryT["countOf"] = [](const InventoryT& inventory, const std::string& recordId) { + 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)); }; - - if constexpr (std::is_same_v) - { // Only for global scripts - // 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["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 diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index 6ce251c8e4..ee8f90ef9f 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -267,8 +267,7 @@ namespace iter->mData.enable(); MWBase::Environment::get().getWorld()->disable(ptr); } - else - MWBase::Environment::get().getWorldModel()->registerPtr(ptr); + MWBase::Environment::get().getWorldModel()->registerPtr(ptr); return; } diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index a357c1c596..f71198cf12 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -18,6 +18,7 @@ #include "inventorystore.hpp" #include "nullaction.hpp" #include "ptr.hpp" +#include "worldmodel.hpp" #include "../mwgui/tooltips.hpp" @@ -373,6 +374,7 @@ namespace MWWorld Ptr newPtr = copyToCellImpl(ptr, cell); newPtr.getCellRef().unsetRefNum(); // This RefNum is only valid within the original cell of the reference newPtr.getRefData().setCount(count); + MWBase::Environment::get().getWorldModel()->registerPtr(newPtr); if (hasInventoryStore(newPtr)) getInventoryStore(newPtr).setActor(newPtr); return newPtr; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 02a0c87f74..0e415739d9 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -788,8 +788,6 @@ namespace MWWorld void World::enable(const Ptr& reference) { - MWBase::Environment::get().getWorldModel()->registerPtr(reference); - if (!reference.isInCell()) return; @@ -840,7 +838,6 @@ namespace MWWorld if (reference == getPlayerPtr()) throw std::runtime_error("can not disable player object"); - MWBase::Environment::get().getWorldModel()->deregisterPtr(reference); reference.getRefData().disable(); if (reference.getCellRef().getRefNum().hasContentFile()) diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index 659ae9a6e3..c04dcbd2bc 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -80,7 +80,6 @@ -- @usage -- # DataFiles/l10n/MyMod/en.yaml -- good_morning: 'Good morning.' --- -- you_have_arrows: |- -- {count, plural, -- one {You have one arrow.} @@ -107,11 +106,12 @@ -- Any object that exists in the game world and has a specific location. -- Player, actors, items, and statics are game objects. -- @type GameObject +-- @field #boolean enabled Whether the object is enabled or disabled. Global scripts can set the value. Items in containers or inventories can't be disabled. -- @field openmw.util#Vector3 position Object position. -- @field openmw.util#Vector3 rotation Object rotation (ZXY order). -- @field #Cell cell The cell where the object currently is. During loading a game and for objects in an inventory or a container `cell` is nil. -- @field #table type Type of the object (one of the tables from the package @{openmw.types#types}). --- @field #number count Count (makes sense if stored in a container). +-- @field #number count Count (>1 means a stack of objects). -- @field #string recordId Returns record ID of the object in lowercase. --- @@ -163,13 +163,39 @@ --- -- Moves object to given cell and position. +-- Can be called only from a global script. -- The effect is not immediate: the position will be updated only in the next --- frame. Can be called only from a global script. +-- frame. Can be called only from a global script. Enables object if it was disabled. +-- Can be used to move objects from an inventory or a container to the world. -- @function [parent=#GameObject] teleport -- @param self -- @param #string cellName Name of the cell to teleport into. For exteriors can be empty. -- @param openmw.util#Vector3 position New position --- @param openmw.util#Vector3 rotation New rotation. Optional argument. If missed, then the current rotation is used. +-- @param openmw.util#Vector3 rotation New rotation. Optional argument. If missing, then the current rotation is used. + +--- +-- Moves object into a container or an inventory. Enables if was disabled. +-- Can be called only from a global script. +-- @function [parent=#GameObject] moveInto +-- @param self +-- @param #Inventory dest +-- @usage item:moveInto(types.Actor.inventory(actor)) + +--- +-- Removes an object or reduces a stack of objects. +-- Can be called only from a global script. +-- @function [parent=#GameObject] remove +-- @param self +-- @param #number count (optional) the number of items to remove (if not specified then the whole stack) + +--- +-- Splits a stack of items. Original stack is reduced by `count`. Returns a new stack with `count` items. +-- Can be called only from a global script. +-- @function [parent=#GameObject] split +-- @param self +-- @param #number count The number of items to return. +-- @usage -- take 50 coins from `money` and put to the container `cont` +-- money:split(50):moveInto(types.Container.content(cont)) --- @@ -246,6 +272,22 @@ -- local all = playerInventory:getAll() -- local weapons = playerInventory:getAll(types.Weapon) +--- +-- Get first item with given recordId from the inventory. Returns nil if not found. +-- @function [parent=#Inventory] find +-- @param self +-- @param #string recordId +-- @return #GameObject +-- @usage inventory:find('gold_001') + +--- +-- Get all items with given recordId from the inventory. +-- @function [parent=#Inventory] findAll +-- @param self +-- @param #string recordId +-- @return #ObjectList +-- @usage for _, item in ipairs(inventory:findAll('common_shirt_01')) do ... end + return nil diff --git a/files/lua_api/openmw/world.lua b/files/lua_api/openmw/world.lua index 4fff22bc41..96d19dc64c 100644 --- a/files/lua_api/openmw/world.lua +++ b/files/lua_api/openmw/world.lua @@ -59,5 +59,19 @@ -- @function [parent=#world] isWorldPaused -- @return #boolean +--- +-- Create a new instance of the given record. +-- After creation the object is in the disabled state. Use :teleport to place to the world or :moveInto to put it into a container or an inventory. +-- @function [parent=#world] createObject +-- @param #string recordId Record ID in lowercase +-- @param #number count (optional, 1 by default) The number of objects in stack +-- @return openmw.core#GameObject +-- @usage -- put 100 gold on the ground at the position of `actor` +-- money = world.createObject('gold_001', 100) +-- money:teleport(actor.cell.name, actor.position) +-- @usage -- put 50 gold into the actor's inventory +-- money = world.createObject('gold_001', 50) +-- money:moveInto(types.Actor.inventory(actor)) + return nil