diff --git a/apps/openmw/mwlua/nearbybindings.cpp b/apps/openmw/mwlua/nearbybindings.cpp index 86c8ef31e8..7e1845aeac 100644 --- a/apps/openmw/mwlua/nearbybindings.cpp +++ b/apps/openmw/mwlua/nearbybindings.cpp @@ -3,11 +3,15 @@ #include #include #include +#include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwphysics/raycasting.hpp" +#include "../mwworld/cell.hpp" +#include "../mwworld/cellstore.hpp" +#include "../mwworld/scene.hpp" #include "luamanagerimp.hpp" #include "objectlists.hpp" @@ -262,6 +266,39 @@ namespace MWLua *MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, from, to, includeFlags); }; + api["findNearestNavMeshPosition"] = [](const osg::Vec3f& position, const sol::optional& options) { + DetourNavigator::AgentBounds agentBounds = defaultAgentBounds; + std::optional searchAreaHalfExtents; + DetourNavigator::Flags includeFlags = defaultIncludeFlags; + + if (options.has_value()) + { + if (const auto& t = options->get>("agentBounds")) + { + if (const auto& v = t->get>("shapeType")) + agentBounds.mShapeType = *v; + if (const auto& v = t->get>("halfExtents")) + agentBounds.mHalfExtents = *v; + } + if (const auto& v = options->get>("searchAreaHalfExtents")) + searchAreaHalfExtents = *v; + if (const auto& v = options->get>("includeFlags")) + includeFlags = *v; + } + + if (!searchAreaHalfExtents.has_value()) + { + const bool isEsm4 = MWBase::Environment::get().getWorldScene()->getCurrentCell()->getCell()->isEsm4(); + const float halfExtents = isEsm4 + ? (1 + 2 * Constants::ESM4CellGridRadius) * Constants::ESM4CellSizeInUnits + : (1 + 2 * Constants::CellGridRadius) * Constants::CellSizeInUnits; + searchAreaHalfExtents = osg::Vec3f(halfExtents, halfExtents, halfExtents); + } + + return DetourNavigator::findNearestNavMeshPosition(*MWBase::Environment::get().getWorld()->getNavigator(), + agentBounds, position, *searchAreaHalfExtents, includeFlags); + }; + return LuaUtil::makeReadOnly(api); } } diff --git a/apps/openmw_test_suite/detournavigator/navigator.cpp b/apps/openmw_test_suite/detournavigator/navigator.cpp index a93693c08b..df4d7a1e99 100644 --- a/apps/openmw_test_suite/detournavigator/navigator.cpp +++ b/apps/openmw_test_suite/detournavigator/navigator.cpp @@ -70,6 +70,22 @@ namespace } }; + constexpr std::array defaultHeightfieldData{ { + 0, 0, 0, 0, 0, // row 0 + 0, -25, -25, -25, -25, // row 1 + 0, -25, -100, -100, -100, // row 2 + 0, -25, -100, -100, -100, // row 3 + 0, -25, -100, -100, -100, // row 4 + } }; + + constexpr std::array defaultHeightfieldDataScalar{ { + 0, 0, 0, 0, 0, // row 0 + 0, -25, -25, -25, -25, // row 1 + 0, -25, -100, -100, -100, // row 2 + 0, -25, -100, -100, -100, // row 3 + 0, -25, -100, -100, -100, // row 4 + } }; + template std::unique_ptr makeSquareHeightfieldTerrainShape( const std::array& values, btScalar heightScale = 1, int upAxis = 2, @@ -150,14 +166,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_then_find_path_should_return_path) { - constexpr std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); @@ -177,20 +186,31 @@ namespace << mPath; } + TEST_F(DetourNavigatorNavigatorTest, find_path_to_the_start_position_should_contain_single_point) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + auto updateGuard = mNavigator->makeUpdateGuard(); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, updateGuard.get()); + mNavigator->update(mPlayerPosition, updateGuard.get()); + updateGuard.reset(); + mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mStart, Flag_walk, mAreaCosts, mEndTolerance, mOut), + Status::Success); + + EXPECT_THAT(mPath, ElementsAre(Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125))) << mPath; + } + TEST_F(DetourNavigatorNavigatorTest, add_object_should_change_navmesh) { mSettings.mWaitUntilMinDistanceToPlayer = 0; mNavigator.reset(new NavigatorImpl( mSettings, std::make_unique(":memory:", std::numeric_limits::max()))); - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); CollisionShapeInstance compound(std::make_unique()); @@ -235,14 +255,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_changed_object_should_change_navmesh) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); CollisionShapeInstance compound(std::make_unique()); @@ -288,14 +301,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, for_overlapping_heightfields_objects_should_use_higher) { - const std::array heightfieldData1{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - CollisionShapeInstance heightfield1(makeSquareHeightfieldTerrainShape(heightfieldData1)); + CollisionShapeInstance heightfield1(makeSquareHeightfieldTerrainShape(defaultHeightfieldDataScalar)); heightfield1.shape().setLocalScaling(btVector3(128, 128, 1)); const std::array heightfieldData2{ { @@ -328,14 +334,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, only_one_heightfield_per_cell_is_allowed) { - const std::array heightfieldData1{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface1 = makeSquareHeightfieldSurface(heightfieldData1); + const HeightfieldSurface surface1 = makeSquareHeightfieldSurface(defaultHeightfieldData); const int cellSize1 = mHeightfieldTileSize * (surface1.mSize - 1); const std::array heightfieldData2{ { @@ -366,14 +365,8 @@ namespace { osg::ref_ptr bulletShape(new Resource::BulletShape); - std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - std::unique_ptr shapePtr = makeSquareHeightfieldTerrainShape(heightfieldData); + std::unique_ptr shapePtr + = makeSquareHeightfieldTerrainShape(defaultHeightfieldDataScalar); shapePtr->setLocalScaling(btVector3(128, 128, 1)); bulletShape->mCollisionShape.reset(shapePtr.release()); @@ -542,14 +535,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_object_remove_and_update_then_find_path_should_return_path) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - CollisionShapeInstance heightfield(makeSquareHeightfieldTerrainShape(heightfieldData)); + CollisionShapeInstance heightfield(makeSquareHeightfieldTerrainShape(defaultHeightfieldDataScalar)); heightfield.shape().setLocalScaling(btVector3(128, 128, 1)); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); @@ -579,14 +565,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_heightfield_remove_and_update_then_find_path_should_return_path) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); @@ -649,14 +628,7 @@ namespace mNavigator.reset(new NavigatorImpl( mSettings, std::make_unique(":memory:", std::numeric_limits::max()))); - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); const btVector3 shift = getHeightfieldShift(mCellPosition, cellSize, surface.mMinHeight, surface.mMaxHeight); @@ -745,14 +717,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_then_raycast_should_return_position) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); @@ -771,14 +736,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, update_for_oscillating_object_that_does_not_change_navmesh_should_not_trigger_navmesh_update) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); CollisionShapeInstance oscillatingBox(std::make_unique(btVector3(20, 20, 20))); @@ -837,14 +795,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, for_not_reachable_destination_find_path_should_provide_partial_path) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); CollisionShapeInstance compound(std::make_unique()); @@ -870,14 +821,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, end_tolerance_should_extent_available_destinations) { - const std::array heightfieldData{ { - 0, 0, 0, 0, 0, // row 0 - 0, -25, -25, -25, -25, // row 1 - 0, -25, -100, -100, -100, // row 2 - 0, -25, -100, -100, -100, // row 3 - 0, -25, -100, -100, -100, // row 4 - } }; - const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); CollisionShapeInstance compound(std::make_unique()); @@ -1000,4 +944,58 @@ namespace INSTANTIATE_TEST_SUITE_P(NotSupportedAgentBounds, DetourNavigatorNavigatorNotSupportedAgentBoundsTest, ValuesIn(notSupportedAgentBounds)); + + TEST_F(DetourNavigatorNavigatorTest, find_nearest_nav_mesh_position_should_return_nav_mesh_position) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + auto updateGuard = mNavigator->makeUpdateGuard(); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, updateGuard.get()); + mNavigator->update(mPlayerPosition, updateGuard.get()); + updateGuard.reset(); + mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); + + const osg::Vec3f position(250, 250, 0); + const osg::Vec3f searchAreaHalfExtents(1000, 1000, 1000); + EXPECT_THAT(findNearestNavMeshPosition(*mNavigator, mAgentBounds, position, searchAreaHalfExtents, Flag_walk), + Optional(Vec3fEq(250, 250, -62.5186))); + } + + TEST_F(DetourNavigatorNavigatorTest, find_nearest_nav_mesh_position_should_return_nullopt_when_too_far) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + auto updateGuard = mNavigator->makeUpdateGuard(); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, updateGuard.get()); + mNavigator->update(mPlayerPosition, updateGuard.get()); + updateGuard.reset(); + mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); + + const osg::Vec3f position(250, 250, 250); + const osg::Vec3f searchAreaHalfExtents(100, 100, 100); + EXPECT_EQ(findNearestNavMeshPosition(*mNavigator, mAgentBounds, position, searchAreaHalfExtents, Flag_walk), + std::nullopt); + } + + TEST_F(DetourNavigatorNavigatorTest, find_nearest_nav_mesh_position_should_return_nullopt_when_flags_do_not_match) + { + const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData); + const int cellSize = mHeightfieldTileSize * (surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + auto updateGuard = mNavigator->makeUpdateGuard(); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, updateGuard.get()); + mNavigator->update(mPlayerPosition, updateGuard.get()); + updateGuard.reset(); + mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); + + const osg::Vec3f position(250, 250, 0); + const osg::Vec3f searchAreaHalfExtents(1000, 1000, 1000); + EXPECT_EQ(findNearestNavMeshPosition(*mNavigator, mAgentBounds, position, searchAreaHalfExtents, Flag_swim), + std::nullopt); + } } diff --git a/components/detournavigator/findsmoothpath.hpp b/components/detournavigator/findsmoothpath.hpp index 8b6889cc07..e5efa8815f 100644 --- a/components/detournavigator/findsmoothpath.hpp +++ b/components/detournavigator/findsmoothpath.hpp @@ -64,7 +64,7 @@ namespace DetourNavigator std::reference_wrapper mSettings; }; - inline std::optional findPath(const dtNavMeshQuery& navMeshQuery, const dtPolyRef startRef, + inline std::optional findPolygonPath(const dtNavMeshQuery& navMeshQuery, const dtPolyRef startRef, const dtPolyRef endRef, const osg::Vec3f& startPos, const osg::Vec3f& endPos, const dtQueryFilter& queryFilter, std::span pathBuffer) { @@ -132,7 +132,7 @@ namespace DetourNavigator std::vector polygonPath(settings.mMaxPolygonPathSize); const auto polygonPathSize - = findPath(navMeshQuery, startRef, endRef, startNavMeshPos, endNavMeshPos, queryFilter, polygonPath); + = findPolygonPath(navMeshQuery, startRef, endRef, startNavMeshPos, endNavMeshPos, queryFilter, polygonPath); if (!polygonPathSize.has_value()) return Status::FindPathOverPolygonsFailed; diff --git a/components/detournavigator/navigatorutils.cpp b/components/detournavigator/navigatorutils.cpp index cad201825c..2f9fbbb32e 100644 --- a/components/detournavigator/navigatorutils.cpp +++ b/components/detournavigator/navigatorutils.cpp @@ -1,8 +1,11 @@ #include "navigatorutils.hpp" +#include "debug.hpp" #include "findrandompointaroundcircle.hpp" #include "navigator.hpp" #include "raycast.hpp" +#include + namespace DetourNavigator { std::optional findRandomPointAroundCircle(const Navigator& navigator, const AgentBounds& agentBounds, @@ -37,4 +40,41 @@ namespace DetourNavigator return std::nullopt; return fromNavMeshCoordinates(settings.mRecast, *result); } + + std::optional findNearestNavMeshPosition(const Navigator& navigator, const AgentBounds& agentBounds, + const osg::Vec3f& position, const osg::Vec3f& searchAreaHalfExtents, const Flags includeFlags) + { + const auto navMesh = navigator.getNavMesh(agentBounds); + if (navMesh == nullptr) + return std::nullopt; + + const auto& settings = navigator.getSettings(); + const osg::Vec3f navMeshPosition = toNavMeshCoordinates(settings.mRecast, position); + const auto lockedNavMesh = navMesh->lockConst(); + + dtNavMeshQuery navMeshQuery; + if (const dtStatus status + = navMeshQuery.init(&lockedNavMesh->getImpl(), settings.mDetour.mMaxNavMeshQueryNodes); + dtStatusFailed(status)) + { + Log(Debug::Error) << "Failed to init dtNavMeshQuery for findNearestNavMeshPosition: " + << WriteDtStatus{ status }; + return std::nullopt; + } + + dtQueryFilter queryFilter; + queryFilter.setIncludeFlags(includeFlags); + + osg::Vec3f nearestNavMeshPos; + const osg::Vec3f endPolyHalfExtents = toNavMeshCoordinates(settings.mRecast, searchAreaHalfExtents); + dtPolyRef polyRef; + if (const dtStatus status = navMeshQuery.findNearestPoly( + navMeshPosition.ptr(), endPolyHalfExtents.ptr(), &queryFilter, &polyRef, nearestNavMeshPos.ptr()); + dtStatusFailed(status) || polyRef == 0) + { + return std::nullopt; + } + + return fromNavMeshCoordinates(settings.mRecast, nearestNavMeshPos); + } } diff --git a/components/detournavigator/navigatorutils.hpp b/components/detournavigator/navigatorutils.hpp index ef6ae96313..ca02682ecd 100644 --- a/components/detournavigator/navigatorutils.hpp +++ b/components/detournavigator/navigatorutils.hpp @@ -16,10 +16,10 @@ namespace DetourNavigator { /** * @brief findPath fills output iterator with points of scene surfaces to be used for actor to walk through. - * @param agentBounds allows to find navmesh for given actor. + * @param agentBounds defines which navmesh to use. * @param start path from given point. * @param end path at given point. - * @param includeFlags setup allowed surfaces for actor to walk. + * @param includeFlags setup allowed navmesh areas. * @param out the beginning of the destination range. * @param endTolerance defines maximum allowed distance to end path point in addition to agentHalfExtents * @return Status. @@ -42,10 +42,10 @@ namespace DetourNavigator /** * @brief findRandomPointAroundCircle returns random location on navmesh within the reach of specified location. - * @param agentBounds allows to find navmesh for given actor. + * @param agentBounds defines which navmesh to use. * @param start is a position where the search starts. * @param maxRadius limit maximum distance from start. - * @param includeFlags setup allowed surfaces for actor to walk. + * @param includeFlags setup allowed navmesh areas. * @return not empty optional with position if point is found and empty optional if point is not found. */ std::optional findRandomPointAroundCircle(const Navigator& navigator, const AgentBounds& agentBounds, @@ -53,14 +53,25 @@ namespace DetourNavigator /** * @brief raycast finds farest navmesh point from start on a line from start to end that has path from start. - * @param agentBounds allows to find navmesh for given actor. + * @param agentBounds defines which navmesh to use. * @param start of the line * @param end of the line - * @param includeFlags setup allowed surfaces for actor to walk. + * @param includeFlags setup allowed navmesh areas. * @return not empty optional with position if point is found and empty optional if point is not found. */ std::optional raycast(const Navigator& navigator, const AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags); + + /** + * @brief findNearestNavMeshPosition finds nearest position on navmesh within given area having given flags. + * @param agentBounds defines which navmesh to use. + * @param position is a center of the search area. + * @param searchAreaHalfExtents defines AABB like area around given postion. + * @param includeFlags setup allowed navmesh areas. + * @return not empty optional with position if position is found and empty optional if position is not found. + */ + std::optional findNearestNavMeshPosition(const Navigator& navigator, const AgentBounds& agentBounds, + const osg::Vec3f& position, const osg::Vec3f& searchAreaHalfExtents, const Flags includeFlags); } #endif diff --git a/files/lua_api/openmw/nearby.lua b/files/lua_api/openmw/nearby.lua index 2e9abb06f6..70b09efd90 100644 --- a/files/lua_api/openmw/nearby.lua +++ b/files/lua_api/openmw/nearby.lua @@ -182,6 +182,17 @@ -- values (default: @{#NAVIGATOR_FLAGS.Walk} + @{#NAVIGATOR_FLAGS.Swim} + @{#NAVIGATOR_FLAGS.OpenDoor} -- + @{#NAVIGATOR_FLAGS.UsePathgrid}). +--- +-- A table of parameters for @{#nearby.findNearestNavMeshPosition} +-- @type FindNearestNavMeshPositionOptions +-- @field [parent=#NavMeshOptions] #AgentBounds agentBounds Identifies which navmesh to use. +-- @field [parent=#NavMeshOptions] #number includeFlags Allowed areas for agent to move, a sum of @{#NAVIGATOR_FLAGS} +-- values (default: @{#NAVIGATOR_FLAGS.Walk} + @{#NAVIGATOR_FLAGS.Swim} + @{#NAVIGATOR_FLAGS.OpenDoor} +-- + @{#NAVIGATOR_FLAGS.UsePathgrid}). +-- @field [parent=#NavMeshOptions] openmw.util#Vector3 searchAreaHalfExtents Defines AABB like area half extents around +-- given position (default: (1 + 2 * CellGridRadius) * CellSize * (1, 1, 1) where CellGridRadius and depends on cell +-- type to cover the whole active grid). + --- -- Find path over navigation mesh from source to destination with given options. Result is unstable since navigation -- mesh generation is asynchronous. @@ -234,4 +245,22 @@ -- agentBounds = Actor.getPathfindingAgentBounds(self), -- }) +--- +-- Finds a nearest position on navigation mesh to the given position within given search area. +-- @function [parent=#nearby] findNearestNavMeshPosition +-- @param openmw.util#Vector3 position Search area center. +-- @param #FindNearestNavMeshPositionOptions options An optional table with additional optional arguments. +-- @return openmw.util#Vector3, #nil +-- @usage local navMeshPosition = nearby.findNearestNavMeshPosition(position) +-- @usage local navMeshPosition = nearby.findNearestNavMeshPosition(position, { +-- includeFlags = nearby.NAVIGATOR_FLAGS.Swim, +-- }) +-- @usage local navMeshPosition = nearby.findNearestNavMeshPosition(position, { +-- agentBounds = Actor.getPathfindingAgentBounds(self), +-- }) +-- @usage local navMeshPosition = nearby.findNearestNavMeshPosition(position, { +-- searchAreaHalfExtents = util.vector3(1000, 1000, 1000), +-- includeFlags = nearby.NAVIGATOR_FLAGS.Walk, +-- }) + return nil diff --git a/scripts/data/integration_tests/test_lua_api/player.lua b/scripts/data/integration_tests/test_lua_api/player.lua index 93c16a8b88..41022828f9 100644 --- a/scripts/data/integration_tests/test_lua_api/player.lua +++ b/scripts/data/integration_tests/test_lua_api/player.lua @@ -82,7 +82,8 @@ testing.registerLocalTest('findPath', } local status, path = nearby.findPath(src, dst, options) testing.expectEqual(status, nearby.FIND_PATH_STATUS.Success, 'Status') - testing.expectLessOrEqual((path[path:size()] - dst):length(), 1, 'Last path point') + testing.expectLessOrEqual((path[path:size()] - dst):length(), 1, + 'Last path point ' .. testing.formatActualExpected(path[path:size()], dst)) end) testing.registerLocalTest('findRandomPointAroundCircle', @@ -94,7 +95,8 @@ testing.registerLocalTest('findRandomPointAroundCircle', includeFlags = nearby.NAVIGATOR_FLAGS.Walk, } local result = nearby.findRandomPointAroundCircle(position, maxRadius, options) - testing.expectGreaterThan((result - position):length(), 1, 'Random point') + testing.expectGreaterThan((result - position):length(), 1, + 'Random point ' .. testing.formatActualExpected(result, position)) end) testing.registerLocalTest('castNavigationRay', @@ -106,7 +108,22 @@ testing.registerLocalTest('castNavigationRay', includeFlags = nearby.NAVIGATOR_FLAGS.Walk + nearby.NAVIGATOR_FLAGS.Swim, } local result = nearby.castNavigationRay(src, dst, options) - testing.expectLessOrEqual((result - dst):length(), 1, 'Navigation hit point') + testing.expectLessOrEqual((result - dst):length(), 1, + 'Navigation hit point ' .. testing.formatActualExpected(result, dst)) + end) + +testing.registerLocalTest('findNearestNavMeshPosition', + function() + local position = util.vector3(4096, 4096, 1000) + local options = { + agentBounds = types.Actor.getPathfindingAgentBounds(self), + includeFlags = nearby.NAVIGATOR_FLAGS.Walk + nearby.NAVIGATOR_FLAGS.Swim, + searchAreaHalfExtents = util.vector3(1000, 1000, 1000), + } + local result = nearby.findNearestNavMeshPosition(position, options) + local expected = util.vector3(4096, 4096, 872.674) + testing.expectLessOrEqual((result - expected):length(), 1, + 'Navigation mesh position ' .. testing.formatActualExpected(result, expected)) end) return { diff --git a/scripts/data/integration_tests/test_lua_api/test.lua b/scripts/data/integration_tests/test_lua_api/test.lua index acc43eca2a..2ec9f09b97 100644 --- a/scripts/data/integration_tests/test_lua_api/test.lua +++ b/scripts/data/integration_tests/test_lua_api/test.lua @@ -95,6 +95,10 @@ tests = { initPlayer() testing.runLocalTest(player, 'castNavigationRay') end}, + {'findNearestNavMeshPosition', function() + initPlayer() + testing.runLocalTest(player, 'findNearestNavMeshPosition') + end}, {'teleport', testTeleport}, {'getGMST', testGetGMST}, } diff --git a/scripts/data/integration_tests/testing_util/testing_util.lua b/scripts/data/integration_tests/testing_util/testing_util.lua index db67cd3b1a..f73ea83e79 100644 --- a/scripts/data/integration_tests/testing_util/testing_util.lua +++ b/scripts/data/integration_tests/testing_util/testing_util.lua @@ -154,6 +154,10 @@ function M.expectThat(value, matcher, msg) end end +function M.formatActualExpected(actual, expected) + return string.format('actual: %s, expected: %s', actual, expected) +end + local localTests = {} local localTestRunner = nil