mirror of
https://github.com/OpenMW/openmw.git
synced 2025-03-02 20:39:42 +00:00
Add mwlua/actions
This commit is contained in:
parent
8facf2952a
commit
9d09ecf8ca
7 changed files with 288 additions and 13 deletions
|
@ -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
|
||||
|
|
140
apps/openmw/mwlua/actions.cpp
Normal file
140
apps/openmw/mwlua/actions.cpp
Normal file
|
@ -0,0 +1,140 @@
|
|||
#include "actions.hpp"
|
||||
|
||||
#include <components/debug/debuglog.hpp>
|
||||
|
||||
#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<bool, MWWorld::InventoryStore::Slots> 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<ObjectId>(item))
|
||||
{
|
||||
itemPtr = worldView.getObjectRegistry()->getPtr(std::get<ObjectId>(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<const MWWorld::ContainerStore*>(&store))
|
||||
{
|
||||
Log(Debug::Warning) << "Object" << idToString(std::get<ObjectId>(item)) << " is not in inventory";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::string& recordId = std::get<std::string>(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);
|
||||
}
|
||||
|
||||
}
|
55
apps/openmw/mwlua/actions.hpp
Normal file
55
apps/openmw/mwlua/actions.hpp
Normal file
|
@ -0,0 +1,55 @@
|
|||
#ifndef MWLUA_ACTIONS_H
|
||||
#define MWLUA_ACTIONS_H
|
||||
|
||||
#include <variant>
|
||||
|
||||
#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<std::string, ObjectId>; // recordId or ObjectId
|
||||
using Equipment = std::map<int, Item>; // 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
|
|
@ -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<int>();
|
||||
if (value.is<LObject>())
|
||||
eqp[slot] = value.as<LObject>().id();
|
||||
else
|
||||
eqp[slot] = value.as<std::string>();
|
||||
}
|
||||
manager->addAction(std::make_unique<SetEquipmentAction>(obj.id(), std::move(eqp)));
|
||||
};
|
||||
selfAPI["getCombatTarget"] = [worldView=context.mWorldView](SelfObject& self) -> sol::optional<LObject>
|
||||
{
|
||||
|
|
|
@ -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>& 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>&& action) { mActionQueue.push_back(std::move(action)); }
|
||||
void addTeleportPlayerAction(std::unique_ptr<TeleportAction>&& action) { mTeleportPlayerAction = std::move(action); }
|
||||
void addUIMessage(std::string_view message) { mUIMessages.emplace_back(message); }
|
||||
|
||||
// Saving
|
||||
|
@ -90,6 +93,8 @@ namespace MWLua
|
|||
std::vector<ObjectId> mActorAddedEvents;
|
||||
|
||||
// Queued actions that should be done in main thread. Processed by applyQueuedChanges().
|
||||
std::vector<std::unique_ptr<Action>> mActionQueue;
|
||||
std::unique_ptr<TeleportAction> mTeleportPlayerAction;
|
||||
std::vector<std::string> mUIMessages;
|
||||
};
|
||||
|
||||
|
|
|
@ -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<osg::Vec3f>& rot)
|
||||
const osg::Vec3f& pos, const sol::optional<osg::Vec3f>& 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<TeleportAction>(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<int>();
|
||||
if (value.is<GObject>())
|
||||
eqp[slot] = value.as<GObject>().id();
|
||||
else
|
||||
eqp[slot] = value.as<std::string>();
|
||||
}
|
||||
return eqp;
|
||||
}
|
||||
|
||||
template <class ObjectT>
|
||||
static void addInventoryBindings(sol::usertype<ObjectT>& 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<ObjectT, GObject>)
|
||||
{ // 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<SetEquipmentAction>(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) {};*/
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue