Lua commands to create/move/remove objects; consistent handling of disabled objects (#6726, #6893)

This commit is contained in:
Petr Mikheev 2023-01-16 01:28:21 +01:00
parent e7120f189b
commit c294898246
13 changed files with 215 additions and 28 deletions

View File

@ -16,6 +16,7 @@
Bug #6645: Enemy block sounds align with animation instead of blocked hits 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 #6661: Saved games that have no preview screenshot cause issues or crashes
Bug #6807: Ultimate Galleon is not working properly 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 #6939: OpenMW-CS: ID columns are too short
Bug #6949: Sun Damage effect doesn't work in quasi exteriors Bug #6949: Sun Damage effect doesn't work in quasi exteriors
Bug #6964: Nerasa Dralor Won't Follow 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 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 #5492: Let rain and snow collide with statics
Feature #6447: Add LOD support to Object Paging Feature #6447: Add LOD support to Object Paging
Feature #6726: Lua API for creating new objects
Feature #6922: Improve launcher appearance Feature #6922: Improve launcher appearance
Feature #6933: Support high-resolution cursor textures Feature #6933: Support high-resolution cursor textures
Feature #6945: Support S3TC-compressed and BGR/BGRA NiPixelData Feature #6945: Support S3TC-compressed and BGR/BGRA NiPixelData

View File

@ -17,6 +17,7 @@
#include "../mwworld/esmstore.hpp" #include "../mwworld/esmstore.hpp"
#include "../mwworld/manualref.hpp" #include "../mwworld/manualref.hpp"
#include "../mwworld/nullaction.hpp" #include "../mwworld/nullaction.hpp"
#include "../mwworld/worldmodel.hpp"
#include "../mwgui/tooltips.hpp" #include "../mwgui/tooltips.hpp"
#include "../mwgui/ustring.hpp" #include "../mwgui/ustring.hpp"
@ -208,6 +209,7 @@ namespace MWClass
newPtr.getRefData().setCount(count); newPtr.getRefData().setCount(count);
} }
newPtr.getCellRef().unsetRefNum(); newPtr.getCellRef().unsetRefNum();
MWBase::Environment::get().getWorldModel()->registerPtr(newPtr);
return newPtr; return newPtr;
} }

View File

