diff --git a/apps/components_tests/detournavigator/navigator.cpp b/apps/components_tests/detournavigator/navigator.cpp index 0a78c33c9e..a6e1d26c79 100644 --- a/apps/components_tests/detournavigator/navigator.cpp +++ b/apps/components_tests/detournavigator/navigator.cpp @@ -139,7 +139,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, find_path_for_empty_should_return_empty) { - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::NavMeshNotFound); EXPECT_EQ(mPath, std::deque()); } @@ -147,7 +147,7 @@ namespace TEST_F(DetourNavigatorNavigatorTest, find_path_for_existing_agent_with_no_navmesh_should_throw_exception) { ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::StartPolygonNotFound); } @@ -156,7 +156,7 @@ namespace ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); mNavigator->removeAgent(mAgentBounds); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::StartPolygonNotFound); } @@ -172,7 +172,7 @@ namespace updateGuard.reset(); mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -194,7 +194,7 @@ namespace updateGuard.reset(); mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mStart, Flag_walk, mAreaCosts, mEndTolerance, mOut), + 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; @@ -218,7 +218,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -237,7 +237,7 @@ namespace mPath.clear(); mOut = std::back_inserter(mPath); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -265,7 +265,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -285,7 +285,7 @@ namespace mPath.clear(); mOut = std::back_inserter(mPath); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -318,7 +318,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -386,7 +386,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -421,7 +421,7 @@ namespace mEnd.x() = 256; mEnd.z() = 300; - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -453,8 +453,8 @@ namespace mStart.x() = 256; mEnd.x() = 256; - EXPECT_EQ( - findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, + {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -487,8 +487,8 @@ namespace mStart.x() = 256; mEnd.x() = 256; - EXPECT_EQ( - findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, + {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -520,7 +520,7 @@ namespace mStart.x() = 256; mEnd.x() = 256; - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -549,7 +549,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -577,7 +577,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -658,7 +658,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -781,7 +781,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -806,7 +806,7 @@ namespace mNavigator->update(mPlayerPosition, nullptr); mNavigator->wait(WaitConditionType::allJobsDone, &mListener); - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), Status::PartialPath); EXPECT_THAT(mPath, @@ -834,7 +834,7 @@ namespace const float endTolerance = 1000.0f; - EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, endTolerance, mOut), + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, endTolerance, {}, mOut), Status::Success); EXPECT_THAT(mPath, @@ -979,6 +979,146 @@ namespace EXPECT_EQ(usedNavMeshTiles, 854); } + TEST_F(DetourNavigatorNavigatorTest, find_path_should_return_path_around_steep_mountains) + { + const std::array heightfieldData{ { + 0, 0, 0, 0, 0, // row 0 + 0, 0, 0, 0, 0, // row 1 + 0, 0, 1000, 0, 0, // row 2 + 0, 0, 0, 0, 0, // row 3 + 0, 0, 0, 0, 0, // row 4 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const osg::Vec3f start(56, 56, 12); + const osg::Vec3f end(464, 464, 12); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 56.66664886474609375, 11.33333301544189453125), + Vec3fEq(396.666656494140625, 79.33331298828125, 11.33333301544189453125), + Vec3fEq(430.666656494140625, 113.33331298828125, 11.33333301544189453125), + Vec3fEq(463.999969482421875, 463.999969482421875, 11.33333301544189453125))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, find_path_should_return_path_around_steep_cliffs) + { + const std::array heightfieldData{ { + 0, 0, 0, 0, 0, // row 0 + 0, 0, 0, 0, 0, // row 1 + 0, 0, -1000, 0, 0, // row 2 + 0, 0, 0, 0, 0, // row 3 + 0, 0, 0, 0, 0, // row 4 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const osg::Vec3f start(56, 56, 12); + const osg::Vec3f end(464, 464, 12); + + EXPECT_EQ(findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 56.66664886474609375, 8.66659259796142578125), + Vec3fEq(385.33331298828125, 79.33331298828125, 8.66659259796142578125), + Vec3fEq(430.666656494140625, 124.66664886474609375, 8.66659259796142578125), + Vec3fEq(463.999969482421875, 463.999969482421875, 8.66659259796142578125))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, find_path_should_return_path_with_checkpoints) + { + const std::array heightfieldData{ { + 0, 0, 0, 0, 0, // row 0 + 0, 0, 0, 0, 0, // row 1 + 0, 0, 1000, 0, 0, // row 2 + 0, 0, 0, 0, 0, // row 3 + 0, 0, 0, 0, 0, // row 4 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const std::vector checkpoints = { + osg::Vec3f(400, 70, 12), + }; + + const osg::Vec3f start(56, 56, 12); + const osg::Vec3f end(464, 464, 12); + + EXPECT_EQ( + findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, checkpoints, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 56.66664886474609375, 11.33333301544189453125), + Vec3fEq(400, 70, 11.33333301544189453125), + Vec3fEq(430.666656494140625, 113.33331298828125, 11.33333301544189453125), + Vec3fEq(463.999969482421875, 463.999969482421875, 11.33333301544189453125))) + << mPath; + } + + TEST_F(DetourNavigatorNavigatorTest, find_path_should_skip_unreachable_checkpoints) + { + const std::array heightfieldData{ { + 0, 0, 0, 0, 0, // row 0 + 0, 0, 0, 0, 0, // row 1 + 0, 0, 1000, 0, 0, // row 2 + 0, 0, 0, 0, 0, // row 3 + 0, 0, 0, 0, 0, // row 4 + } }; + const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData); + const int cellSize = heightfieldTileSize * static_cast(surface.mSize - 1); + + ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); + mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr); + mNavigator->update(mPlayerPosition, nullptr); + mNavigator->wait(WaitConditionType::allJobsDone, &mListener); + + const std::vector checkpoints = { + osg::Vec3f(400, 70, 10000), + osg::Vec3f(256, 256, 1000), + osg::Vec3f(-1000, -1000, 0), + }; + + const osg::Vec3f start(56, 56, 12); + const osg::Vec3f end(464, 464, 12); + + EXPECT_EQ( + findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, checkpoints, mOut), + Status::Success); + + EXPECT_THAT(mPath, + ElementsAre( // + Vec3fEq(56.66664886474609375, 56.66664886474609375, 11.33333301544189453125), + Vec3fEq(396.666656494140625, 79.33331298828125, 11.33333301544189453125), + Vec3fEq(430.666656494140625, 113.33331298828125, 11.33333301544189453125), + Vec3fEq(463.999969482421875, 463.999969482421875, 11.33333301544189453125))) + << mPath; + } + struct DetourNavigatorNavigatorNotSupportedAgentBoundsTest : TestWithParam { }; diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 956a038f11..deb9140320 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -589,8 +589,7 @@ namespace MWBase virtual bool hasCollisionWithDoor( const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0; - virtual bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, - std::span ignore, std::vector* occupyingActors = nullptr) const = 0; + virtual bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const = 0; virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; diff --git a/apps/openmw/mwlua/nearbybindings.cpp b/apps/openmw/mwlua/nearbybindings.cpp index a6d762499a..6c244a0fd4 100644 --- a/apps/openmw/mwlua/nearbybindings.cpp +++ b/apps/openmw/mwlua/nearbybindings.cpp @@ -233,6 +233,7 @@ namespace MWLua DetourNavigator::Flags includeFlags = defaultIncludeFlags; DetourNavigator::AreaCosts areaCosts{}; float destinationTolerance = 1; + std::vector checkpoints; if (options.has_value()) { @@ -258,13 +259,24 @@ namespace MWLua } if (const auto& v = options->get>("destinationTolerance")) destinationTolerance = *v; + if (const auto& t = options->get>("checkpoints")) + { + for (const auto& [k, v] : *t) + { + const int index = k.as(); + const osg::Vec3f position = v.as(); + if (index != static_cast(checkpoints.size() + 1)) + throw std::runtime_error("checkpoints is not an array"); + checkpoints.push_back(position); + } + } } std::vector path; - const DetourNavigator::Status status - = DetourNavigator::findPath(*MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, - source, destination, includeFlags, areaCosts, destinationTolerance, std::back_inserter(path)); + const DetourNavigator::Status status = DetourNavigator::findPath( + *MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, source, destination, + includeFlags, areaCosts, destinationTolerance, checkpoints, std::back_inserter(path)); sol::table result(lua, sol::create); LuaUtil::copyVectorToTable(path, result); diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 7b7148f1de..cae4d57b39 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -1,13 +1,11 @@ #include "aicombat.hpp" -#include -#include - -#include - -#include - #include +#include +#include +#include +#include +#include #include #include "../mwphysics/raycasting.hpp" @@ -303,8 +301,8 @@ namespace MWMechanics const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); const ESM::Pathgrid* pathgrid = world->getStore().get().search(*actor.getCell()->getCell()); const auto& pathGridGraph = getPathGridGraph(pathgrid); - mPathFinder.buildPath(actor, vActorPos, vTargetPos, actor.getCell(), pathGridGraph, agentBounds, - navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full); + mPathFinder.buildPath(actor, vActorPos, vTargetPos, pathGridGraph, agentBounds, navigatorFlags, areaCosts, + storage.mAttackRange, PathType::Full); if (!mPathFinder.isPathConstructed()) { @@ -317,8 +315,8 @@ namespace MWMechanics if (hit.has_value() && (*hit - vTargetPos).length() <= rangeAttack) { // If the point is close enough, try to find a path to that point. - mPathFinder.buildPath(actor, vActorPos, *hit, actor.getCell(), pathGridGraph, agentBounds, - navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full); + mPathFinder.buildPath(actor, vActorPos, *hit, pathGridGraph, agentBounds, navigatorFlags, areaCosts, + storage.mAttackRange, PathType::Full); if (mPathFinder.isPathConstructed()) { // If path to that point is found use it as custom destination. @@ -394,8 +392,8 @@ namespace MWMechanics osg::Vec3f localPos = actor.getRefData().getPosition().asVec3(); coords.toLocal(localPos); - size_t closestPointIndex = PathFinder::getClosestPoint(pathgrid, localPos); - for (size_t i = 0; i < pathgrid->mPoints.size(); i++) + const std::size_t closestPointIndex = Misc::getClosestPoint(*pathgrid, localPos); + for (std::size_t i = 0; i < pathgrid->mPoints.size(); i++) { if (i != closestPointIndex && getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, i)) @@ -456,7 +454,8 @@ namespace MWMechanics float dist = (actor.getRefData().getPosition().asVec3() - target.getRefData().getPosition().asVec3()).length(); if ((dist > fFleeDistance && !storage.mLOS) - || pathTo(actor, PathFinder::makeOsgVec3(storage.mFleeDest), duration, supportedMovementDirections)) + || pathTo( + actor, Misc::Convert::makeOsgVec3f(storage.mFleeDest), duration, supportedMovementDirections)) { state = AiCombatStorage::FleeState_Idle; } diff --git a/apps/openmw/mwmechanics/aicombat.hpp b/apps/openmw/mwmechanics/aicombat.hpp index d5a9c3464c..42baaf6349 100644 --- a/apps/openmw/mwmechanics/aicombat.hpp +++ b/apps/openmw/mwmechanics/aicombat.hpp @@ -2,12 +2,13 @@ #define GAME_MWMECHANICS_AICOMBAT_H #include "aitemporarybase.hpp" +#include "aitimer.hpp" +#include "movement.hpp" #include "typedaipackage.hpp" #include "../mwworld/cellstore.hpp" // for Doors -#include "aitimer.hpp" -#include "movement.hpp" +#include namespace ESM { diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 4bcfc7dedd..3fcb28307c 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -180,8 +180,8 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& = world->getStore().get().search(*actor.getCell()->getCell()); const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); - mPathFinder.buildLimitedPath(actor, position, dest, actor.getCell(), getPathGridGraph(pathgrid), - agentBounds, navigatorFlags, areaCosts, endTolerance, pathType); + mPathFinder.buildLimitedPath(actor, position, dest, getPathGridGraph(pathgrid), agentBounds, + navigatorFlags, areaCosts, endTolerance, pathType); mRotateOnTheRunChecks = 3; // give priority to go directly on target if there is minimal opportunity @@ -501,7 +501,11 @@ DetourNavigator::Flags MWMechanics::AiPackage::getNavigatorFlags(const MWWorld:: result |= DetourNavigator::Flag_swim; if (actorClass.canWalk(actor) && actor.getClass().getWalkSpeed(actor) > 0) - result |= DetourNavigator::Flag_walk | DetourNavigator::Flag_usePathgrid; + { + result |= DetourNavigator::Flag_walk; + if (getTypeId() != AiPackageTypeId::Wander) + result |= DetourNavigator::Flag_usePathgrid; + } if (canOpenDoors(actor) && getTypeId() != AiPackageTypeId::Wander) result |= DetourNavigator::Flag_openDoor; diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index edb62c97c4..42aa62ffe3 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -16,6 +16,8 @@ namespace ESM { struct Cell; + struct Pathgrid; + namespace AiSequence { struct AiSequence; diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 3cc7aac838..71cc8b5e7d 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "../mwbase/environment.hpp" @@ -29,17 +30,6 @@ namespace MWMechanics { - static const int COUNT_BEFORE_RESET = 10; - static const float IDLE_POSITION_CHECK_INTERVAL = 1.5f; - - // to prevent overcrowding - static const int DESTINATION_TOLERANCE = 64; - - // distance must be long enough that NPC will need to move to get there. - static const int MINIMUM_WANDER_DISTANCE = DESTINATION_TOLERANCE * 2; - - static const std::size_t MAX_IDLE_SIZE = 8; - const std::string_view AiWander::sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1] = { "idle2", "idle3", @@ -53,11 +43,22 @@ namespace MWMechanics namespace { + constexpr int countBeforeReset = 10; + constexpr float idlePositionCheckInterval = 1.5f; + + // to prevent overcrowding + constexpr unsigned destinationTolerance = 64; + + // distance must be long enough that NPC will need to move to get there. + constexpr unsigned minimumWanderDistance = destinationTolerance * 2; + + constexpr std::size_t maxIdleSize = 8; + inline int getCountBeforeReset(const MWWorld::ConstPtr& actor) { if (actor.getClass().isPureWaterCreature(actor) || actor.getClass().isPureFlyingCreature(actor)) return 1; - return COUNT_BEFORE_RESET; + return countBeforeReset; } osg::Vec3f getRandomPointAround(const osg::Vec3f& position, const float distance) @@ -99,16 +100,42 @@ namespace MWMechanics std::vector getInitialIdle(const std::vector& idle) { - std::vector result(MAX_IDLE_SIZE, 0); - std::copy_n(idle.begin(), std::min(MAX_IDLE_SIZE, idle.size()), result.begin()); + std::vector result(maxIdleSize, 0); + std::copy_n(idle.begin(), std::min(maxIdleSize, idle.size()), result.begin()); return result; } - std::vector getInitialIdle(const unsigned char (&idle)[MAX_IDLE_SIZE]) + std::vector getInitialIdle(const unsigned char (&idle)[maxIdleSize]) { return std::vector(std::begin(idle), std::end(idle)); } + void trimAllowedPositions(const std::deque& path, std::vector& allowedPositions) + { + // TODO: how to add these back in once the door opens? + // Idea: keep a list of detected closed doors (see aicombat.cpp) + // Every now and then check whether one of the doors is opened. (maybe + // at the end of playing idle?) If the door is opened then re-calculate + // allowed positions starting from the spawn point. + std::vector points(path.begin(), path.end()); + while (points.size() >= 2) + { + const osg::Vec3f point = points.back(); + for (std::size_t j = 0; j < allowedPositions.size(); j++) + { + // FIXME: doesn't handle a door with the same X/Y + // coordinates but with a different Z + if (std::abs(allowedPositions[j].x() - point.x()) <= 0.5 + && std::abs(allowedPositions[j].y() - point.y()) <= 0.5) + { + allowedPositions.erase(allowedPositions.begin() + j); + break; + } + } + points.pop_back(); + } + } + } AiWanderStorage::AiWanderStorage() @@ -118,9 +145,9 @@ namespace MWMechanics , mCanWanderAlongPathGrid(true) , mIdleAnimation(0) , mBadIdles() - , mPopulateAvailableNodes(true) - , mAllowedNodes() - , mTrimCurrentNode(false) + , mPopulateAvailablePositions(true) + , mAllowedPositions() + , mTrimCurrentPosition(false) , mCheckIdlePositionTimer(0) , mStuckCount(0) { @@ -128,8 +155,8 @@ namespace MWMechanics AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat) : TypedAiPackage(repeat) - , mDistance(std::max(0, distance)) - , mDuration(std::max(0, duration)) + , mDistance(static_cast(std::max(0, distance))) + , mDuration(static_cast(std::max(0, duration))) , mRemainingDuration(duration) , mTimeOfDay(timeOfDay) , mIdle(getInitialIdle(idle)) @@ -215,20 +242,12 @@ namespace MWMechanics { const ESM::Pathgrid* pathgrid = MWBase::Environment::get().getESMStore()->get().search(*actor.getCell()->getCell()); - if (mUsePathgrid) - { - mPathFinder.buildPathByPathgrid( - pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(pathgrid)); - } - else - { - const auto agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor); - constexpr float endTolerance = 0; - const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); - const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); - mPathFinder.buildPath(actor, pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(pathgrid), - agentBounds, navigatorFlags, areaCosts, endTolerance, PathType::Full); - } + const auto agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor); + constexpr float endTolerance = 0; + const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); + const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); + mPathFinder.buildPath(actor, pos.asVec3(), mDestination, getPathGridGraph(pathgrid), agentBounds, + navigatorFlags, areaCosts, endTolerance, PathType::Full); if (mPathFinder.isPathConstructed()) storage.setState(AiWanderStorage::Wander_Walking, !mUsePathgrid); @@ -259,9 +278,6 @@ namespace MWMechanics bool AiWander::reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos) { - if (mDistance <= 0) - storage.mCanWanderAlongPathGrid = false; - if (isPackageCompleted()) { stopWalking(actor); @@ -276,13 +292,15 @@ namespace MWMechanics mStoredInitialActorPosition = true; } - // Initialization to discover & store allowed node points for this actor. - if (storage.mPopulateAvailableNodes) + // Initialization to discover & store allowed positions points for this actor. + if (storage.mPopulateAvailablePositions) { - getAllowedNodes(actor, storage); + fillAllowedPositions(actor, storage); } - auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + MWBase::World& world = *MWBase::Environment::get().getWorld(); + + auto& prng = world.getPrng(); if (canActorMoveByZAxis(actor) && mDistance > 0) { // Typically want to idle for a short time before the next wander @@ -295,7 +313,7 @@ namespace MWMechanics } // If the package has a wander distance but no pathgrid is available, // randomly idle or wander near spawn point - else if (storage.mAllowedNodes.empty() && mDistance > 0 && !storage.mIsWanderingManually) + else if (storage.mAllowedPositions.empty() && mDistance > 0 && !storage.mIsWanderingManually) { // Typically want to idle for a short time before the next wander if (Misc::Rng::rollDice(100, prng) >= 96) @@ -307,7 +325,7 @@ namespace MWMechanics storage.setState(AiWanderStorage::Wander_IdleNow); } } - else if (storage.mAllowedNodes.empty() && !storage.mIsWanderingManually) + else if (storage.mAllowedPositions.empty() && !storage.mIsWanderingManually) { storage.mCanWanderAlongPathGrid = false; } @@ -323,9 +341,9 @@ namespace MWMechanics // Construct a new path if there isn't one if (!mPathFinder.isPathConstructed()) { - if (!storage.mAllowedNodes.empty()) + if (!storage.mAllowedPositions.empty()) { - setPathToAnAllowedNode(actor, storage, pos); + setPathToAnAllowedPosition(actor, storage, pos); } } } @@ -336,7 +354,7 @@ namespace MWMechanics if (storage.mIsWanderingManually && storage.mState == AiWanderStorage::Wander_Walking && (mPathFinder.getPathSize() == 0 || isDestinationHidden(actor, mPathFinder.getPath().back()) - || isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back()))) + || world.isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back()))) completeManualWalking(actor, storage); return false; // AiWander package not yet completed @@ -366,12 +384,12 @@ namespace MWMechanics std::size_t attempts = 10; // If a unit can't wander out of water, don't want to hang here const bool isWaterCreature = actor.getClass().isPureWaterCreature(actor); const bool isFlyingCreature = actor.getClass().isPureFlyingCreature(actor); - const auto world = MWBase::Environment::get().getWorld(); - const auto agentBounds = world->getPathfindingAgentBounds(actor); - const auto navigator = world->getNavigator(); + MWBase::World& world = *MWBase::Environment::get().getWorld(); + const auto agentBounds = world.getPathfindingAgentBounds(actor); + const auto navigator = world.getNavigator(); const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); - auto& prng = MWBase::Environment::get().getWorld()->getPrng(); + Misc::Rng::Generator& prng = world.getPrng(); do { @@ -411,7 +429,7 @@ namespace MWMechanics if (isDestinationHidden(actor, mDestination)) continue; - if (isAreaOccupiedByOtherActor(actor, mDestination)) + if (world.isAreaOccupiedByOtherActor(actor, mDestination)) continue; constexpr float endTolerance = 0; @@ -491,17 +509,17 @@ namespace MWMechanics void AiWander::onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage) { - // Check if an idle actor is too far from all allowed nodes or too close to a door - if so start walking. + // Check if an idle actor is too far from all allowed positions or too close to a door - if so start walking. storage.mCheckIdlePositionTimer += duration; - if (storage.mCheckIdlePositionTimer >= IDLE_POSITION_CHECK_INTERVAL && !isStationary()) + if (storage.mCheckIdlePositionTimer >= idlePositionCheckInterval && !isStationary()) { storage.mCheckIdlePositionTimer = 0; // restart timer static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 1.6f; - if (proximityToDoor(actor, distance) || !isNearAllowedNode(actor, storage, distance)) + if (proximityToDoor(actor, distance) || !isNearAllowedPosition(actor, storage, distance)) { storage.setState(AiWanderStorage::Wander_MoveNow); - storage.mTrimCurrentNode = false; // just in case + storage.mTrimCurrentPosition = false; // just in case return; } } @@ -517,16 +535,14 @@ namespace MWMechanics } } - bool AiWander::isNearAllowedNode(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const + bool AiWander::isNearAllowedPosition( + const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const { const osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); - for (const ESM::Pathgrid::Point& node : storage.mAllowedNodes) - { - osg::Vec3f point(node.mX, node.mY, node.mZ); - if ((actorPos - point).length2() < distance * distance) - return true; - } - return false; + const float squaredDistance = distance * distance; + return std::ranges::find_if(storage.mAllowedPositions, [&](const osg::Vec3& v) { + return (actorPos - v).length2() < squaredDistance; + }) != storage.mAllowedPositions.end(); } void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, @@ -535,7 +551,7 @@ namespace MWMechanics // Is there no destination or are we there yet? if ((!mPathFinder.isPathConstructed()) || pathTo(actor, osg::Vec3f(mPathFinder.getPath().back()), duration, supportedMovementDirections, - DESTINATION_TOLERANCE)) + destinationTolerance)) { stopWalking(actor); storage.setState(AiWanderStorage::Wander_ChooseAction); @@ -586,8 +602,8 @@ namespace MWMechanics if (proximityToDoor(actor, distance)) { // remove allowed points then select another random destination - storage.mTrimCurrentNode = true; - trimAllowedNodes(storage.mAllowedNodes, mPathFinder); + storage.mTrimCurrentPosition = true; + trimAllowedPositions(mPathFinder.getPath(), storage.mAllowedPositions); mObstacleCheck.clear(); stopWalking(actor); storage.setState(AiWanderStorage::Wander_MoveNow); @@ -606,67 +622,67 @@ namespace MWMechanics } } - void AiWander::setPathToAnAllowedNode( + void AiWander::setPathToAnAllowedPosition( const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos) { - auto world = MWBase::Environment::get().getWorld(); - auto& prng = world->getPrng(); - unsigned int randNode = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng); - const ESM::Pathgrid::Point& dest = storage.mAllowedNodes[randNode]; + MWBase::World& world = *MWBase::Environment::get().getWorld(); + Misc::Rng::Generator& prng = world.getPrng(); + const std::size_t randomAllowedPositionIndex + = static_cast(Misc::Rng::rollDice(storage.mAllowedPositions.size(), prng)); + const osg::Vec3f randomAllowedPosition = storage.mAllowedPositions[randomAllowedPositionIndex]; const osg::Vec3f start = actorPos.asVec3(); - // don't take shortcuts for wandering - const ESM::Pathgrid* pathgrid = world->getStore().get().search(*actor.getCell()->getCell()); - const osg::Vec3f destVec3f = PathFinder::makeOsgVec3(dest); - mPathFinder.buildPathByPathgrid(start, destVec3f, actor.getCell(), getPathGridGraph(pathgrid)); + const MWWorld::Cell& cell = *actor.getCell()->getCell(); + const ESM::Pathgrid* pathgrid = world.getStore().get().search(cell); + const PathgridGraph& pathgridGraph = getPathGridGraph(pathgrid); - if (mPathFinder.isPathConstructed()) + const Misc::CoordinateConverter converter = Misc::makeCoordinateConverter(cell); + std::deque path + = pathgridGraph.aStarSearch(Misc::getClosestPoint(*pathgrid, converter.toLocalVec3(start)), + Misc::getClosestPoint(*pathgrid, converter.toLocalVec3(randomAllowedPosition))); + + // Choose a different position and delete this one from possible positions because it is uncreachable: + if (path.empty()) { - mDestination = destVec3f; - mHasDestination = true; - mUsePathgrid = true; - // Remove this node as an option and add back the previously used node (stops NPC from picking the same - // node): - ESM::Pathgrid::Point temp = storage.mAllowedNodes[randNode]; - storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode); - // check if mCurrentNode was taken out of mAllowedNodes - if (storage.mTrimCurrentNode && storage.mAllowedNodes.size() > 1) - storage.mTrimCurrentNode = false; - else - storage.mAllowedNodes.push_back(storage.mCurrentNode); - storage.mCurrentNode = temp; - - storage.setState(AiWanderStorage::Wander_Walking); + storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex); + return; } - // Choose a different node and delete this one from possible nodes because it is uncreachable: + + // Drop nearest pathgrid point. + path.pop_front(); + + std::vector checkpoints(path.size()); + for (std::size_t i = 0; i < path.size(); ++i) + checkpoints[i] = Misc::Convert::makeOsgVec3f(converter.toWorldPoint(path[i])); + + const DetourNavigator::AgentBounds agentBounds = world.getPathfindingAgentBounds(actor); + const DetourNavigator::Flags flags = getNavigatorFlags(actor); + const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, flags); + constexpr float endTolerance = 0; + mPathFinder.buildPath(actor, start, randomAllowedPosition, pathgridGraph, agentBounds, flags, areaCosts, + endTolerance, PathType::Full, checkpoints); + + if (!mPathFinder.isPathConstructed()) + { + storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex); + return; + } + + mDestination = randomAllowedPosition; + mHasDestination = true; + mUsePathgrid = true; + // Remove this position as an option and add back the previously used position (stops NPC from picking the + // same position): + storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex); + // check if mCurrentPosition was taken out of mAllowedPositions + if (storage.mTrimCurrentPosition && storage.mAllowedPositions.size() > 1) + storage.mTrimCurrentPosition = false; else - storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode); - } + storage.mAllowedPositions.push_back(storage.mCurrentPosition); + storage.mCurrentPosition = randomAllowedPosition; - void AiWander::trimAllowedNodes(std::vector& nodes, const PathFinder& pathfinder) - { - // TODO: how to add these back in once the door opens? - // Idea: keep a list of detected closed doors (see aicombat.cpp) - // Every now and then check whether one of the doors is opened. (maybe - // at the end of playing idle?) If the door is opened then re-calculate - // allowed nodes starting from the spawn point. - auto paths = pathfinder.getPath(); - while (paths.size() >= 2) - { - const auto pt = paths.back(); - for (unsigned int j = 0; j < nodes.size(); j++) - { - // FIXME: doesn't handle a door with the same X/Y - // coordinates but with a different Z - if (std::abs(nodes[j].mX - pt.x()) <= 0.5 && std::abs(nodes[j].mY - pt.y()) <= 0.5) - { - nodes.erase(nodes.begin() + j); - break; - } - } - paths.pop_back(); - } + storage.setState(AiWanderStorage::Wander_Walking); } void AiWander::stopWalking(const MWWorld::Ptr& actor) @@ -742,20 +758,20 @@ namespace MWMechanics return; AiWanderStorage& storage = state.get(); - if (storage.mPopulateAvailableNodes) - getAllowedNodes(actor, storage); + if (storage.mPopulateAvailablePositions) + fillAllowedPositions(actor, storage); - if (storage.mAllowedNodes.empty()) + if (storage.mAllowedPositions.empty()) return; auto& prng = MWBase::Environment::get().getWorld()->getPrng(); - int index = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng); - ESM::Pathgrid::Point worldDest = storage.mAllowedNodes[index]; + int index = Misc::Rng::rollDice(storage.mAllowedPositions.size(), prng); + const osg::Vec3f worldDest = storage.mAllowedPositions[index]; const Misc::CoordinateConverter converter = Misc::makeCoordinateConverter(*actor.getCell()->getCell()); - ESM::Pathgrid::Point dest = converter.toLocalPoint(worldDest); + osg::Vec3f dest = converter.toLocalVec3(worldDest); - bool isPathGridOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange( - PathFinder::makeOsgVec3(worldDest), 60); + const bool isPathGridOccupied + = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(worldDest, 60); // add offset only if the selected pathgrid is occupied by another actor if (isPathGridOccupied) @@ -775,19 +791,17 @@ namespace MWMechanics const ESM::Pathgrid::Point& connDest = points[randomIndex]; // add an offset towards random neighboring node - osg::Vec3f dir = PathFinder::makeOsgVec3(connDest) - PathFinder::makeOsgVec3(dest); - float length = dir.length(); + osg::Vec3f dir = Misc::Convert::makeOsgVec3f(connDest) - dest; + const float length = dir.length(); dir.normalize(); for (int j = 1; j <= 3; j++) { // move for 5-15% towards random neighboring node - dest - = PathFinder::makePathgridPoint(PathFinder::makeOsgVec3(dest) + dir * (j * 5 * length / 100.f)); - worldDest = converter.toWorldPoint(dest); + dest = dest + dir * (j * 5 * length / 100.f); isOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange( - PathFinder::makeOsgVec3(worldDest), 60); + converter.toWorldVec3(dest), 60); if (!isOccupied) break; @@ -807,19 +821,18 @@ namespace MWMechanics // place above to prevent moving inside objects, e.g. stairs, because a vector between pathgrids can be // underground. Adding 20 in adjustPosition() is not enough. - dest.mZ += 60; + dest.z() += 60; converter.toWorld(dest); state.reset(); - osg::Vec3f pos(static_cast(dest.mX), static_cast(dest.mY), static_cast(dest.mZ)); - MWBase::Environment::get().getWorld()->moveObject(actor, pos); + MWBase::Environment::get().getWorld()->moveObject(actor, dest); actor.getClass().adjustPosition(actor, false); } void AiWander::getNeighbouringNodes( - ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points) + const osg::Vec3f& dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points) { const ESM::Pathgrid* pathgrid = MWBase::Environment::get().getESMStore()->get().search(*currentCell->getCell()); @@ -827,19 +840,19 @@ namespace MWMechanics if (pathgrid == nullptr || pathgrid->mPoints.empty()) return; - size_t index = PathFinder::getClosestPoint(pathgrid, PathFinder::makeOsgVec3(dest)); + const size_t index = Misc::getClosestPoint(*pathgrid, dest); getPathGridGraph(pathgrid).getNeighbouringPoints(index, points); } - void AiWander::getAllowedNodes(const MWWorld::Ptr& actor, AiWanderStorage& storage) + void AiWander::fillAllowedPositions(const MWWorld::Ptr& actor, AiWanderStorage& storage) { // infrequently used, therefore no benefit in caching it as a member const MWWorld::CellStore* cellStore = actor.getCell(); const ESM::Pathgrid* pathgrid = MWBase::Environment::get().getESMStore()->get().search(*cellStore->getCell()); - storage.mAllowedNodes.clear(); + storage.mAllowedPositions.clear(); // If there is no path this actor doesn't go anywhere. See: // https://forum.openmw.org/viewtopic.php?t=1556 @@ -860,34 +873,35 @@ namespace MWMechanics const osg::Vec3f npcPos = converter.toLocalVec3(mInitialActorPosition); // Find closest pathgrid point - size_t closestPointIndex = PathFinder::getClosestPoint(pathgrid, npcPos); + const std::size_t closestPointIndex = Misc::getClosestPoint(*pathgrid, npcPos); - // mAllowedNodes for this actor with pathgrid point indexes based on mDistance + // mAllowedPositions for this actor with pathgrid point indexes based on mDistance // and if the point is connected to the closest current point // NOTE: mPoints is in local coordinates size_t pointIndex = 0; for (size_t counter = 0; counter < pathgrid->mPoints.size(); counter++) { - osg::Vec3f nodePos(PathFinder::makeOsgVec3(pathgrid->mPoints[counter])); + const osg::Vec3f nodePos = Misc::Convert::makeOsgVec3f(pathgrid->mPoints[counter]); if ((npcPos - nodePos).length2() <= mDistance * mDistance && getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, counter)) { - storage.mAllowedNodes.push_back(converter.toWorldPoint(pathgrid->mPoints[counter])); + storage.mAllowedPositions.push_back( + Misc::Convert::makeOsgVec3f(converter.toWorldPoint(pathgrid->mPoints[counter]))); pointIndex = counter; } } - if (storage.mAllowedNodes.size() == 1) + if (storage.mAllowedPositions.size() == 1) { - storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(mInitialActorPosition)); + storage.mAllowedPositions.push_back(mInitialActorPosition); addNonPathGridAllowedPoints(pathgrid, pointIndex, storage, converter); } - if (!storage.mAllowedNodes.empty()) + if (!storage.mAllowedPositions.empty()) { - setCurrentNodeToClosestAllowedNode(storage); + setCurrentPositionToClosestAllowedPosition(storage); } } - storage.mPopulateAvailableNodes = false; + storage.mPopulateAvailablePositions = false; } // When only one path grid point in wander distance, @@ -901,44 +915,44 @@ namespace MWMechanics { if (edge.mV0 == pointIndex) { - AddPointBetweenPathGridPoints(converter.toWorldPoint(pathGrid->mPoints[edge.mV0]), + addPositionBetweenPathgridPoints(converter.toWorldPoint(pathGrid->mPoints[edge.mV0]), converter.toWorldPoint(pathGrid->mPoints[edge.mV1]), storage); } } } - void AiWander::AddPointBetweenPathGridPoints( + void AiWander::addPositionBetweenPathgridPoints( const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage) { - osg::Vec3f vectorStart = PathFinder::makeOsgVec3(start); - osg::Vec3f delta = PathFinder::makeOsgVec3(end) - vectorStart; + osg::Vec3f vectorStart = Misc::Convert::makeOsgVec3f(start); + osg::Vec3f delta = Misc::Convert::makeOsgVec3f(end) - vectorStart; float length = delta.length(); delta.normalize(); - int distance = std::max(mDistance / 2, MINIMUM_WANDER_DISTANCE); + unsigned distance = std::max(mDistance / 2, minimumWanderDistance); // must not travel longer than distance between waypoints or NPC goes past waypoint - distance = std::min(distance, static_cast(length)); + distance = std::min(distance, static_cast(length)); delta *= distance; - storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(vectorStart + delta)); + storage.mAllowedPositions.push_back(vectorStart + delta); } - void AiWander::setCurrentNodeToClosestAllowedNode(AiWanderStorage& storage) + void AiWander::setCurrentPositionToClosestAllowedPosition(AiWanderStorage& storage) { - float distanceToClosestNode = std::numeric_limits::max(); + float distanceToClosestPosition = std::numeric_limits::max(); size_t index = 0; - for (size_t i = 0; i < storage.mAllowedNodes.size(); ++i) + for (size_t i = 0; i < storage.mAllowedPositions.size(); ++i) { - osg::Vec3f nodePos(PathFinder::makeOsgVec3(storage.mAllowedNodes[i])); - float tempDist = (mInitialActorPosition - nodePos).length2(); - if (tempDist < distanceToClosestNode) + const osg::Vec3f position = storage.mAllowedPositions[i]; + const float tempDist = (mInitialActorPosition - position).length2(); + if (tempDist < distanceToClosestPosition) { index = i; - distanceToClosestNode = tempDist; + distanceToClosestPosition = tempDist; } } - storage.mCurrentNode = storage.mAllowedNodes[index]; - storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + index); + storage.mCurrentPosition = storage.mAllowedPositions[index]; + storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + index); } void AiWander::writeState(ESM::AiSequence::AiSequence& sequence) const @@ -970,8 +984,8 @@ namespace MWMechanics AiWander::AiWander(const ESM::AiSequence::AiWander* wander) : TypedAiPackage(makeDefaultOptions().withRepeat(wander->mData.mShouldRepeat != 0)) - , mDistance(std::max(static_cast(0), wander->mData.mDistance)) - , mDuration(std::max(static_cast(0), wander->mData.mDuration)) + , mDistance(static_cast(std::max(static_cast(0), wander->mData.mDistance))) + , mDuration(static_cast(std::max(static_cast(0), wander->mData.mDuration))) , mRemainingDuration(wander->mDurationData.mRemainingDuration) , mTimeOfDay(wander->mData.mTimeOfDay) , mIdle(getInitialIdle(wander->mData.mIdle)) diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index f08980ad29..3e0b704524 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -1,14 +1,15 @@ #ifndef GAME_MWMECHANICS_AIWANDER_H #define GAME_MWMECHANICS_AIWANDER_H -#include "typedaipackage.hpp" - -#include -#include - #include "aitemporarybase.hpp" #include "aitimer.hpp" #include "pathfinding.hpp" +#include "typedaipackage.hpp" + +#include + +#include +#include namespace ESM { @@ -51,14 +52,13 @@ namespace MWMechanics unsigned short mIdleAnimation; std::vector mBadIdles; // Idle animations that when called cause errors - // do we need to calculate allowed nodes based on mDistance - bool mPopulateAvailableNodes; + bool mPopulateAvailablePositions; - // allowed pathgrid nodes based on mDistance from the spawn point - std::vector mAllowedNodes; + // allowed destination positions based on mDistance from the spawn point + std::vector mAllowedPositions; - ESM::Pathgrid::Point mCurrentNode; - bool mTrimCurrentNode; + osg::Vec3f mCurrentPosition; + bool mTrimCurrentPosition; float mCheckIdlePositionTimer; int mStuckCount; @@ -132,7 +132,8 @@ namespace MWMechanics bool playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); bool checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); int getRandomIdle() const; - void setPathToAnAllowedNode(const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos); + void setPathToAnAllowedPosition( + const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos); void evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage); void doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, MWWorld::MovementDirectionFlags supportedMovementDirections, AiWanderStorage& storage); @@ -145,28 +146,27 @@ namespace MWMechanics void wanderNearStart(const MWWorld::Ptr& actor, AiWanderStorage& storage, int wanderDistance); bool destinationIsAtWater(const MWWorld::Ptr& actor, const osg::Vec3f& destination); void completeManualWalking(const MWWorld::Ptr& actor, AiWanderStorage& storage); - bool isNearAllowedNode(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const; + bool isNearAllowedPosition(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const; - const int mDistance; // how far the actor can wander from the spawn point - const int mDuration; + // how far the actor can wander from the spawn point + const unsigned mDistance; + const unsigned mDuration; float mRemainingDuration; const int mTimeOfDay; const std::vector mIdle; bool mStoredInitialActorPosition; - osg::Vec3f - mInitialActorPosition; // Note: an original engine does not reset coordinates even when actor changes a cell + // Note: an original engine does not reset coordinates even when actor changes a cell + osg::Vec3f mInitialActorPosition; bool mHasDestination; osg::Vec3f mDestination; bool mUsePathgrid; void getNeighbouringNodes( - ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points); + const osg::Vec3f& dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points); - void getAllowedNodes(const MWWorld::Ptr& actor, AiWanderStorage& storage); - - void trimAllowedNodes(std::vector& nodes, const PathFinder& pathfinder); + void fillAllowedPositions(const MWWorld::Ptr& actor, AiWanderStorage& storage); // constants for converting idleSelect values into groupNames enum GroupIndex @@ -175,12 +175,12 @@ namespace MWMechanics GroupIndex_MaxIdle = 9 }; - void setCurrentNodeToClosestAllowedNode(AiWanderStorage& storage); + void setCurrentPositionToClosestAllowedPosition(AiWanderStorage& storage); void addNonPathGridAllowedPoints(const ESM::Pathgrid* pathGrid, size_t pointIndex, AiWanderStorage& storage, const Misc::CoordinateConverter& converter); - void AddPointBetweenPathGridPoints( + void addPositionBetweenPathgridPoints( const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage); /// lookup table for converting idleSelect value to groupName diff --git a/apps/openmw/mwmechanics/obstacle.cpp b/apps/openmw/mwmechanics/obstacle.cpp index a6eb4f9c24..9a66eafb51 100644 --- a/apps/openmw/mwmechanics/obstacle.cpp +++ b/apps/openmw/mwmechanics/obstacle.cpp @@ -106,21 +106,6 @@ namespace MWMechanics return visitor.mResult; } - bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, bool ignorePlayer, - std::vector* occupyingActors) - { - const auto world = MWBase::Environment::get().getWorld(); - const osg::Vec3f halfExtents = world->getPathfindingAgentBounds(actor).mHalfExtents; - const auto maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); - if (ignorePlayer) - { - const std::array ignore{ actor, world->getPlayerConstPtr() }; - return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore, occupyingActors); - } - const std::array ignore{ actor }; - return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore, occupyingActors); - } - ObstacleCheck::ObstacleCheck() : mEvadeDirectionIndex(std::size(evadeDirections) - 1) { diff --git a/apps/openmw/mwmechanics/obstacle.hpp b/apps/openmw/mwmechanics/obstacle.hpp index 532bc91331..a1c973765f 100644 --- a/apps/openmw/mwmechanics/obstacle.hpp +++ b/apps/openmw/mwmechanics/obstacle.hpp @@ -5,8 +5,6 @@ #include -#include - namespace MWWorld { class Ptr; @@ -24,9 +22,6 @@ namespace MWMechanics /** \return Pointer to the door, or empty pointer if none exists **/ const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist); - bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, - bool ignorePlayer = false, std::vector* occupyingActors = nullptr); - class ObstacleCheck { public: diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index dc9d8e4061..165250c5c8 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -38,7 +39,7 @@ namespace // points to a quadtree may help for (size_t counter = 0; counter < grid->mPoints.size(); counter++) { - float potentialDistBetween = MWMechanics::PathFinder::distanceSquared(grid->mPoints[counter], pos); + float potentialDistBetween = Misc::distanceSquared(grid->mPoints[counter], pos); if (potentialDistBetween < closestDistanceReachable) { // found a closer one @@ -197,7 +198,7 @@ namespace MWMechanics // point right behind the wall that is closer than any pathgrid // point outside the wall osg::Vec3f startPointInLocalCoords(converter.toLocalVec3(startPoint)); - size_t startNode = getClosestPoint(pathgrid, startPointInLocalCoords); + const size_t startNode = Misc::getClosestPoint(*pathgrid, startPointInLocalCoords); osg::Vec3f endPointInLocalCoords(converter.toLocalVec3(endPoint)); std::pair endNode @@ -206,8 +207,8 @@ namespace MWMechanics // if it's shorter for actor to travel from start to end, than to travel from either // start or end to nearest pathgrid point, just travel from start to end. float startToEndLength2 = (endPointInLocalCoords - startPointInLocalCoords).length2(); - float endTolastNodeLength2 = distanceSquared(pathgrid->mPoints[endNode.first], endPointInLocalCoords); - float startTo1stNodeLength2 = distanceSquared(pathgrid->mPoints[startNode], startPointInLocalCoords); + float endTolastNodeLength2 = Misc::distanceSquared(pathgrid->mPoints[endNode.first], endPointInLocalCoords); + float startTo1stNodeLength2 = Misc::distanceSquared(pathgrid->mPoints[startNode], startPointInLocalCoords); if ((startToEndLength2 < startTo1stNodeLength2) || (startToEndLength2 < endTolastNodeLength2)) { *out++ = endPoint; @@ -223,7 +224,7 @@ namespace MWMechanics { ESM::Pathgrid::Point temp(pathgrid->mPoints[startNode]); converter.toWorld(temp); - *out++ = makeOsgVec3(temp); + *out++ = Misc::Convert::makeOsgVec3f(temp); } else { @@ -234,8 +235,8 @@ namespace MWMechanics if (path.size() > 1) { ESM::Pathgrid::Point secondNode = *(++path.begin()); - osg::Vec3f firstNodeVec3f = makeOsgVec3(pathgrid->mPoints[startNode]); - osg::Vec3f secondNodeVec3f = makeOsgVec3(secondNode); + osg::Vec3f firstNodeVec3f = Misc::Convert::makeOsgVec3f(pathgrid->mPoints[startNode]); + osg::Vec3f secondNodeVec3f = Misc::Convert::makeOsgVec3f(secondNode); osg::Vec3f toSecondNodeVec3f = secondNodeVec3f - firstNodeVec3f; osg::Vec3f toStartPointVec3f = startPointInLocalCoords - firstNodeVec3f; if (toSecondNodeVec3f * toStartPointVec3f > 0) @@ -259,7 +260,7 @@ namespace MWMechanics // convert supplied path to world coordinates std::transform(path.begin(), path.end(), out, [&](ESM::Pathgrid::Point& point) { converter.toWorld(point); - return makeOsgVec3(point); + return Misc::Convert::makeOsgVec3f(point); }); } @@ -359,26 +360,16 @@ namespace MWMechanics mConstructed = true; } - void PathFinder::buildPathByPathgrid(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph) - { - mPath.clear(); - mCell = cell; - - buildPathByPathgridImpl(startPoint, endPoint, pathgridGraph, std::back_inserter(mPath)); - - mConstructed = !mPath.empty(); - } - void PathFinder::buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) + const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType, + std::span checkpoints) { mPath.clear(); // If it's not possible to build path over navmesh due to disabled navmesh generation fallback to straight path DetourNavigator::Status status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, flags, - areaCosts, endTolerance, pathType, std::back_inserter(mPath)); + areaCosts, endTolerance, pathType, checkpoints, std::back_inserter(mPath)); if (status != DetourNavigator::Status::Success) mPath.clear(); @@ -390,19 +381,19 @@ namespace MWMechanics } void PathFinder::buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, - const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) + const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType, std::span checkpoints) { mPath.clear(); - mCell = cell; + mCell = actor.getCell(); DetourNavigator::Status status = DetourNavigator::Status::NavMeshNotFound; if (!actor.getClass().isPureWaterCreature(actor) && !actor.getClass().isPureFlyingCreature(actor)) { status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, flags, areaCosts, endTolerance, - pathType, std::back_inserter(mPath)); + pathType, checkpoints, std::back_inserter(mPath)); if (status != DetourNavigator::Status::Success) mPath.clear(); } @@ -411,7 +402,7 @@ namespace MWMechanics && (flags & DetourNavigator::Flag_usePathgrid) == 0) { status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, - flags | DetourNavigator::Flag_usePathgrid, areaCosts, endTolerance, pathType, + flags | DetourNavigator::Flag_usePathgrid, areaCosts, endTolerance, pathType, checkpoints, std::back_inserter(mPath)); if (status != DetourNavigator::Status::Success) mPath.clear(); @@ -429,12 +420,13 @@ namespace MWMechanics DetourNavigator::Status PathFinder::buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, - PathType pathType, std::back_insert_iterator> out) + PathType pathType, std::span checkpoints, + std::back_insert_iterator> out) { - const auto world = MWBase::Environment::get().getWorld(); - const auto navigator = world->getNavigator(); - const auto status = DetourNavigator::findPath( - *navigator, agentBounds, startPoint, endPoint, flags, areaCosts, endTolerance, out); + const MWBase::World& world = *MWBase::Environment::get().getWorld(); + const DetourNavigator::Navigator& navigator = *world.getNavigator(); + const DetourNavigator::Status status = DetourNavigator::findPath( + navigator, agentBounds, startPoint, endPoint, flags, areaCosts, endTolerance, checkpoints, out); if (pathType == PathType::Partial && status == DetourNavigator::Status::PartialPath) return DetourNavigator::Status::Success; @@ -451,9 +443,9 @@ namespace MWMechanics } void PathFinder::buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, - const osg::Vec3f& endPoint, const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, - const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) + const osg::Vec3f& endPoint, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType) { const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); const auto maxDistance @@ -461,9 +453,9 @@ namespace MWMechanics const auto startToEnd = endPoint - startPoint; const auto distance = startToEnd.length(); if (distance <= maxDistance) - return buildPath(actor, startPoint, endPoint, cell, pathgridGraph, agentBounds, flags, areaCosts, - endTolerance, pathType); + return buildPath( + actor, startPoint, endPoint, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType); const auto end = startPoint + startToEnd * maxDistance / distance; - buildPath(actor, startPoint, end, cell, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType); + buildPath(actor, startPoint, end, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType); } } diff --git a/apps/openmw/mwmechanics/pathfinding.hpp b/apps/openmw/mwmechanics/pathfinding.hpp index 94242404e4..55064d9e88 100644 --- a/apps/openmw/mwmechanics/pathfinding.hpp +++ b/apps/openmw/mwmechanics/pathfinding.hpp @@ -4,12 +4,13 @@ #include #include #include +#include + +#include #include #include #include -#include -#include namespace MWWorld { @@ -102,23 +103,20 @@ namespace MWMechanics void buildStraightPath(const osg::Vec3f& endPoint); - void buildPathByPathgrid(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph); - void buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, - PathType pathType); + PathType pathType, std::span checkpoints = {}); void buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, - const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType); + const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType, std::span checkpoints = {}); void buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, - const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, - const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, - const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType); + const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds, + const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, + PathType pathType); /// Remove front point if exist and within tolerance void update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance, @@ -145,61 +143,6 @@ namespace MWMechanics mPath.push_back(point); } - /// utility function to convert a osg::Vec3f to a Pathgrid::Point - static ESM::Pathgrid::Point makePathgridPoint(const osg::Vec3f& v) - { - return ESM::Pathgrid::Point(static_cast(v[0]), static_cast(v[1]), static_cast(v[2])); - } - - /// utility function to convert an ESM::Position to a Pathgrid::Point - static ESM::Pathgrid::Point makePathgridPoint(const ESM::Position& p) - { - return ESM::Pathgrid::Point( - static_cast(p.pos[0]), static_cast(p.pos[1]), static_cast(p.pos[2])); - } - - static osg::Vec3f makeOsgVec3(const ESM::Pathgrid::Point& p) - { - return osg::Vec3f(static_cast(p.mX), static_cast(p.mY), static_cast(p.mZ)); - } - - // Slightly cheaper version for comparisons. - // Caller needs to be careful for very short distances (i.e. less than 1) - // or when accumuating the results i.e. (a + b)^2 != a^2 + b^2 - // - static float distanceSquared(const ESM::Pathgrid::Point& point, const osg::Vec3f& pos) - { - return (MWMechanics::PathFinder::makeOsgVec3(point) - pos).length2(); - } - - // Return the closest pathgrid point index from the specified position - // coordinates. NOTE: Does not check if there is a sensible way to get there - // (e.g. a cliff in front). - // - // NOTE: pos is expected to be in local coordinates, as is grid->mPoints - // - static size_t getClosestPoint(const ESM::Pathgrid* grid, const osg::Vec3f& pos) - { - assert(grid && !grid->mPoints.empty()); - - float distanceBetween = distanceSquared(grid->mPoints[0], pos); - size_t closestIndex = 0; - - // TODO: if this full scan causes performance problems mapping pathgrid - // points to a quadtree may help - for (size_t counter = 1; counter < grid->mPoints.size(); counter++) - { - float potentialDistBetween = distanceSquared(grid->mPoints[counter], pos); - if (potentialDistBetween < distanceBetween) - { - distanceBetween = potentialDistBetween; - closestIndex = counter; - } - } - - return closestIndex; - } - private: bool mConstructed = false; std::deque mPath; @@ -211,7 +154,8 @@ namespace MWMechanics [[nodiscard]] DetourNavigator::Status buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, - PathType pathType, std::back_insert_iterator> out); + PathType pathType, std::span checkpoints, + std::back_insert_iterator> out); }; } diff --git a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp index c1fa0ad1ee..6270cd3083 100644 --- a/apps/openmw/mwphysics/hasspherecollisioncallback.hpp +++ b/apps/openmw/mwphysics/hasspherecollisioncallback.hpp @@ -10,7 +10,7 @@ namespace MWPhysics { // https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_collision_detection - bool testAabbAgainstSphere( + inline bool testAabbAgainstSphere( const btVector3& aabbMin, const btVector3& aabbMax, const btVector3& position, const btScalar radius) { const btVector3 nearest(std::clamp(position.x(), aabbMin.x(), aabbMax.x()), @@ -18,35 +18,28 @@ namespace MWPhysics return nearest.distance(position) < radius; } - template class HasSphereCollisionCallback final : public btBroadphaseAabbCallback { public: - HasSphereCollisionCallback(const btVector3& position, const btScalar radius, const int mask, const int group, - const Ignore& ignore, OnCollision* onCollision) + explicit HasSphereCollisionCallback(const btVector3& position, const btScalar radius, const int mask, + const int group, const btCollisionObject* ignore) : mPosition(position) , mRadius(radius) , mIgnore(ignore) , mCollisionFilterMask(mask) , mCollisionFilterGroup(group) - , mOnCollision(onCollision) { } bool process(const btBroadphaseProxy* proxy) override { - if (mResult && mOnCollision == nullptr) + if (mResult) return false; const auto collisionObject = static_cast(proxy->m_clientObject); - if (mIgnore(collisionObject) || !needsCollision(*proxy) + if (mIgnore == collisionObject || !needsCollision(*proxy) || !testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius)) return true; mResult = true; - if (mOnCollision != nullptr) - { - (*mOnCollision)(collisionObject); - return true; - } return !mResult; } @@ -55,10 +48,9 @@ namespace MWPhysics private: btVector3 mPosition; btScalar mRadius; - Ignore mIgnore; + const btCollisionObject* mIgnore; int mCollisionFilterMask; int mCollisionFilterGroup; - OnCollision* mOnCollision; bool mResult = false; bool needsCollision(const btBroadphaseProxy& proxy) const diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 5e7c70788d..f403f97c2f 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -849,36 +849,18 @@ namespace MWPhysics mWaterCollisionObject.get(), CollisionType_Water, CollisionType_Actor | CollisionType_Projectile); } - bool PhysicsSystem::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, - std::span ignore, std::vector* occupyingActors) const + bool PhysicsSystem::isAreaOccupiedByOtherActor( + const MWWorld::LiveCellRefBase* actor, const osg::Vec3f& position, const float radius) const { - std::vector ignoredObjects; - ignoredObjects.reserve(ignore.size()); - for (const auto& v : ignore) - if (const auto it = mActors.find(v.mRef); it != mActors.end()) - ignoredObjects.push_back(it->second->getCollisionObject()); - std::sort(ignoredObjects.begin(), ignoredObjects.end()); - ignoredObjects.erase(std::unique(ignoredObjects.begin(), ignoredObjects.end()), ignoredObjects.end()); - const auto ignoreFilter = [&](const btCollisionObject* v) { - return std::binary_search(ignoredObjects.begin(), ignoredObjects.end(), v); - }; - const auto bulletPosition = Misc::Convert::toBullet(position); - const auto aabbMin = bulletPosition - btVector3(radius, radius, radius); - const auto aabbMax = bulletPosition + btVector3(radius, radius, radius); + const btCollisionObject* ignoredObject = nullptr; + if (const auto it = mActors.find(actor); it != mActors.end()) + ignoredObject = it->second->getCollisionObject(); + const btVector3 bulletPosition = Misc::Convert::toBullet(position); + const btVector3 aabbMin = bulletPosition - btVector3(radius, radius, radius); + const btVector3 aabbMax = bulletPosition + btVector3(radius, radius, radius); const int mask = MWPhysics::CollisionType_Actor; const int group = MWPhysics::CollisionType_AnyPhysical; - if (occupyingActors == nullptr) - { - HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter, - static_cast(nullptr)); - mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); - return callback.getResult(); - } - const auto onCollision = [&](const btCollisionObject* object) { - if (PtrHolder* holder = static_cast(object->getUserPointer())) - occupyingActors->push_back(holder->getPtr()); - }; - HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter, &onCollision); + HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoredObject); mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); return callback.getResult(); } diff --git a/apps/openmw/mwphysics/physicssystem.hpp b/apps/openmw/mwphysics/physicssystem.hpp index 546d72676e..8a845b4c41 100644 --- a/apps/openmw/mwphysics/physicssystem.hpp +++ b/apps/openmw/mwphysics/physicssystem.hpp @@ -281,8 +281,8 @@ namespace MWPhysics std::for_each(mAnimatedObjects.begin(), mAnimatedObjects.end(), function); } - bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, - std::span ignore, std::vector* occupyingActors) const; + bool isAreaOccupiedByOtherActor( + const MWWorld::LiveCellRefBase* actor, const osg::Vec3f& position, float radius) const; void reportStats(unsigned int frameNumber, osg::Stats& stats) const; void reportCollision(const btVector3& position, const btVector3& normal); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 02b93706b3..7c0d6f4df8 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -3871,10 +3871,11 @@ namespace MWWorld return btRayAabb(localFrom, localTo, aabbMin, aabbMax, hitDistance, hitNormal); } - bool World::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, - std::span ignore, std::vector* occupyingActors) const + bool World::isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const { - return mPhysics->isAreaOccupiedByOtherActor(position, radius, ignore, occupyingActors); + const osg::Vec3f halfExtents = getPathfindingAgentBounds(actor).mHalfExtents; + const float maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); + return mPhysics->isAreaOccupiedByOtherActor(actor.mRef, position, 2 * maxHalfExtent); } void World::reportStats(unsigned int frameNumber, osg::Stats& stats) const diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index a9af507857..4e93acee45 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -664,8 +664,7 @@ namespace MWWorld bool hasCollisionWithDoor( const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const override; - bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, - std::span ignore, std::vector* occupyingActors) const override; + bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const override; void reportStats(unsigned int frameNumber, osg::Stats& stats) const override; diff --git a/components/detournavigator/findsmoothpath.hpp b/components/detournavigator/findsmoothpath.hpp index e5efa8815f..d01b6bc1c7 100644 --- a/components/detournavigator/findsmoothpath.hpp +++ b/components/detournavigator/findsmoothpath.hpp @@ -13,7 +13,6 @@ #include -#include #include #include #include @@ -64,6 +63,25 @@ namespace DetourNavigator std::reference_wrapper mSettings; }; + template + class ToNavMeshCoordinatesSpan + { + public: + explicit ToNavMeshCoordinatesSpan(std::span span, const RecastSettings& settings) + : mSpan(span) + , mSettings(settings) + { + } + + std::size_t size() const noexcept { return mSpan.size(); } + + T operator[](std::size_t i) const noexcept { return toNavMeshCoordinates(mSettings, mSpan[i]); } + + private: + std::span mSpan; + const RecastSettings& mSettings; + }; + 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) @@ -79,7 +97,7 @@ namespace DetourNavigator } Status makeSmoothPath(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& start, const osg::Vec3f& end, - std::span polygonPath, std::size_t polygonPathSize, std::size_t maxSmoothPathSize, + std::span polygonPath, std::size_t polygonPathSize, std::size_t maxSmoothPathSize, bool skipFirst, std::output_iterator auto& out) { assert(polygonPathSize <= polygonPath.size()); @@ -95,7 +113,7 @@ namespace DetourNavigator dtStatusFailed(status)) return Status::FindStraightPathFailed; - for (int i = 0; i < cornersCount; ++i) + for (int i = skipFirst ? 1 : 0; i < cornersCount; ++i) *out++ = Misc::Convert::makeOsgVec3f(&cornerVertsBuffer[static_cast(i) * 3]); return Status::Success; @@ -103,7 +121,8 @@ namespace DetourNavigator Status findSmoothPath(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, const DetourSettings& settings, - float endTolerance, std::output_iterator auto out) + float endTolerance, const ToNavMeshCoordinatesSpan& checkpoints, + std::output_iterator auto out) { dtQueryFilter queryFilter; queryFilter.setIncludeFlags(includeFlags); @@ -131,29 +150,66 @@ namespace DetourNavigator return Status::EndPolygonNotFound; std::vector polygonPath(settings.mMaxPolygonPathSize); - const auto polygonPathSize - = findPolygonPath(navMeshQuery, startRef, endRef, startNavMeshPos, endNavMeshPos, queryFilter, polygonPath); + std::span polygonPathBuffer = polygonPath; + dtPolyRef currentRef = startRef; + osg::Vec3f currentNavMeshPos = startNavMeshPos; + bool skipFirst = false; - if (!polygonPathSize.has_value()) + for (std::size_t i = 0; i < checkpoints.size(); ++i) + { + const osg::Vec3f checkpointPos = checkpoints[i]; + osg::Vec3f checkpointNavMeshPos; + dtPolyRef checkpointRef; + if (const dtStatus status = navMeshQuery.findNearestPoly(checkpointPos.ptr(), polyHalfExtents.ptr(), + &queryFilter, &checkpointRef, checkpointNavMeshPos.ptr()); + dtStatusFailed(status) || checkpointRef == 0) + continue; + + const std::optional toCheckpointPathSize = findPolygonPath(navMeshQuery, currentRef, + checkpointRef, currentNavMeshPos, checkpointNavMeshPos, queryFilter, polygonPath); + + if (!toCheckpointPathSize.has_value()) + continue; + + if (*toCheckpointPathSize == 0) + continue; + + if (polygonPath[*toCheckpointPathSize - 1] != checkpointRef) + continue; + + const Status smoothStatus = makeSmoothPath(navMeshQuery, currentNavMeshPos, checkpointNavMeshPos, + polygonPath, *toCheckpointPathSize, settings.mMaxSmoothPathSize, skipFirst, out); + + if (smoothStatus != Status::Success) + return smoothStatus; + + currentRef = checkpointRef; + currentNavMeshPos = checkpointNavMeshPos; + skipFirst = true; + } + + const std::optional toEndPathSize = findPolygonPath( + navMeshQuery, currentRef, endRef, currentNavMeshPos, endNavMeshPos, queryFilter, polygonPathBuffer); + + if (!toEndPathSize.has_value()) return Status::FindPathOverPolygonsFailed; - if (*polygonPathSize == 0) - return Status::Success; + if (*toEndPathSize == 0) + return currentRef == endRef ? Status::Success : Status::PartialPath; osg::Vec3f targetNavMeshPos; if (const dtStatus status = navMeshQuery.closestPointOnPoly( - polygonPath[*polygonPathSize - 1], end.ptr(), targetNavMeshPos.ptr(), nullptr); + polygonPath[*toEndPathSize - 1], end.ptr(), targetNavMeshPos.ptr(), nullptr); dtStatusFailed(status)) return Status::TargetPolygonNotFound; - const bool partialPath = polygonPath[*polygonPathSize - 1] != endRef; - const Status smoothStatus = makeSmoothPath(navMeshQuery, startNavMeshPos, targetNavMeshPos, polygonPath, - *polygonPathSize, settings.mMaxSmoothPathSize, out); + const Status smoothStatus = makeSmoothPath(navMeshQuery, currentNavMeshPos, targetNavMeshPos, polygonPath, + *toEndPathSize, settings.mMaxSmoothPathSize, skipFirst, out); if (smoothStatus != Status::Success) return smoothStatus; - return partialPath ? Status::PartialPath : Status::Success; + return polygonPath[*toEndPathSize - 1] == endRef ? Status::Success : Status::PartialPath; } } diff --git a/components/detournavigator/navigatorutils.hpp b/components/detournavigator/navigatorutils.hpp index ca02682ecd..d3b8b5e35a 100644 --- a/components/detournavigator/navigatorutils.hpp +++ b/components/detournavigator/navigatorutils.hpp @@ -11,6 +11,7 @@ #include #include +#include namespace DetourNavigator { @@ -21,13 +22,13 @@ namespace DetourNavigator * @param end path at given point. * @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 + * @param endTolerance defines maximum allowed distance to end path point in addition to agentHalfExtents. + * @param checkpoints is a sequence of positions the path should go over if possible. * @return Status. - * Equal to out if no path is found. */ inline Status findPath(const Navigator& navigator, const AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, float endTolerance, - std::output_iterator auto out) + std::span checkpoints, std::output_iterator auto out) { const auto navMesh = navigator.getNavMesh(agentBounds); if (navMesh == nullptr) @@ -37,7 +38,8 @@ namespace DetourNavigator const auto locked = navMesh->lock(); return findSmoothPath(locked->getQuery(), toNavMeshCoordinates(settings.mRecast, agentBounds.mHalfExtents), toNavMeshCoordinates(settings.mRecast, start), toNavMeshCoordinates(settings.mRecast, end), includeFlags, - areaCosts, settings.mDetour, endTolerance, outTransform); + areaCosts, settings.mDetour, endTolerance, ToNavMeshCoordinatesSpan(checkpoints, settings.mRecast), + outTransform); } /** diff --git a/components/misc/coordinateconverter.hpp b/components/misc/coordinateconverter.hpp index 7853880809..6c4d8dbf71 100644 --- a/components/misc/coordinateconverter.hpp +++ b/components/misc/coordinateconverter.hpp @@ -59,10 +59,18 @@ namespace Misc point.y() -= static_cast(mCellY); } + osg::Vec3f toWorldVec3(const osg::Vec3f& point) const + { + osg::Vec3f result = point; + toWorld(result); + return result; + } + osg::Vec3f toLocalVec3(const osg::Vec3f& point) const { - return osg::Vec3f( - point.x() - static_cast(mCellX), point.y() - static_cast(mCellY), point.z()); + osg::Vec3f result = point; + toLocal(result); + return result; } private: diff --git a/components/misc/pathgridutils.hpp b/components/misc/pathgridutils.hpp new file mode 100644 index 0000000000..5ca58f4d08 --- /dev/null +++ b/components/misc/pathgridutils.hpp @@ -0,0 +1,53 @@ +#ifndef OPENMW_COMPONENTS_MISC_PATHGRIDUTILS_H +#define OPENMW_COMPONENTS_MISC_PATHGRIDUTILS_H + +#include "convert.hpp" + +#include + +#include + +#include + +namespace Misc +{ + // Slightly cheaper version for comparisons. + // Caller needs to be careful for very short distances (i.e. less than 1) + // or when accumuating the results i.e. (a + b)^2 != a^2 + b^2 + // + inline float distanceSquared(const ESM::Pathgrid::Point& point, const osg::Vec3f& pos) + { + return (Misc::Convert::makeOsgVec3f(point) - pos).length2(); + } + + // Return the closest pathgrid point index from the specified position + // coordinates. NOTE: Does not check if there is a sensible way to get there + // (e.g. a cliff in front). + // + // NOTE: pos is expected to be in local coordinates, as is grid->mPoints + // + inline std::size_t getClosestPoint(const ESM::Pathgrid& grid, const osg::Vec3f& pos) + { + if (grid.mPoints.empty()) + throw std::invalid_argument("Pathgrid has no points"); + + float minDistance = distanceSquared(grid.mPoints[0], pos); + std::size_t closestIndex = 0; + + // TODO: if this full scan causes performance problems mapping pathgrid + // points to a quadtree may help + for (std::size_t i = 1; i < grid.mPoints.size(); ++i) + { + const float distance = distanceSquared(grid.mPoints[i], pos); + if (minDistance > distance) + { + minDistance = distance; + closestIndex = i; + } + } + + return closestIndex; + } +} + +#endif diff --git a/files/lua_api/openmw/nearby.lua b/files/lua_api/openmw/nearby.lua index 29f1e79c24..a09e0cf50a 100644 --- a/files/lua_api/openmw/nearby.lua +++ b/files/lua_api/openmw/nearby.lua @@ -180,6 +180,7 @@ -- @field [parent=#FindPathOptions] #AreaCosts areaCosts a table defining relative cost for each type of area. -- @field [parent=#FindPathOptions] #number destinationTolerance a floating point number representing maximum allowed -- distance between destination and a nearest point on the navigation mesh in addition to agent size (default: 1). +-- @field [parent=#FindPathOptions] #table checkpoints an array of positions to build path over if possible. --- -- A table of parameters for @{#nearby.findRandomPointAroundCircle} and @{#nearby.castNavigationRay} diff --git a/scripts/data/integration_tests/test_lua_api/global.lua b/scripts/data/integration_tests/test_lua_api/global.lua index e5bfb5ef36..3a85e224d1 100644 --- a/scripts/data/integration_tests/test_lua_api/global.lua +++ b/scripts/data/integration_tests/test_lua_api/global.lua @@ -315,6 +315,7 @@ registerPlayerTest('player rotation') registerPlayerTest('player forward running') registerPlayerTest('player diagonal walking') registerPlayerTest('findPath') +registerPlayerTest('findPath with checkpoints') registerPlayerTest('findRandomPointAroundCircle') registerPlayerTest('castNavigationRay') registerPlayerTest('findNearestNavMeshPosition') diff --git a/scripts/data/integration_tests/test_lua_api/menu.lua b/scripts/data/integration_tests/test_lua_api/menu.lua index 3fc45bf2e7..8a2a278046 100644 --- a/scripts/data/integration_tests/test_lua_api/menu.lua +++ b/scripts/data/integration_tests/test_lua_api/menu.lua @@ -82,6 +82,7 @@ registerGlobalTest('player rotation', 'rotating player should not lead to nan ro registerGlobalTest('player forward running') registerGlobalTest('player diagonal walking') registerGlobalTest('findPath') +registerGlobalTest('findPath with checkpoints') registerGlobalTest('findRandomPointAroundCircle') registerGlobalTest('castNavigationRay') registerGlobalTest('findNearestNavMeshPosition') diff --git a/scripts/data/integration_tests/test_lua_api/player.lua b/scripts/data/integration_tests/test_lua_api/player.lua index 0b481fba2b..88c4d2ee0f 100644 --- a/scripts/data/integration_tests/test_lua_api/player.lua +++ b/scripts/data/integration_tests/test_lua_api/player.lua @@ -179,6 +179,39 @@ testing.registerLocalTest('findPath', testing.expectEqual(status, nearby.FIND_PATH_STATUS.Success, 'Status') testing.expectLessOrEqual((path[#path] - dst):length(), 1, 'Last path point ' .. testing.formatActualExpected(path[#path], dst)) + testing.expectThat(path, matchers.equalTo({ + matchers.closeToVector(util.vector3(4096, 4096, 1746.27099609375), 1e-1), + matchers.closeToVector(util.vector3(4500, 4500, 1745.95263671875), 1e-1), + })) + end) + +testing.registerLocalTest('findPath with checkpoints', + function() + local src = util.vector3(4096, 4096, 1745) + local dst = util.vector3(4500, 4500, 1745.95263671875) + local options = { + agentBounds = types.Actor.getPathfindingAgentBounds(self), + includeFlags = nearby.NAVIGATOR_FLAGS.Walk + nearby.NAVIGATOR_FLAGS.Swim, + areaCosts = { + water = 1, + door = 2, + ground = 1, + pathgrid = 1, + }, + destinationTolerance = 1, + checkpoints = { + util.vector3(4200, 4100, 1750), + }, + } + local status, path = nearby.findPath(src, dst, options) + testing.expectEqual(status, nearby.FIND_PATH_STATUS.Success, 'Status') + testing.expectLessOrEqual((path[#path] - dst):length(), 1, + 'Last path point ' .. testing.formatActualExpected(path[#path], dst)) + testing.expectThat(path, matchers.equalTo({ + matchers.closeToVector(util.vector3(4096, 4096, 1746.27099609375), 1e-1), + matchers.closeToVector(util.vector3(4200, 4100, 1749.5076904296875), 1e-1), + matchers.closeToVector(util.vector3(4500, 4500, 1745.95263671875), 1e-1), + })) end) testing.registerLocalTest('findRandomPointAroundCircle',