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

7220-lua-add-a-general-purpose-lexical-parser
Petr Mikheev 2 years ago
parent e7120f189b
commit c294898246

@ -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

@ -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;
}

@ -75,6 +75,8 @@ namespace MWLua
const CellT& cell, sol::optional<sol::table> type) {
ObjectIdList res = std::make_shared<std::vector<ObjectId>>();
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));

@ -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
{

@ -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<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);
}

@ -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();

@ -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));

@ -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<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>)
{ // 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<osg::Vec3f>& 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<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();
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())
context.mLuaManager->addTeleportPlayerAction(std::move(action));
else
@ -335,24 +427,40 @@ namespace MWLua
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();
MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr);
return store.count(ESM::RefId::stringRefId(recordId));
};
if constexpr (std::is_same_v<ObjectT, GObject>)
{ // 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<ObjectT> {
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<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>

@ -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;
}

@ -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;

@ -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())

@ -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

@ -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

Loading…
Cancel
Save