@ -75,6 +75,8 @@ namespace MWLua
const CellT& cell, sol::optional<sol::table> type) { const CellT& cell, sol::optional<sol::table> type) {
ObjectIdList res = std::make_shared<std::vector<ObjectId>>(); ObjectIdList res = std::make_shared<std::vector<ObjectId>>();
auto visitor = [&](const MWWorld::Ptr& ptr) { auto visitor = [&](const MWWorld::Ptr& ptr) {
if (ptr.getRefData().isDeleted())
return true;
MWBase::Environment::get().getWorldModel()->registerPtr(ptr); MWBase::Environment::get().getWorldModel()->registerPtr(ptr);
if (getLiveCellRefType(ptr.mRef) == ptr.getType()) if (getLiveCellRefType(ptr.mRef) == ptr.getType())
res->push_back(getId(ptr)); res->push_back(getId(ptr));

View File

@ -23,6 +23,7 @@ namespace MWLua
LocalScripts(LuaUtil::LuaState* lua, const LObject& obj); LocalScripts(LuaUtil::LuaState* lua, const LObject& obj);
MWBase::LuaManager::ActorControls* getActorControls() { return &mData.mControls; } MWBase::LuaManager::ActorControls* getActorControls() { return &mData.mControls; }
const MWWorld::Ptr& getPtr() const { return mData.ptr(); }
struct SelfObject : public LObject struct SelfObject : public LObject
{ {

View File

@ -7,7 +7,10 @@
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/statemanager.hpp" #include "../mwbase/statemanager.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp" #include "../mwworld/esmstore.hpp"
#include "../mwworld/manualref.hpp"
#include "../mwworld/scene.hpp"
#include "../mwworld/store.hpp" #include "../mwworld/store.hpp"
#include "eventqueue.hpp" #include "eventqueue.hpp"
@ -48,7 +51,7 @@ namespace MWLua
{ {
auto* lua = context.mLua; auto* lua = context.mLua;
sol::table api(lua->sol(), sol::create); sol::table api(lua->sol(), sol::create);
api["API_REVISION"] = 32; api["API_REVISION"] = 33;
api["quit"] = [lua]() { api["quit"] = [lua]() {
Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback(); Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback();
MWBase::Environment::get().getStateManager()->requestQuit(); MWBase::Environment::get().getStateManager()->requestQuit();
@ -83,7 +86,17 @@ namespace MWLua
api["getExteriorCell"] api["getExteriorCell"]
= [](int x, int y) { return GCell{ MWBase::Environment::get().getWorldModel()->getExterior(x, y) }; }; = [](int x, int y) { return GCell{ MWBase::Environment::get().getWorldModel()->getExterior(x, y) }; };
api["activeActors"] = GObjectList{ worldView->getActorsInScene() }; api["activeActors"] = GObjectList{ worldView->getActorsInScene() };
// TODO: add world.placeNewObject(recordId, cell, pos, [rot]) api["createObject"] = [](std::string_view recordId, sol::optional<int> 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); return LuaUtil::makeReadOnly(api);
} }

View File

@ -154,6 +154,9 @@ namespace MWLua
mWorldView.update(); mWorldView.update();
std::erase_if(mActiveLocalScripts,
[](const LocalScripts* l) { return l->getPtr().isEmpty() || l->getPtr().getRefData().isDeleted(); });
mGlobalScripts.statsNextFrame(); mGlobalScripts.statsNextFrame();
for (LocalScripts* scripts : mActiveLocalScripts) for (LocalScripts* scripts : mActiveLocalScripts)
scripts->statsNextFrame(); scripts->statsNextFrame();

View File

@ -20,6 +20,8 @@ namespace MWLua
std::string ptrToString(const MWWorld::Ptr& ptr) std::string ptrToString(const MWWorld::Ptr& ptr)
{ {
std::string res = "object"; std::string res = "object";
if (ptr.getRefData().isDeleted())
res = "deleted object";
res.append(idToString(getId(ptr))); res.append(idToString(getId(ptr)));
res.append(" ("); res.append(" (");
res.append(getLuaObjectTypeName(ptr)); res.append(getLuaObjectTypeName(ptr));

View File

@ -7,6 +7,7 @@
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"
#include "../mwworld/containerstore.hpp" #include "../mwworld/containerstore.hpp"
#include "../mwworld/player.hpp" #include "../mwworld/player.hpp"
#include "../mwworld/scene.hpp"
#include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/creaturestats.hpp"
@ -87,6 +88,8 @@ namespace MWLua
{ {
MWWorld::Ptr newObj = world->moveObject(obj, cell, mPos); MWWorld::Ptr newObj = world->moveObject(obj, cell, mPos);
world->rotateObject(newObj, mRot); world->rotateObject(newObj, mRot);
if (!newObj.getRefData().isEnabled())
world->enable(newObj);
} }
} }
@ -198,6 +201,32 @@ namespace MWLua
context.mLuaManager->addAction(std::make_unique<ActivateAction>(context.mLua, o.id(), actor.id())); context.mLuaManager->addAction(std::make_unique<ActivateAction>(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, GObject>)
objectT["enabled"] = sol::property(isEnabled, setEnabled);
else
objectT["enabled"] = sol::readonly_property(isEnabled);
if constexpr (std::is_same_v<ObjectT, GObject>) if constexpr (std::is_same_v<ObjectT, GObject>)
{ // Only for global scripts { // Only for global scripts
objectT["addScript"] = [context](const GObject& object, std::string_view path, sol::object initData) { objectT["addScript"] = [context](const GObject& object, std::string_view path, sol::object initData) {
@ -243,11 +272,74 @@ namespace MWLua
localScripts->removeScript(*scriptId); localScripts->removeScript(*scriptId);
}; };
objectT["teleport"] = [context](const GObject& object, std::string_view cell, const osg::Vec3f& pos, auto removeFn = [context](const GObject& object, int countToRemove) {
const sol::optional<osg::Vec3f>& optRot) {
MWWorld::Ptr ptr = object.ptr(); 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<int> 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<GObject>& 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<osg::Vec3f>& 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(); osg::Vec3f rot = optRot ? *optRot : ptr.getRefData().getPosition().asRotationVec3();
auto action = std::make_unique<TeleportAction>(context.mLua, object.id(), cell, pos, rot); auto action = std::make_unique<TeleportAction>(context.mLua, getId(ptr), cell, pos, rot);
if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr())
context.mLuaManager->addTeleportPlayerAction(std::move(action)); context.mLuaManager->addTeleportPlayerAction(std::move(action));
else else
@ -335,25 +427,41 @@ namespace MWLua
return ObjectList<ObjectT>{ list }; return ObjectList<ObjectT>{ 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(); const MWWorld::Ptr& ptr = inventory.mObj.ptr();
MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr);
return store.count(ESM::RefId::stringRefId(recordId)); return store.count(ESM::RefId::stringRefId(recordId));
}; };
inventoryT["find"] = [](const InventoryT& inventory, std::string_view recordId) -> sol::optional<ObjectT> {
if constexpr (std::is_same_v<ObjectT, GObject>) const MWWorld::Ptr& ptr = inventory.mObj.ptr();
{ // Only for global scripts MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr);
// TODO auto itemId = ESM::RefId::stringRefId(recordId);
// obj.inventory:drop(obj2, [count]) for (const MWWorld::Ptr& item : store)
// obj.inventory:drop(recordId, [count]) {
// obj.inventory:addNew(recordId, [count]) if (item.getCellRef().getRefId() == itemId)
// obj.inventory:remove(obj/recordId, [count]) {
/*objectT["moveInto"] = [](const GObject& obj, const InventoryT& inventory) {}; MWBase::Environment::get().getWorldModel()->registerPtr(item);
inventoryT["drop"] = [](const InventoryT& inventory) {}; return ObjectT(getId(item));
inventoryT["addNew"] = [](const InventoryT& inventory) {};
inventoryT["remove"] = [](const InventoryT& inventory) {};*/
} }
} }
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<std::vector<ObjectId>>();
for (const MWWorld::Ptr& item : store)
{
if (item.getCellRef().getRefId() == itemId)
{
MWBase::Environment::get().getWorldModel()->registerPtr(item);
list->push_back(getId(item));
}
}
return ObjectList<ObjectT>{ list };
};
}
template <class ObjectT> template <class ObjectT>
void initObjectBindings(const std::string& prefix, const Context& context) void initObjectBindings(const std::string& prefix, const Context& context)

View File

@ -267,7 +267,6 @@ namespace
iter->mData.enable(); iter->mData.enable();
MWBase::Environment::get().getWorld()->disable(ptr); MWBase::Environment::get().getWorld()->disable(ptr);
} }
else
MWBase::Environment::get().getWorldModel()->registerPtr(ptr); MWBase::Environment::get().getWorldModel()->registerPtr(ptr);
return; return;
} }

