diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 0cf609652b..3c29baf3aa 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -57,7 +57,7 @@ add_openmw_dir (mwscript add_openmw_dir (mwlua luamanagerimp actions object worldview userdataserializer eventqueue query - luabindings localscripts objectbindings asyncbindings camerabindings uibindings + luabindings localscripts objectbindings cellbindings asyncbindings camerabindings uibindings ) add_openmw_dir (mwsound diff --git a/apps/openmw/mwlua/actions.cpp b/apps/openmw/mwlua/actions.cpp index bc9cdc3aad..00707b3d31 100644 --- a/apps/openmw/mwlua/actions.cpp +++ b/apps/openmw/mwlua/actions.cpp @@ -2,6 +2,7 @@ #include +#include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/player.hpp" @@ -11,23 +12,14 @@ 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); + MWWorld::CellStore* cell = worldView.findCell(mCell, mPos); if (!cell) { Log(Debug::Error) << "LuaManager::applyTeleport -> cell not found: '" << mCell << "'"; return; } + MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr obj = worldView.getObjectRegistry()->getPtr(mObject, false); const MWWorld::Class& cls = obj.getClass(); bool isPlayer = obj == world->getPlayerPtr(); @@ -40,7 +32,7 @@ namespace MWLua std::memcpy(esmPos.pos, &mPos, sizeof(osg::Vec3f)); std::memcpy(esmPos.rot, &mRot, sizeof(osg::Vec3f)); world->getPlayer().setTeleported(true); - if (exterior) + if (cell->isExterior()) world->changeToExteriorCell(esmPos, true); else world->changeToInteriorCell(mCell, esmPos, true); diff --git a/apps/openmw/mwlua/cellbindings.cpp b/apps/openmw/mwlua/cellbindings.cpp new file mode 100644 index 0000000000..a23fb47c32 --- /dev/null +++ b/apps/openmw/mwlua/cellbindings.cpp @@ -0,0 +1,62 @@ +#include "luabindings.hpp" + +#include + +#include "../mwworld/cellstore.hpp" + +namespace MWLua +{ + + template + static void initCellBindings(const std::string& prefix, const Context& context) + { + sol::usertype cellT = context.mLua->sol().new_usertype(prefix + "Cell"); + + cellT[sol::meta_function::equal_to] = [](const CellT& a, const CellT& b) { return a.mStore == b.mStore; }; + cellT[sol::meta_function::to_string] = [](const CellT& c) + { + const ESM::Cell* cell = c.mStore->getCell(); + std::stringstream res; + if (cell->isExterior()) + res << "exterior(" << cell->getGridX() << ", " << cell->getGridY() << ")"; + else + res << "interior(" << cell->mName << ")"; + return res.str(); + }; + + cellT["name"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->mName; }); + cellT["region"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->mRegion; }); + cellT["gridX"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->getGridX(); }); + cellT["gridY"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->getGridY(); }); + cellT["isExterior"] = sol::readonly_property([](const CellT& c) { return c.mStore->isExterior(); }); + cellT["hasWater"] = sol::readonly_property([](const CellT& c) { return c.mStore->getCell()->hasWater(); }); + + cellT["isInSameSpace"] = [](const CellT& c, const ObjectT& obj) + { + const MWWorld::Ptr& ptr = obj.ptr(); + if (!ptr.isInCell()) + return false; + MWWorld::CellStore* cell = ptr.getCell(); + return cell == c.mStore || (cell->isExterior() && c.mStore->isExterior()); + }; + + if constexpr (std::is_same_v) + { // only for global scripts + cellT["selectObjects"] = [context](const CellT& cell, const Queries::Query& query) + { + return GObjectList{selectObjectsFromCellStore(query, cell.mStore, context)}; + }; + } + } + + void initCellBindingsForLocalScripts(const Context& context) + { + initCellBindings("L", context); + } + + void initCellBindingsForGlobalScripts(const Context& context) + { + initCellBindings("G", context); + } + +} diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index 76a01ee416..bc50062546 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -62,6 +62,22 @@ namespace MWLua { sol::table api(context.mLua->sol(), sol::create); WorldView* worldView = context.mWorldView; + api["getCellByName"] = [worldView=context.mWorldView](const std::string& name) -> sol::optional + { + MWWorld::CellStore* cell = worldView->findNamedCell(name); + if (cell) + return GCell{cell}; + else + return {}; + }; + api["getExteriorCell"] = [worldView=context.mWorldView](int x, int y) -> sol::optional + { + MWWorld::CellStore* cell = worldView->findExteriorCell(x, y); + if (cell) + return GCell{cell}; + else + return {}; + }; api["activeActors"] = GObjectList{worldView->getActorsInScene()}; api["selectObjects"] = [context](const Queries::Query& query) { diff --git a/apps/openmw/mwlua/luabindings.hpp b/apps/openmw/mwlua/luabindings.hpp index cb38e63fb7..ab83ce995f 100644 --- a/apps/openmw/mwlua/luabindings.hpp +++ b/apps/openmw/mwlua/luabindings.hpp @@ -11,6 +11,11 @@ #include "query.hpp" #include "worldview.hpp" +namespace MWWorld +{ + class CellStore; +} + namespace MWLua { @@ -25,6 +30,18 @@ namespace MWLua void initObjectBindingsForLocalScripts(const Context&); void initObjectBindingsForGlobalScripts(const Context&); + // Implemented in cellbindings.cpp + struct LCell // for local scripts + { + MWWorld::CellStore* mStore; + }; + struct GCell // for global scripts + { + MWWorld::CellStore* mStore; + }; + void initCellBindingsForLocalScripts(const Context&); + void initCellBindingsForGlobalScripts(const Context&); + // Implemented in asyncbindings.cpp struct AsyncPackageId { diff --git a/apps/openmw/mwlua/object.cpp b/apps/openmw/mwlua/object.cpp index 2eb8d16f3c..e3f5323736 100644 --- a/apps/openmw/mwlua/object.cpp +++ b/apps/openmw/mwlua/object.cpp @@ -49,12 +49,19 @@ namespace MWLua return fallback; } + bool isMarker(const MWWorld::Ptr& ptr) + { + std::string_view id = *ptr.getCellRef().getRefIdPtr(); + return id == "prisonmarker" || id == "divinemarker" || id == "templemarker" || id == "northmarker"; + } + std::string_view getMWClassName(const MWWorld::Ptr& ptr) { if (*ptr.getCellRef().getRefIdPtr() == "player") return "Player"; - else - return getMWClassName(typeid(ptr.getClass()), ptr.getTypeName()); + if (isMarker(ptr)) + return "Marker"; + return getMWClassName(typeid(ptr.getClass()), ptr.getTypeName()); } std::string ptrToString(const MWWorld::Ptr& ptr) diff --git a/apps/openmw/mwlua/object.hpp b/apps/openmw/mwlua/object.hpp index c4f1dc32f1..b45c8e247c 100644 --- a/apps/openmw/mwlua/object.hpp +++ b/apps/openmw/mwlua/object.hpp @@ -18,6 +18,7 @@ namespace MWLua inline const ObjectId& getId(const MWWorld::Ptr& ptr) { return ptr.getCellRef().getRefNum(); } std::string idToString(const ObjectId& id); std::string ptrToString(const MWWorld::Ptr& ptr); + bool isMarker(const MWWorld::Ptr& ptr); std::string_view getMWClassName(const std::type_index& cls_type, std::string_view fallback = "Unknown"); std::string_view getMWClassName(const MWWorld::Ptr& ptr); diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index ca6fbae31f..64ac84e4f0 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -39,6 +39,9 @@ namespace sol namespace MWLua { + template + using Cell = std::conditional_t, LCell, GCell>; + template static const MWWorld::Ptr& requireClass(const MWWorld::Ptr& ptr) { @@ -95,10 +98,13 @@ namespace MWLua { return o.ptr().getCellRef().getRefId(); }); - objectT["cell"] = sol::readonly_property([](const ObjectT& o) + objectT["cell"] = sol::readonly_property([](const ObjectT& o) -> sol::optional> { - MWBase::World* world = MWBase::Environment::get().getWorld(); - return world->getCellName(o.ptr().getCell()); + const MWWorld::Ptr& ptr = o.ptr(); + if (ptr.isInCell()) + return Cell{ptr.getCell()}; + else + return {}; }); objectT["position"] = sol::readonly_property([](const ObjectT& o) -> osg::Vec3f { @@ -117,6 +123,22 @@ namespace MWLua context.mLocalEventQueue->push_back({dest.id(), std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer)}); }; + objectT["canMove"] = [context](const ObjectT& o) + { + const MWWorld::Class& cls = o.ptr().getClass(); + return cls.getMaxSpeed(o.ptr()) > 0; + }; + objectT["getRunSpeed"] = [context](const ObjectT& o) + { + const MWWorld::Class& cls = o.ptr().getClass(); + return cls.getRunSpeed(o.ptr()); + }; + objectT["getWalkSpeed"] = [context](const ObjectT& o) + { + const MWWorld::Class& cls = o.ptr().getClass(); + return cls.getWalkSpeed(o.ptr()); + }; + if constexpr (std::is_same_v) { // Only for global scripts objectT["addScript"] = [luaManager=context.mLuaManager](const GObject& object, const std::string& path) @@ -155,9 +177,17 @@ namespace MWLua { return ptr(o).getCellRef().getDoorDest().asRotationVec3(); }); - objectT["destCell"] = sol::readonly_property([ptr](const ObjectT& o) -> std::string_view + objectT["destCell"] = sol::readonly_property( + [ptr, worldView=context.mWorldView](const ObjectT& o) -> sol::optional> { - return ptr(o).getCellRef().getDestCell(); + const MWWorld::CellRef& cellRef = ptr(o).getCellRef(); + if (!cellRef.getTeleport()) + return {}; + MWWorld::CellStore* cell = worldView->findCell(cellRef.getDestCell(), cellRef.getDoorDest().asVec3()); + if (cell) + return Cell{cell}; + else + return {}; }); } diff --git a/apps/openmw/mwlua/query.cpp b/apps/openmw/mwlua/query.cpp index f51b99d8d5..f42c7ae2d4 100644 --- a/apps/openmw/mwlua/query.cpp +++ b/apps/openmw/mwlua/query.cpp @@ -5,6 +5,7 @@ #include #include "../mwclass/container.hpp" +#include "../mwworld/cellstore.hpp" #include "worldview.hpp" @@ -24,11 +25,16 @@ namespace MWLua static std::array objectFields = { Queries::Field({"type"}, typeid(std::string)), Queries::Field({"recordId"}, typeid(std::string)), + Queries::Field({"cell", "name"}, typeid(std::string)), + Queries::Field({"cell", "region"}, typeid(std::string)), + Queries::Field({"cell", "isExterior"}, typeid(bool)), Queries::Field({"count"}, typeid(int32_t)), }; static std::array doorFields = { Queries::Field({"isTeleport"}, typeid(bool)), - Queries::Field({"destCell"}, typeid(std::string)), + Queries::Field({"destCell", "name"}, typeid(std::string)), + Queries::Field({"destCell", "region"}, typeid(std::string)), + Queries::Field({"destCell", "isExterior"}, typeid(bool)), }; return std::vector{ createGroup("OBJECT", objectFields), @@ -42,13 +48,8 @@ namespace MWLua return fieldGroups; } - ObjectIdList selectObjectsFromList(const Queries::Query& query, const ObjectIdList& list, const Context& context) + bool checkQueryConditions(const Queries::Query& query, const ObjectId& id, const Context& context) { - if (!query.mOrderBy.empty() || !query.mGroupBy.empty() || query.mOffset > 0) - throw std::runtime_error("OrderBy, GroupBy, and Offset are not supported"); - - ObjectIdList res = std::make_shared>(); - std::vector condStack; auto compareFn = [](auto&& a, auto&& b, Queries::Condition::Type t) { switch (t) @@ -63,88 +64,125 @@ namespace MWLua throw std::runtime_error("Unsupported condition type"); } }; + sol::object obj; + MWWorld::Ptr ptr; + if (context.mIsGlobal) + { + GObject g(id, context.mWorldView->getObjectRegistry()); + if (!g.isValid()) + return false; + ptr = g.ptr(); + obj = sol::make_object(context.mLua->sol(), g); + } + else + { + LObject l(id, context.mWorldView->getObjectRegistry()); + if (!l.isValid()) + return false; + ptr = l.ptr(); + obj = sol::make_object(context.mLua->sol(), l); + } + if (ptr.getRefData().getCount() == 0) + return false; + // It is stupid, but "prisonmarker" has class "Door" despite that it is only an invisible marker. So we ignore all markers. + if (isMarker(ptr)) + return false; + const MWWorld::Class& cls = ptr.getClass(); + if (cls.isActivator() != (query.mQueryType == ObjectQueryTypes::ACTIVATORS)) + return false; + if (cls.isActor() != (query.mQueryType == ObjectQueryTypes::ACTORS)) + return false; + if (cls.isDoor() != (query.mQueryType == ObjectQueryTypes::DOORS)) + return false; + if ((typeid(cls) == typeid(MWClass::Container)) != (query.mQueryType == ObjectQueryTypes::CONTAINERS)) + return false; + + std::vector condStack; + for (const Queries::Operation& op : query.mFilter.mOperations) + { + switch(op.mType) + { + case Queries::Operation::PUSH: + { + const Queries::Condition& cond = query.mFilter.mConditions[op.mConditionIndex]; + sol::object fieldObj = obj; + for (const std::string& field : cond.mField->path()) + fieldObj = LuaUtil::getFieldOrNil(fieldObj, field); + bool c; + if (fieldObj == sol::nil) + c = false; + else if (cond.mField->type() == typeid(std::string)) + c = compareFn(fieldObj.as(), std::get(cond.mValue), cond.mType); + else if (cond.mField->type() == typeid(float)) + c = compareFn(fieldObj.as(), std::get(cond.mValue), cond.mType); + else if (cond.mField->type() == typeid(double)) + c = compareFn(fieldObj.as(), std::get(cond.mValue), cond.mType); + else if (cond.mField->type() == typeid(bool)) + c = compareFn(fieldObj.as(), std::get(cond.mValue), cond.mType); + else if (cond.mField->type() == typeid(int32_t)) + c = compareFn(fieldObj.as(), std::get(cond.mValue), cond.mType); + else if (cond.mField->type() == typeid(int64_t)) + c = compareFn(fieldObj.as(), std::get(cond.mValue), cond.mType); + else + throw std::runtime_error("Unknown field type"); + condStack.push_back(c); + break; + } + case Queries::Operation::NOT: + condStack.back() = !condStack.back(); + break; + case Queries::Operation::AND: + { + bool v = condStack.back(); + condStack.pop_back(); + condStack.back() = condStack.back() && v; + break; + } + case Queries::Operation::OR: + { + bool v = condStack.back(); + condStack.pop_back(); + condStack.back() = condStack.back() || v; + break; + } + } + } + return condStack.empty() || condStack.back() != 0; + } + + ObjectIdList selectObjectsFromList(const Queries::Query& query, const ObjectIdList& list, const Context& context) + { + if (!query.mOrderBy.empty() || !query.mGroupBy.empty() || query.mOffset > 0) + throw std::runtime_error("OrderBy, GroupBy, and Offset are not supported"); + + ObjectIdList res = std::make_shared>(); for (const ObjectId& id : *list) { if (static_cast(res->size()) == query.mLimit) break; - sol::object obj; - MWWorld::Ptr ptr; - if (context.mIsGlobal) - { - GObject g(id, context.mWorldView->getObjectRegistry()); - if (!g.isValid()) continue; - ptr = g.ptr(); - obj = sol::make_object(context.mLua->sol(), g); - } - else - { - LObject l(id, context.mWorldView->getObjectRegistry()); - if (!l.isValid()) continue; - ptr = l.ptr(); - obj = sol::make_object(context.mLua->sol(), l); - } - const MWWorld::Class& cls = ptr.getClass(); - if (cls.isActivator() && query.mQueryType != ObjectQueryTypes::ACTIVATORS) - continue; - if (cls.isActor() && query.mQueryType != ObjectQueryTypes::ACTORS) - continue; - if (cls.isDoor() && query.mQueryType != ObjectQueryTypes::DOORS) - continue; - if (typeid(cls) == typeid(MWClass::Container) && query.mQueryType != ObjectQueryTypes::CONTAINERS) - continue; - - condStack.clear(); - for (const Queries::Operation& op : query.mFilter.mOperations) - { - switch(op.mType) - { - case Queries::Operation::PUSH: - { - const Queries::Condition& cond = query.mFilter.mConditions[op.mConditionIndex]; - sol::object fieldObj = obj; - for (const std::string& field : cond.mField->path()) - fieldObj = sol::table(fieldObj)[field]; - bool c; - if (cond.mField->type() == typeid(std::string)) - c = compareFn(fieldObj.as(), std::get(cond.mValue), cond.mType); - else if (cond.mField->type() == typeid(float)) - c = compareFn(fieldObj.as(), std::get(cond.mValue), cond.mType); - else if (cond.mField->type() == typeid(double)) - c = compareFn(fieldObj.as(), std::get(cond.mValue), cond.mType); - else if (cond.mField->type() == typeid(bool)) - c = compareFn(fieldObj.as(), std::get(cond.mValue), cond.mType); - else if (cond.mField->type() == typeid(int32_t)) - c = compareFn(fieldObj.as(), std::get(cond.mValue), cond.mType); - else if (cond.mField->type() == typeid(int64_t)) - c = compareFn(fieldObj.as(), std::get(cond.mValue), cond.mType); - else - throw std::runtime_error("Unknown field type"); - condStack.push_back(c); - break; - } - case Queries::Operation::NOT: - condStack.back() = !condStack.back(); - break; - case Queries::Operation::AND: - { - bool v = condStack.back(); - condStack.pop_back(); - condStack.back() = condStack.back() && v; - break; - } - case Queries::Operation::OR: - { - bool v = condStack.back(); - condStack.pop_back(); - condStack.back() = condStack.back() || v; - break; - } - } - } - if (condStack.empty() || condStack.back() != 0) + if (checkQueryConditions(query, id, context)) res->push_back(id); } return res; } + ObjectIdList selectObjectsFromCellStore(const Queries::Query& query, MWWorld::CellStore* store, const Context& context) + { + if (!query.mOrderBy.empty() || !query.mGroupBy.empty() || query.mOffset > 0) + throw std::runtime_error("OrderBy, GroupBy, and Offset are not supported"); + + ObjectIdList res = std::make_shared>(); + auto visitor = [&](const MWWorld::Ptr& ptr) + { + if (static_cast(res->size()) == query.mLimit) + return false; + context.mWorldView->getObjectRegistry()->registerPtr(ptr); + if (checkQueryConditions(query, getId(ptr), context)) + res->push_back(getId(ptr)); + return static_cast(res->size()) != query.mLimit; + }; + store->forEach(std::move(visitor)); // TODO: maybe use store->forEachType depending on query.mType + return res; + } + } diff --git a/apps/openmw/mwlua/query.hpp b/apps/openmw/mwlua/query.hpp index 6bb3fe09fa..65bf0c5105 100644 --- a/apps/openmw/mwlua/query.hpp +++ b/apps/openmw/mwlua/query.hpp @@ -32,6 +32,7 @@ namespace MWLua // TODO: Implement custom fields. QueryFieldGroup registerCustomFields(...); ObjectIdList selectObjectsFromList(const Queries::Query& query, const ObjectIdList& list, const Context&); + ObjectIdList selectObjectsFromCellStore(const Queries::Query& query, MWWorld::CellStore* store, const Context&); } diff --git a/apps/openmw/mwlua/worldview.cpp b/apps/openmw/mwlua/worldview.cpp index 6e54efac39..beaa2d4770 100644 --- a/apps/openmw/mwlua/worldview.cpp +++ b/apps/openmw/mwlua/worldview.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "../mwclass/container.hpp" @@ -33,6 +34,10 @@ namespace MWLua WorldView::ObjectGroup* WorldView::chooseGroup(const MWWorld::Ptr& ptr) { + // It is important to check `isMarker` first. + // For example "prisonmarker" has class "Door" despite that it is only an invisible marker. + if (isMarker(ptr)) + return nullptr; const MWWorld::Class& cls = ptr.getClass(); if (cls.isActivator()) return &mActivatorsInScene; @@ -113,4 +118,35 @@ namespace MWLua group.mChanged = true; } + // TODO: If Lua scripts will use several threads at the same time, then `find*Cell` functions should have critical sections. + MWWorld::CellStore* WorldView::findCell(const std::string& name, osg::Vec3f position) + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + bool exterior = name.empty() || world->getExterior(name); + if (exterior) + { + int cellX, cellY; + world->positionToIndex(position.x(), position.y(), cellX, cellY); + return world->getExterior(cellX, cellY); + } + else + return world->getInterior(name); + } + + MWWorld::CellStore* WorldView::findNamedCell(const std::string& name) + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + const ESM::Cell* esmCell = world->getExterior(name); + if (esmCell) + return world->getExterior(esmCell->getGridX(), esmCell->getGridY()); + else + return world->getInterior(name); + } + + MWWorld::CellStore* WorldView::findExteriorCell(int x, int y) + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + return world->getExterior(x, y); + } + } diff --git a/apps/openmw/mwlua/worldview.hpp b/apps/openmw/mwlua/worldview.hpp index bf2b458792..ea27e0ff84 100644 --- a/apps/openmw/mwlua/worldview.hpp +++ b/apps/openmw/mwlua/worldview.hpp @@ -44,6 +44,10 @@ namespace MWLua // If onlyActive = true, then search only among the objects that are currently in the scene. // TODO: ObjectIdList selectObjects(const Queries::Query& query, bool onlyActive); + MWWorld::CellStore* findCell(const std::string& name, osg::Vec3f position); + MWWorld::CellStore* findNamedCell(const std::string& name); + MWWorld::CellStore* findExteriorCell(int x, int y); + void load(ESM::ESMReader& esm); void save(ESM::ESMWriter& esm) const;