diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c3d34e1f0..3ce58ef8ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -174,6 +174,7 @@ Bug #7943: Using "addSoulGem" and "dropSoulGem" commands to creatures works only with "Weapon & Shield" flagged ones Bug #7970: Difference of GetPCSleep (?) behavior between vanilla and OpenMW Bug #7980: Paralyzed NPCs' lips move + Feature #1415: Infinite fall failsafe Feature #2566: Handle NAM9 records for manual cell references Feature #3537: Shader-based water ripples Feature #5173: Support for NiFogProperty diff --git a/apps/openmw/mwworld/scene.cpp b/apps/openmw/mwworld/scene.cpp index 5f585c1d26..d6993a6f94 100644 --- a/apps/openmw/mwworld/scene.cpp +++ b/apps/openmw/mwworld/scene.cpp @@ -39,6 +39,8 @@ #include "../mwphysics/object.hpp" #include "../mwphysics/physicssystem.hpp" +#include "../mwworld/actionteleport.hpp" + #include "cellpreloader.hpp" #include "cellstore.hpp" #include "cellvisitors.hpp" @@ -138,10 +140,22 @@ namespace } void addObject(const MWWorld::Ptr& ptr, const MWWorld::World& world, const MWPhysics::PhysicsSystem& physics, - DetourNavigator::Navigator& navigator, const DetourNavigator::UpdateGuard* navigatorUpdateGuard = nullptr) + float& lowestPoint, bool isInterior, DetourNavigator::Navigator& navigator, + const DetourNavigator::UpdateGuard* navigatorUpdateGuard = nullptr) { if (const auto object = physics.getObject(ptr)) { + // Find the lowest point of this collision object in world space from its AABB if interior + // this point is used to determine the infinite fall cutoff from lowest point in the cell + if (isInterior) + { + btVector3 aabbMin; + btVector3 aabbMax; + const auto transform = object->getTransform(); + object->getShapeInstance()->mCollisionShape->getAabb(transform, aabbMin, aabbMax); + lowestPoint = std::min(lowestPoint, static_cast(aabbMin.z())); + } + const DetourNavigator::ObjectTransform objectTransform{ ptr.getRefData().getPosition(), ptr.getCellRef().getScale() }; @@ -526,6 +540,7 @@ namespace MWWorld navigatorUpdateGuard.reset(); assert(mActiveCells.empty()); mCurrentCell = nullptr; + mLowestPoint = std::numeric_limits::max(); mPreloader->clear(); } @@ -556,12 +571,39 @@ namespace MWWorld void Scene::playerMoved(const osg::Vec3f& pos) { - if (!mCurrentCell || !mCurrentCell->isExterior()) + if (!mCurrentCell) return; - osg::Vec2i newCell = getNewGridCenter(pos, &mCurrentGridCenter); - if (newCell != mCurrentGridCenter) - requestChangeCellGrid(pos, newCell); + // The player is reset when z is 90 units below the lowest reference bound z. + constexpr float lowestPointAdjustment = -90.0f; + if (mCurrentCell->isExterior()) + { + osg::Vec2i newCell = getNewGridCenter(pos, &mCurrentGridCenter); + if (newCell != mCurrentGridCenter) + requestChangeCellGrid(pos, newCell); + } + else if (pos.z() < mLowestPoint + lowestPointAdjustment) + { + // Player has fallen into the void, reset to interior marker/coc (#1415) + const std::string_view cellNameId = mCurrentCell->getCell()->getNameId(); + MWBase::World* world = MWBase::Environment::get().getWorld(); + MWWorld::Ptr playerPtr = world->getPlayerPtr(); + + // Check that collision is enabled, which is opposite to Vanilla + // this change was decided in MR #4100 as the behaviour is preferable + if (world->isActorCollisionEnabled(playerPtr)) + { + ESM::Position newPos; + const ESM::RefId refId = world->findInteriorPosition(cellNameId, newPos); + + // Only teleport if that teleport point is > the lowest point, rare edge case + if (!refId.empty() && newPos.pos[2] >= mLowestPoint - lowestPointAdjustment) + { + MWWorld::ActionTeleport(refId, newPos, false).execute(playerPtr); + Log(Debug::Warning) << "Player position has been reset due to falling into the void"; + } + } + } } void Scene::requestChangeCellGrid(const osg::Vec3f& position, const osg::Vec2i& cell, bool changeEvent) @@ -840,6 +882,7 @@ namespace MWWorld , mPreloadDoors(Settings::cells().mPreloadDoors) , mPreloadFastTravel(Settings::cells().mPreloadFastTravel) , mPredictionTime(Settings::cells().mPredictionTime) + , mLowestPoint(std::numeric_limits::max()) { mPreloader = std::make_unique(rendering.getResourceSystem(), physics->getShapeManager(), rendering.getTerrain(), rendering.getLandManager()); @@ -970,20 +1013,23 @@ namespace MWWorld void Scene::insertCell( CellStore& cell, Loading::Listener* loadingListener, const DetourNavigator::UpdateGuard* navigatorUpdateGuard) { + const bool isInterior = !cell.isExterior(); InsertVisitor insertVisitor(cell, loadingListener); cell.forEach(insertVisitor); insertVisitor.insert( [&](const MWWorld::Ptr& ptr) { addObject(ptr, mWorld, mPagedRefs, *mPhysics, mRendering); }); - insertVisitor.insert( - [&](const MWWorld::Ptr& ptr) { addObject(ptr, mWorld, *mPhysics, mNavigator, navigatorUpdateGuard); }); + insertVisitor.insert([&](const MWWorld::Ptr& ptr) { + addObject(ptr, mWorld, *mPhysics, mLowestPoint, isInterior, mNavigator, navigatorUpdateGuard); + }); } void Scene::addObjectToScene(const Ptr& ptr) { + const bool isInterior = mCurrentCell && !mCurrentCell->isExterior(); try { addObject(ptr, mWorld, mPagedRefs, *mPhysics, mRendering); - addObject(ptr, mWorld, *mPhysics, mNavigator); + addObject(ptr, mWorld, *mPhysics, mLowestPoint, isInterior, mNavigator); mWorld.scaleObject(ptr, ptr.getCellRef().getScale()); } catch (std::exception& e) diff --git a/apps/openmw/mwworld/scene.hpp b/apps/openmw/mwworld/scene.hpp index 93d814ec51..116e52e535 100644 --- a/apps/openmw/mwworld/scene.hpp +++ b/apps/openmw/mwworld/scene.hpp @@ -102,6 +102,7 @@ namespace MWWorld bool mPreloadDoors; bool mPreloadFastTravel; float mPredictionTime; + float mLowestPoint; int mHalfGridSize = Constants::CellGridRadius;