View File

@ -18,6 +18,7 @@
#include "inventorystore.hpp" #include "inventorystore.hpp"
#include "nullaction.hpp" #include "nullaction.hpp"
#include "ptr.hpp" #include "ptr.hpp"
#include "worldmodel.hpp"
#include "../mwgui/tooltips.hpp" #include "../mwgui/tooltips.hpp"
@ -373,6 +374,7 @@ namespace MWWorld
Ptr newPtr = copyToCellImpl(ptr, cell); Ptr newPtr = copyToCellImpl(ptr, cell);
newPtr.getCellRef().unsetRefNum(); // This RefNum is only valid within the original cell of the reference newPtr.getCellRef().unsetRefNum(); // This RefNum is only valid within the original cell of the reference
newPtr.getRefData().setCount(count); newPtr.getRefData().setCount(count);
MWBase::Environment::get().getWorldModel()->registerPtr(newPtr);
if (hasInventoryStore(newPtr)) if (hasInventoryStore(newPtr))
getInventoryStore(newPtr).setActor(newPtr); getInventoryStore(newPtr).setActor(newPtr);
return newPtr; return newPtr;

View File

@ -788,8 +788,6 @@ namespace MWWorld
void World::enable(const Ptr& reference) void World::enable(const Ptr& reference)
{ {
MWBase::Environment::get().getWorldModel()->registerPtr(reference);
if (!reference.isInCell()) if (!reference.isInCell())
return; return;
@ -840,7 +838,6 @@ namespace MWWorld
if (reference == getPlayerPtr()) if (reference == getPlayerPtr())
throw std::runtime_error("can not disable player object"); throw std::runtime_error("can not disable player object");
MWBase::Environment::get().getWorldModel()->deregisterPtr(reference);
reference.getRefData().disable(); reference.getRefData().disable();
if (reference.getCellRef().getRefNum().hasContentFile()) if (reference.getCellRef().getRefNum().hasContentFile())

View File

@ -80,7 +80,6 @@
-- @usage -- @usage
-- # DataFiles/l10n/MyMod/en.yaml -- # DataFiles/l10n/MyMod/en.yaml
-- good_morning: 'Good morning.' -- good_morning: 'Good morning.'
--
-- you_have_arrows: |- -- you_have_arrows: |-
-- {count, plural, -- {count, plural,
-- one {You have one arrow.} -- one {You have one arrow.}
@ -107,11 +106,12 @@
-- Any object that exists in the game world and has a specific location. -- Any object that exists in the game world and has a specific location.
-- Player, actors, items, and statics are game objects. -- Player, actors, items, and statics are game objects.
-- @type GameObject -- @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 position Object position.
-- @field openmw.util#Vector3 rotation Object rotation (ZXY order). -- @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 #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 #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. -- @field #string recordId Returns record ID of the object in lowercase.
--- ---
@ -163,13 +163,39 @@
--- ---
-- Moves object to given cell and position. -- 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 -- 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 -- @function [parent=#GameObject] teleport
-- @param self -- @param self
-- @param #string cellName Name of the cell to teleport into. For exteriors can be empty. -- @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 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 all = playerInventory:getAll()
-- local weapons = playerInventory:getAll(types.Weapon) -- 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 return nil

View File

@ -59,5 +59,19 @@
-- @function [parent=#world] isWorldPaused -- @function [parent=#world] isWorldPaused
-- @return #boolean -- @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 return nil