1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-11-29 16:34:30 +00:00

Merge branch openmw:master into master

This commit is contained in:
Igilq 2025-07-28 16:34:40 +00:00
commit 3275b357ac
40 changed files with 849 additions and 592 deletions

View file

@ -1,6 +1,7 @@
Checks: > Checks: >
-*, -*,
portability-*, portability-*,
-portability-template-virtual-member-function,
clang-analyzer-*, clang-analyzer-*,
-clang-analyzer-optin.*, -clang-analyzer-optin.*,
-clang-analyzer-cplusplus.NewDeleteLeaks, -clang-analyzer-cplusplus.NewDeleteLeaks,
@ -16,4 +17,4 @@ CheckOptions:
- key: readability-identifier-naming.NamespaceCase - key: readability-identifier-naming.NamespaceCase
value: CamelCase value: CamelCase
- key: readability-identifier-naming.NamespaceIgnoredRegexp - key: readability-identifier-naming.NamespaceIgnoredRegexp
value: 'osg(DB|FX|Particle|Shadow|Viewer|Util)?' value: 'bpo|osg(DB|FX|Particle|Shadow|Viewer|Util)?'

View file

@ -139,7 +139,7 @@ namespace
TEST_F(DetourNavigatorNavigatorTest, find_path_for_empty_should_return_empty) 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); Status::NavMeshNotFound);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>()); EXPECT_EQ(mPath, std::deque<osg::Vec3f>());
} }
@ -147,7 +147,7 @@ namespace
TEST_F(DetourNavigatorNavigatorTest, find_path_for_existing_agent_with_no_navmesh_should_throw_exception) TEST_F(DetourNavigatorNavigatorTest, find_path_for_existing_agent_with_no_navmesh_should_throw_exception)
{ {
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); 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); Status::StartPolygonNotFound);
} }
@ -156,7 +156,7 @@ namespace
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->removeAgent(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); Status::StartPolygonNotFound);
} }
@ -172,7 +172,7 @@ namespace
updateGuard.reset(); updateGuard.reset();
mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); 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); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -194,7 +194,7 @@ namespace
updateGuard.reset(); updateGuard.reset();
mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); 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); Status::Success);
EXPECT_THAT(mPath, ElementsAre(Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125))) << mPath; EXPECT_THAT(mPath, ElementsAre(Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125))) << mPath;
@ -218,7 +218,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr); mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener); 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); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -237,7 +237,7 @@ namespace
mPath.clear(); mPath.clear();
mOut = std::back_inserter(mPath); 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); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -265,7 +265,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr); mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener); 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); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -285,7 +285,7 @@ namespace
mPath.clear(); mPath.clear();
mOut = std::back_inserter(mPath); 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); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -318,7 +318,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr); mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener); 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); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -386,7 +386,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr); mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener); 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); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -421,7 +421,7 @@ namespace
mEnd.x() = 256; mEnd.x() = 256;
mEnd.z() = 300; 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); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -453,8 +453,8 @@ namespace
mStart.x() = 256; mStart.x() = 256;
mEnd.x() = 256; mEnd.x() = 256;
EXPECT_EQ( EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance,
findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), {}, mOut),
Status::Success); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -487,8 +487,8 @@ namespace
mStart.x() = 256; mStart.x() = 256;
mEnd.x() = 256; mEnd.x() = 256;
EXPECT_EQ( EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance,
findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), {}, mOut),
Status::Success); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -520,7 +520,7 @@ namespace
mStart.x() = 256; mStart.x() = 256;
mEnd.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); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -549,7 +549,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr); mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener); 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); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -577,7 +577,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr); mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener); 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); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -658,7 +658,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr); mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener); 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); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -781,7 +781,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr); mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); 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); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -806,7 +806,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr); mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener); 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); Status::PartialPath);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -834,7 +834,7 @@ namespace
const float endTolerance = 1000.0f; 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); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -979,6 +979,146 @@ namespace
EXPECT_EQ(usedNavMeshTiles, 854); EXPECT_EQ(usedNavMeshTiles, 854);
} }
TEST_F(DetourNavigatorNavigatorTest, find_path_should_return_path_around_steep_mountains)
{
const std::array<float, 5 * 5> 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<int>(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<float, 5 * 5> 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<int>(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<float, 5 * 5> 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<int>(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<osg::Vec3f> 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<float, 5 * 5> 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<int>(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<osg::Vec3f> 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<AgentBounds> struct DetourNavigatorNavigatorNotSupportedAgentBoundsTest : TestWithParam<AgentBounds>
{ {
}; };

View file

@ -9,8 +9,6 @@
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
namespace sfs = std::filesystem;
namespace namespace
{ {
// from configfileparser.cpp // from configfileparser.cpp

View file

@ -10,7 +10,6 @@
#include <components/files/conversion.hpp> #include <components/files/conversion.hpp>
namespace bpo = boost::program_options; namespace bpo = boost::program_options;
namespace sfs = std::filesystem;
#ifndef _WIN32 #ifndef _WIN32
int main(int argc, char* argv[]) int main(int argc, char* argv[])

View file

@ -589,8 +589,7 @@ namespace MWBase
virtual bool hasCollisionWithDoor( virtual bool hasCollisionWithDoor(
const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0; const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0;
virtual bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, virtual bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const = 0;
std::span<const MWWorld::ConstPtr> ignore, std::vector<MWWorld::Ptr>* occupyingActors = nullptr) const = 0;
virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0;

View file

@ -233,6 +233,7 @@ namespace MWLua
DetourNavigator::Flags includeFlags = defaultIncludeFlags; DetourNavigator::Flags includeFlags = defaultIncludeFlags;
DetourNavigator::AreaCosts areaCosts{}; DetourNavigator::AreaCosts areaCosts{};
float destinationTolerance = 1; float destinationTolerance = 1;
std::vector<osg::Vec3f> checkpoints;
if (options.has_value()) if (options.has_value())
{ {
@ -258,13 +259,24 @@ namespace MWLua
} }
if (const auto& v = options->get<sol::optional<float>>("destinationTolerance")) if (const auto& v = options->get<sol::optional<float>>("destinationTolerance"))
destinationTolerance = *v; destinationTolerance = *v;
if (const auto& t = options->get<sol::optional<sol::table>>("checkpoints"))
{
for (const auto& [k, v] : *t)
{
const int index = k.as<int>();
const osg::Vec3f position = v.as<osg::Vec3f>();
if (index != static_cast<int>(checkpoints.size() + 1))
throw std::runtime_error("checkpoints is not an array");
checkpoints.push_back(position);
}
}
} }
std::vector<osg::Vec3f> path; std::vector<osg::Vec3f> path;
const DetourNavigator::Status status const DetourNavigator::Status status = DetourNavigator::findPath(
= DetourNavigator::findPath(*MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, *MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, source, destination,
source, destination, includeFlags, areaCosts, destinationTolerance, std::back_inserter(path)); includeFlags, areaCosts, destinationTolerance, checkpoints, std::back_inserter(path));
sol::table result(lua, sol::create); sol::table result(lua, sol::create);
LuaUtil::copyVectorToTable(path, result); LuaUtil::copyVectorToTable(path, result);

View file

@ -73,6 +73,21 @@ namespace
namespace MWMechanics namespace MWMechanics
{ {
struct ActiveSpells::UpdateContext
{
bool mUpdatedEnemy = false;
bool mUpdatedHitOverlay = false;
bool mUpdateSpellWindow = false;
bool mPlayNonLooping = false;
bool mEraseRemoved = false;
bool mUpdate;
UpdateContext(bool update)
: mUpdate(update)
{
}
};
ActiveSpells::IterationGuard::IterationGuard(ActiveSpells& spells) ActiveSpells::IterationGuard::IterationGuard(ActiveSpells& spells)
: mActiveSpells(spells) : mActiveSpells(spells)
{ {
@ -256,8 +271,9 @@ namespace MWMechanics
++spellIt; ++spellIt;
} }
UpdateContext context(duration > 0.f);
for (const auto& spell : mQueue) for (const auto& spell : mQueue)
addToSpells(ptr, spell); addToSpells(ptr, spell, context);
mQueue.clear(); mQueue.clear();
// Vanilla only does this on cell change I think // Vanilla only does this on cell change I think
@ -267,20 +283,17 @@ namespace MWMechanics
if (spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power if (spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power
&& !isSpellActive(spell->mId)) && !isSpellActive(spell->mId))
{ {
mSpells.emplace_back(ActiveSpellParams{ spell, ptr, true }); initParams(ptr, ActiveSpellParams{ spell, ptr, true }, context);
mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId());
} }
} }
bool updateSpellWindow = false;
bool playNonLooping = false;
if (ptr.getClass().hasInventoryStore(ptr) if (ptr.getClass().hasInventoryStore(ptr)
&& !(creatureStats.isDead() && !creatureStats.isDeathAnimationFinished())) && !(creatureStats.isDead() && !creatureStats.isDeathAnimationFinished()))
{ {
auto& store = ptr.getClass().getInventoryStore(ptr); auto& store = ptr.getClass().getInventoryStore(ptr);
if (store.getInvListener() != nullptr) if (store.getInvListener() != nullptr)
{ {
playNonLooping = !store.isFirstEquip(); context.mPlayNonLooping = !store.isFirstEquip();
const auto world = MWBase::Environment::get().getWorld(); const auto world = MWBase::Environment::get().getWorld();
for (int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++) for (int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++)
{ {
@ -306,82 +319,108 @@ namespace MWMechanics
// invisibility manually // invisibility manually
purgeEffect(ptr, ESM::MagicEffect::Invisibility); purgeEffect(ptr, ESM::MagicEffect::Invisibility);
applyPurges(ptr); applyPurges(ptr);
ActiveSpellParams& params = mSpells.emplace_back(ActiveSpellParams{ *slot, enchantment, ptr }); ActiveSpellParams* params = initParams(ptr, ActiveSpellParams{ *slot, enchantment, ptr }, context);
params.setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); if (params)
updateSpellWindow = true; context.mUpdateSpellWindow = true;
} }
} }
} }
const MWWorld::Ptr player = MWMechanics::getPlayer(); const MWWorld::Ptr player = MWMechanics::getPlayer();
bool updatedHitOverlay = false;
bool updatedEnemy = false;
// Update effects // Update effects
context.mEraseRemoved = true;
for (auto spellIt = mSpells.begin(); spellIt != mSpells.end();) for (auto spellIt = mSpells.begin(); spellIt != mSpells.end();)
{ {
const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId( updateActiveSpell(ptr, duration, spellIt, context);
spellIt->mCasterActorId); // Maybe make this search outside active grid? }
bool removedSpell = false;
std::optional<ActiveSpellParams> reflected;
for (auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();)
{
auto result = applyMagicEffect(ptr, caster, *spellIt, *it, duration, playNonLooping);
if (result.mType == MagicApplicationResult::Type::REFLECTED)
{
if (!reflected)
{
if (Settings::game().mClassicReflectedAbsorbSpellsBehavior)
reflected = { *spellIt, caster };
else
reflected = { *spellIt, ptr };
}
auto& reflectedEffect = reflected->mEffects.emplace_back(*it);
reflectedEffect.mFlags
= ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption;
it = spellIt->mEffects.erase(it);
}
else if (result.mType == MagicApplicationResult::Type::REMOVED)
it = spellIt->mEffects.erase(it);
else
{
++it;
if (!updatedEnemy && result.mShowHealth && caster == player && ptr != player)
{
MWBase::Environment::get().getWindowManager()->setEnemy(ptr);
updatedEnemy = true;
}
if (!updatedHitOverlay && result.mShowHit && ptr == player)
{
MWBase::Environment::get().getWindowManager()->activateHitOverlay(false);
updatedHitOverlay = true;
}
}
removedSpell = applyPurges(ptr, &spellIt, &it);
if (removedSpell)
break;
}
if (reflected)
{
const ESM::Static* reflectStatic = MWBase::Environment::get().getESMStore()->get<ESM::Static>().find(
ESM::RefId::stringRefId("VFX_Reflect"));
MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr);
if (animation && !reflectStatic->mModel.empty())
{
const VFS::Path::Normalized reflectStaticModel
= Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(reflectStatic->mModel));
animation->addEffect(
reflectStaticModel, ESM::MagicEffect::indexToName(ESM::MagicEffect::Reflect), false);
}
caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected);
}
if (removedSpell)
continue;
if (Settings::game().mClassicCalmSpellsBehavior)
{
ESM::MagicEffect::Effects effect
= ptr.getClass().isNpc() ? ESM::MagicEffect::CalmHumanoid : ESM::MagicEffect::CalmCreature;
if (creatureStats.getMagicEffects().getOrDefault(effect).getMagnitude() > 0.f)
creatureStats.getAiSequence().stopCombat();
}
if (ptr == player && context.mUpdateSpellWindow)
{
// Something happened with the spell list -- possibly while the game is paused,
// so we want to make the spell window get the memo.
// We don't normally want to do this, so this targets constant enchantments.
MWBase::Environment::get().getWindowManager()->updateSpellWindow();
}
}
bool ActiveSpells::updateActiveSpell(
const MWWorld::Ptr& ptr, float duration, Collection::iterator& spellIt, UpdateContext& context)
{
const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(
spellIt->mCasterActorId); // Maybe make this search outside active grid?
bool removedSpell = false;
std::optional<ActiveSpellParams> reflected;
for (auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();)
{
auto result = applyMagicEffect(ptr, caster, *spellIt, *it, duration, context.mPlayNonLooping);
if (result.mType == MagicApplicationResult::Type::REFLECTED)
{
if (!reflected)
{
if (Settings::game().mClassicReflectedAbsorbSpellsBehavior)
reflected = { *spellIt, caster };
else
reflected = { *spellIt, ptr };
}
auto& reflectedEffect = reflected->mEffects.emplace_back(*it);
reflectedEffect.mFlags
= ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption;
it = spellIt->mEffects.erase(it);
}
else if (result.mType == MagicApplicationResult::Type::REMOVED)
it = spellIt->mEffects.erase(it);
else
{
const MWWorld::Ptr player = MWMechanics::getPlayer();
++it;
if (!context.mUpdatedEnemy && result.mShowHealth && caster == player && ptr != player)
{
MWBase::Environment::get().getWindowManager()->setEnemy(ptr);
context.mUpdatedEnemy = true;
}
if (!context.mUpdatedHitOverlay && result.mShowHit && ptr == player)
{
MWBase::Environment::get().getWindowManager()->activateHitOverlay(false);
context.mUpdatedHitOverlay = true;
}
}
removedSpell = applyPurges(ptr, &spellIt, &it);
if (removedSpell)
break;
}
if (reflected)
{
const ESM::Static* reflectStatic = MWBase::Environment::get().getESMStore()->get<ESM::Static>().find(
ESM::RefId::stringRefId("VFX_Reflect"));
MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr);
if (animation && !reflectStatic->mModel.empty())
{
const VFS::Path::Normalized reflectStaticModel
= Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(reflectStatic->mModel));
animation->addEffect(
reflectStaticModel, ESM::MagicEffect::indexToName(ESM::MagicEffect::Reflect), false);
}
caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected);
}
if (removedSpell)
return true;
if (context.mEraseRemoved)
{
bool remove = false; bool remove = false;
if (spellIt->hasFlag(ESM::ActiveSpells::Flag_SpellStore)) if (spellIt->hasFlag(ESM::ActiveSpells::Flag_SpellStore))
{ {
try try
{ {
auto& spells = ptr.getClass().getCreatureStats(ptr).getSpells();
remove = !spells.hasSpell(spellIt->mSourceSpellId); remove = !spells.hasSpell(spellIt->mSourceSpellId);
} }
catch (const std::runtime_error& e) catch (const std::runtime_error& e)
@ -413,30 +452,27 @@ namespace MWMechanics
for (const auto& effect : params.mEffects) for (const auto& effect : params.mEffects)
onMagicEffectRemoved(ptr, params, effect); onMagicEffectRemoved(ptr, params, effect);
applyPurges(ptr, &spellIt); applyPurges(ptr, &spellIt);
updateSpellWindow = true; context.mUpdateSpellWindow = true;
continue; return true;
} }
++spellIt;
}
if (Settings::game().mClassicCalmSpellsBehavior)
{
ESM::MagicEffect::Effects effect
= ptr.getClass().isNpc() ? ESM::MagicEffect::CalmHumanoid : ESM::MagicEffect::CalmCreature;
if (creatureStats.getMagicEffects().getOrDefault(effect).getMagnitude() > 0.f)
creatureStats.getAiSequence().stopCombat();
}
if (ptr == player && updateSpellWindow)
{
// Something happened with the spell list -- possibly while the game is paused,
// so we want to make the spell window get the memo.
// We don't normally want to do this, so this targets constant enchantments.
MWBase::Environment::get().getWindowManager()->updateSpellWindow();
} }
++spellIt;
return false;
} }
void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell) ActiveSpells::ActiveSpellParams* ActiveSpells::initParams(
const MWWorld::Ptr& ptr, const ActiveSpellParams& params, UpdateContext& context)
{
mSpells.emplace_back(params).setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId());
auto it = mSpells.end();
--it;
// We instantly apply the effect with a duration of 0 so continuous effects can be purged before truly applying
if (context.mUpdate && updateActiveSpell(ptr, 0.f, it, context))
return nullptr;
return &*it;
}
void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell, UpdateContext& context)
{ {
if (!spell.hasFlag(ESM::ActiveSpells::Flag_Stackable)) if (!spell.hasFlag(ESM::ActiveSpells::Flag_Stackable))
{ {
@ -454,8 +490,7 @@ namespace MWMechanics
onMagicEffectRemoved(ptr, params, effect); onMagicEffectRemoved(ptr, params, effect);
} }
} }
mSpells.emplace_back(spell); initParams(ptr, spell, context);
mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId());
} }
ActiveSpells::ActiveSpells() ActiveSpells::ActiveSpells()
@ -608,6 +643,8 @@ namespace MWMechanics
{ {
purge( purge(
[=](const ActiveSpellParams&, const ESM::ActiveEffect& effect) { [=](const ActiveSpellParams&, const ESM::ActiveEffect& effect) {
if (!(effect.mFlags & ESM::ActiveEffect::Flag_Applied))
return false;
if (effectArg.empty()) if (effectArg.empty())
return effect.mEffectId == effectId; return effect.mEffectId == effectId;
return effect.mEffectId == effectId && effect.getSkillOrAttribute() == effectArg; return effect.mEffectId == effectId && effect.getSkillOrAttribute() == effectArg;

View file

@ -116,17 +116,23 @@ namespace MWMechanics
IterationGuard(ActiveSpells& spells); IterationGuard(ActiveSpells& spells);
~IterationGuard(); ~IterationGuard();
}; };
struct UpdateContext;
std::list<ActiveSpellParams> mSpells; std::list<ActiveSpellParams> mSpells;
std::vector<ActiveSpellParams> mQueue; std::vector<ActiveSpellParams> mQueue;
std::queue<Predicate> mPurges; std::queue<Predicate> mPurges;
bool mIterating; bool mIterating;
void addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell); void addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell, UpdateContext& context);
bool applyPurges(const MWWorld::Ptr& ptr, std::list<ActiveSpellParams>::iterator* currentSpell = nullptr, bool applyPurges(const MWWorld::Ptr& ptr, std::list<ActiveSpellParams>::iterator* currentSpell = nullptr,
std::vector<ActiveEffect>::iterator* currentEffect = nullptr); std::vector<ActiveEffect>::iterator* currentEffect = nullptr);
bool updateActiveSpell(
const MWWorld::Ptr& ptr, float duration, Collection::iterator& spellIt, UpdateContext& context);
ActiveSpellParams* initParams(const MWWorld::Ptr& ptr, const ActiveSpellParams& params, UpdateContext& context);
public: public:
ActiveSpells(); ActiveSpells();

View file

@ -1326,19 +1326,37 @@ namespace MWMechanics
const MWWorld::Ptr player = getPlayer(); const MWWorld::Ptr player = getPlayer();
const MWBase::World* const world = MWBase::Environment::get().getWorld(); const MWBase::World* const world = MWBase::Environment::get().getWorld();
struct CacheEntry
{
MWWorld::Ptr mPtr;
float mMaxSpeed;
osg::Vec3f mHalfExtents;
Movement& mMovement;
};
std::vector<CacheEntry> cache;
cache.reserve(mActors.size());
for (const Actor& actor : mActors) for (const Actor& actor : mActors)
{ {
if (actor.isInvalid()) if (actor.isInvalid())
continue; continue;
const MWWorld::Ptr& ptr = actor.getPtr(); const MWWorld::Ptr& ptr = actor.getPtr();
const MWWorld::Class& cls = ptr.getClass();
cache.push_back({ ptr, cls.getMaxSpeed(ptr), world->getHalfExtents(ptr), cls.getMovementSettings(ptr) });
}
for (const CacheEntry& cached : cache)
{
const MWWorld::Ptr& ptr = cached.mPtr;
if (ptr == player) if (ptr == player)
continue; // Don't interfere with player controls. continue; // Don't interfere with player controls.
const float maxSpeed = ptr.getClass().getMaxSpeed(ptr); const float maxSpeed = cached.mMaxSpeed;
if (maxSpeed == 0.0) if (maxSpeed == 0.0)
continue; // Can't move, so there is no sense to predict collisions. continue; // Can't move, so there is no sense to predict collisions.
Movement& movement = ptr.getClass().getMovementSettings(ptr); Movement& movement = cached.mMovement;
const osg::Vec2f origMovement(movement.mPosition[0], movement.mPosition[1]); const osg::Vec2f origMovement(movement.mPosition[0], movement.mPosition[1]);
const bool isMoving = origMovement.length2() > 0.01; const bool isMoving = origMovement.length2() > 0.01;
if (movement.mPosition[1] < 0) if (movement.mPosition[1] < 0)
@ -1379,7 +1397,7 @@ namespace MWMechanics
const osg::Vec2f baseSpeed = origMovement * maxSpeed; const osg::Vec2f baseSpeed = origMovement * maxSpeed;
const osg::Vec3f basePos = ptr.getRefData().getPosition().asVec3(); const osg::Vec3f basePos = ptr.getRefData().getPosition().asVec3();
const float baseRotZ = ptr.getRefData().getPosition().rot[2]; const float baseRotZ = ptr.getRefData().getPosition().rot[2];
const osg::Vec3f halfExtents = world->getHalfExtents(ptr); const osg::Vec3f& halfExtents = cached.mHalfExtents;
const float maxDistToCheck = isMoving ? maxDistForPartialAvoiding : maxDistForStrictAvoiding; const float maxDistToCheck = isMoving ? maxDistForPartialAvoiding : maxDistForStrictAvoiding;
float timeToCheck = maxTimeToCheck; float timeToCheck = maxTimeToCheck;
@ -1392,15 +1410,13 @@ namespace MWMechanics
float angleToApproachingActor = 0; float angleToApproachingActor = 0;
// Iterate through all other actors and predict collisions. // Iterate through all other actors and predict collisions.
for (const Actor& otherActor : mActors) for (const CacheEntry& otherCached : cache)
{ {
if (otherActor.isInvalid()) const MWWorld::Ptr& otherPtr = otherCached.mPtr;
continue;
const MWWorld::Ptr& otherPtr = otherActor.getPtr();
if (otherPtr == ptr || otherPtr == currentTarget) if (otherPtr == ptr || otherPtr == currentTarget)
continue; continue;
const osg::Vec3f otherHalfExtents = world->getHalfExtents(otherPtr); const osg::Vec3f& otherHalfExtents = otherCached.mHalfExtents;
const osg::Vec3f deltaPos = otherPtr.getRefData().getPosition().asVec3() - basePos; const osg::Vec3f deltaPos = otherPtr.getRefData().getPosition().asVec3() - basePos;
const osg::Vec2f relPos = Misc::rotateVec2f(osg::Vec2f(deltaPos.x(), deltaPos.y()), baseRotZ); const osg::Vec2f relPos = Misc::rotateVec2f(osg::Vec2f(deltaPos.x(), deltaPos.y()), baseRotZ);
const float dist = deltaPos.length(); const float dist = deltaPos.length();
@ -1413,8 +1429,7 @@ namespace MWMechanics
if (deltaPos.z() > halfExtents.z() * 2 || deltaPos.z() < -otherHalfExtents.z() * 2) if (deltaPos.z() > halfExtents.z() * 2 || deltaPos.z() < -otherHalfExtents.z() * 2)
continue; continue;
const osg::Vec3f speed = otherPtr.getClass().getMovementSettings(otherPtr).asVec3() const osg::Vec3f speed = otherCached.mMovement.asVec3() * otherCached.mMaxSpeed;
* otherPtr.getClass().getMaxSpeed(otherPtr);
const float rotZ = otherPtr.getRefData().getPosition().rot[2]; const float rotZ = otherPtr.getRefData().getPosition().rot[2];
const osg::Vec2f relSpeed const osg::Vec2f relSpeed
= Misc::rotateVec2f(osg::Vec2f(speed.x(), speed.y()), baseRotZ - rotZ) - baseSpeed; = Misc::rotateVec2f(osg::Vec2f(speed.x(), speed.y()), baseRotZ - rotZ) - baseSpeed;

View file

@ -1,13 +1,11 @@
#include "aicombat.hpp" #include "aicombat.hpp"
#include <components/misc/coordinateconverter.hpp>
#include <components/misc/rng.hpp>
#include <components/esm3/aisequence.hpp>
#include <components/misc/mathutil.hpp>
#include <components/detournavigator/navigatorutils.hpp> #include <components/detournavigator/navigatorutils.hpp>
#include <components/esm3/aisequence.hpp>
#include <components/misc/coordinateconverter.hpp>
#include <components/misc/mathutil.hpp>
#include <components/misc/pathgridutils.hpp>
#include <components/misc/rng.hpp>
#include <components/sceneutil/positionattitudetransform.hpp> #include <components/sceneutil/positionattitudetransform.hpp>
#include "../mwphysics/raycasting.hpp" #include "../mwphysics/raycasting.hpp"
@ -178,11 +176,16 @@ namespace MWMechanics
currentCell = actor.getCell(); currentCell = actor.getCell();
} }
const MWWorld::Class& actorClass = actor.getClass();
MWMechanics::CreatureStats& stats = actorClass.getCreatureStats(actor);
if (stats.isParalyzed() || stats.getKnockedDown())
return false;
bool forceFlee = false; bool forceFlee = false;
if (!canFight(actor, target)) if (!canFight(actor, target))
{ {
storage.stopAttack(); storage.stopAttack();
actor.getClass().getCreatureStats(actor).setAttackingOrSpell(false); stats.setAttackingOrSpell(false);
storage.mActionCooldown = 0.f; storage.mActionCooldown = 0.f;
// Continue combat if target is player or player follower/escorter and an attack has been attempted // Continue combat if target is player or player follower/escorter and an attack has been attempted
const auto& playerFollowersAndEscorters const auto& playerFollowersAndEscorters
@ -191,18 +194,14 @@ namespace MWMechanics
= (std::find(playerFollowersAndEscorters.begin(), playerFollowersAndEscorters.end(), target) = (std::find(playerFollowersAndEscorters.begin(), playerFollowersAndEscorters.end(), target)
!= playerFollowersAndEscorters.end()); != playerFollowersAndEscorters.end());
if ((target == MWMechanics::getPlayer() || targetSidesWithPlayer) if ((target == MWMechanics::getPlayer() || targetSidesWithPlayer)
&& ((actor.getClass().getCreatureStats(actor).getHitAttemptActorId() && ((stats.getHitAttemptActorId() == target.getClass().getCreatureStats(target).getActorId())
== target.getClass().getCreatureStats(target).getActorId()) || (target.getClass().getCreatureStats(target).getHitAttemptActorId() == stats.getActorId())))
|| (target.getClass().getCreatureStats(target).getHitAttemptActorId()
== actor.getClass().getCreatureStats(actor).getActorId())))
forceFlee = true; forceFlee = true;
else // Otherwise end combat else // Otherwise end combat
return true; return true;
} }
const MWWorld::Class& actorClass = actor.getClass(); stats.setMovementFlag(CreatureStats::Flag_Run, true);
actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true);
float& actionCooldown = storage.mActionCooldown; float& actionCooldown = storage.mActionCooldown;
std::unique_ptr<Action>& currentAction = storage.mCurrentAction; std::unique_ptr<Action>& currentAction = storage.mCurrentAction;
@ -302,8 +301,8 @@ namespace MWMechanics
const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags);
const ESM::Pathgrid* pathgrid = world->getStore().get<ESM::Pathgrid>().search(*actor.getCell()->getCell()); const ESM::Pathgrid* pathgrid = world->getStore().get<ESM::Pathgrid>().search(*actor.getCell()->getCell());
const auto& pathGridGraph = getPathGridGraph(pathgrid); const auto& pathGridGraph = getPathGridGraph(pathgrid);
mPathFinder.buildPath(actor, vActorPos, vTargetPos, actor.getCell(), pathGridGraph, agentBounds, mPathFinder.buildPath(actor, vActorPos, vTargetPos, pathGridGraph, agentBounds, navigatorFlags, areaCosts,
navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full); storage.mAttackRange, PathType::Full);
if (!mPathFinder.isPathConstructed()) if (!mPathFinder.isPathConstructed())
{ {
@ -316,8 +315,8 @@ namespace MWMechanics
if (hit.has_value() && (*hit - vTargetPos).length() <= rangeAttack) if (hit.has_value() && (*hit - vTargetPos).length() <= rangeAttack)
{ {
// If the point is close enough, try to find a path to that point. // If the point is close enough, try to find a path to that point.
mPathFinder.buildPath(actor, vActorPos, *hit, actor.getCell(), pathGridGraph, agentBounds, mPathFinder.buildPath(actor, vActorPos, *hit, pathGridGraph, agentBounds, navigatorFlags, areaCosts,
navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full); storage.mAttackRange, PathType::Full);
if (mPathFinder.isPathConstructed()) if (mPathFinder.isPathConstructed())
{ {
// If path to that point is found use it as custom destination. // If path to that point is found use it as custom destination.
@ -330,7 +329,7 @@ namespace MWMechanics
{ {
storage.mUseCustomDestination = false; storage.mUseCustomDestination = false;
storage.stopAttack(); storage.stopAttack();
actor.getClass().getCreatureStats(actor).setAttackingOrSpell(false); stats.setAttackingOrSpell(false);
currentAction = std::make_unique<ActionFlee>(); currentAction = std::make_unique<ActionFlee>();
actionCooldown = currentAction->getActionCooldown(); actionCooldown = currentAction->getActionCooldown();
storage.startFleeing(); storage.startFleeing();
@ -393,8 +392,8 @@ namespace MWMechanics
osg::Vec3f localPos = actor.getRefData().getPosition().asVec3(); osg::Vec3f localPos = actor.getRefData().getPosition().asVec3();
coords.toLocal(localPos); coords.toLocal(localPos);
size_t closestPointIndex = PathFinder::getClosestPoint(pathgrid, localPos); const std::size_t closestPointIndex = Misc::getClosestPoint(*pathgrid, localPos);
for (size_t i = 0; i < pathgrid->mPoints.size(); i++) for (std::size_t i = 0; i < pathgrid->mPoints.size(); i++)
{ {
if (i != closestPointIndex if (i != closestPointIndex
&& getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, i)) && getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, i))
@ -455,7 +454,8 @@ namespace MWMechanics
float dist float dist
= (actor.getRefData().getPosition().asVec3() - target.getRefData().getPosition().asVec3()).length(); = (actor.getRefData().getPosition().asVec3() - target.getRefData().getPosition().asVec3()).length();
if ((dist > fFleeDistance && !storage.mLOS) 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; state = AiCombatStorage::FleeState_Idle;
} }

View file

@ -2,12 +2,13 @@
#define GAME_MWMECHANICS_AICOMBAT_H #define GAME_MWMECHANICS_AICOMBAT_H
#include "aitemporarybase.hpp" #include "aitemporarybase.hpp"
#include "aitimer.hpp"
#include "movement.hpp"
#include "typedaipackage.hpp" #include "typedaipackage.hpp"
#include "../mwworld/cellstore.hpp" // for Doors #include "../mwworld/cellstore.hpp" // for Doors
#include "aitimer.hpp" #include <components/esm3/loadpgrd.hpp>
#include "movement.hpp"
namespace ESM namespace ESM
{ {

View file

@ -180,8 +180,8 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f&
= world->getStore().get<ESM::Pathgrid>().search(*actor.getCell()->getCell()); = world->getStore().get<ESM::Pathgrid>().search(*actor.getCell()->getCell());
const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor);
const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags);
mPathFinder.buildLimitedPath(actor, position, dest, actor.getCell(), getPathGridGraph(pathgrid), mPathFinder.buildLimitedPath(actor, position, dest, getPathGridGraph(pathgrid), agentBounds,
agentBounds, navigatorFlags, areaCosts, endTolerance, pathType); navigatorFlags, areaCosts, endTolerance, pathType);
mRotateOnTheRunChecks = 3; mRotateOnTheRunChecks = 3;
// give priority to go directly on target if there is minimal opportunity // 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; result |= DetourNavigator::Flag_swim;
if (actorClass.canWalk(actor) && actor.getClass().getWalkSpeed(actor) > 0) 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) if (canOpenDoors(actor) && getTypeId() != AiPackageTypeId::Wander)
result |= DetourNavigator::Flag_openDoor; result |= DetourNavigator::Flag_openDoor;

View file

@ -16,6 +16,8 @@
namespace ESM namespace ESM
{ {
struct Cell; struct Cell;
struct Pathgrid;
namespace AiSequence namespace AiSequence
{ {
struct AiSequence; struct AiSequence;

View file

@ -8,6 +8,7 @@
#include <components/detournavigator/navigatorutils.hpp> #include <components/detournavigator/navigatorutils.hpp>
#include <components/esm3/aisequence.hpp> #include <components/esm3/aisequence.hpp>
#include <components/misc/coordinateconverter.hpp> #include <components/misc/coordinateconverter.hpp>
#include <components/misc/pathgridutils.hpp>
#include <components/misc/rng.hpp> #include <components/misc/rng.hpp>
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
@ -29,17 +30,6 @@
namespace MWMechanics 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] = { const std::string_view AiWander::sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1] = {
"idle2", "idle2",
"idle3", "idle3",
@ -53,11 +43,22 @@ namespace MWMechanics
namespace 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) inline int getCountBeforeReset(const MWWorld::ConstPtr& actor)
{ {
if (actor.getClass().isPureWaterCreature(actor) || actor.getClass().isPureFlyingCreature(actor)) if (actor.getClass().isPureWaterCreature(actor) || actor.getClass().isPureFlyingCreature(actor))
return 1; return 1;
return COUNT_BEFORE_RESET; return countBeforeReset;
} }
osg::Vec3f getRandomPointAround(const osg::Vec3f& position, const float distance) osg::Vec3f getRandomPointAround(const osg::Vec3f& position, const float distance)
@ -99,16 +100,42 @@ namespace MWMechanics
std::vector<unsigned char> getInitialIdle(const std::vector<unsigned char>& idle) std::vector<unsigned char> getInitialIdle(const std::vector<unsigned char>& idle)
{ {
std::vector<unsigned char> result(MAX_IDLE_SIZE, 0); std::vector<unsigned char> result(maxIdleSize, 0);
std::copy_n(idle.begin(), std::min(MAX_IDLE_SIZE, idle.size()), result.begin()); std::copy_n(idle.begin(), std::min(maxIdleSize, idle.size()), result.begin());
return result; return result;
} }
std::vector<unsigned char> getInitialIdle(const unsigned char (&idle)[MAX_IDLE_SIZE]) std::vector<unsigned char> getInitialIdle(const unsigned char (&idle)[maxIdleSize])
{ {
return std::vector<unsigned char>(std::begin(idle), std::end(idle)); return std::vector<unsigned char>(std::begin(idle), std::end(idle));
} }
void trimAllowedPositions(const std::deque<osg::Vec3f>& path, std::vector<osg::Vec3f>& 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<osg::Vec3f> 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() AiWanderStorage::AiWanderStorage()
@ -118,9 +145,9 @@ namespace MWMechanics
, mCanWanderAlongPathGrid(true) , mCanWanderAlongPathGrid(true)
, mIdleAnimation(0) , mIdleAnimation(0)
, mBadIdles() , mBadIdles()
, mPopulateAvailableNodes(true) , mPopulateAvailablePositions(true)
, mAllowedNodes() , mAllowedPositions()
, mTrimCurrentNode(false) , mTrimCurrentPosition(false)
, mCheckIdlePositionTimer(0) , mCheckIdlePositionTimer(0)
, mStuckCount(0) , mStuckCount(0)
{ {
@ -128,8 +155,8 @@ namespace MWMechanics
AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector<unsigned char>& idle, bool repeat) AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector<unsigned char>& idle, bool repeat)
: TypedAiPackage<AiWander>(repeat) : TypedAiPackage<AiWander>(repeat)
, mDistance(std::max(0, distance)) , mDistance(static_cast<unsigned>(std::max(0, distance)))
, mDuration(std::max(0, duration)) , mDuration(static_cast<unsigned>(std::max(0, duration)))
, mRemainingDuration(duration) , mRemainingDuration(duration)
, mTimeOfDay(timeOfDay) , mTimeOfDay(timeOfDay)
, mIdle(getInitialIdle(idle)) , mIdle(getInitialIdle(idle))
@ -215,20 +242,12 @@ namespace MWMechanics
{ {
const ESM::Pathgrid* pathgrid const ESM::Pathgrid* pathgrid
= MWBase::Environment::get().getESMStore()->get<ESM::Pathgrid>().search(*actor.getCell()->getCell()); = MWBase::Environment::get().getESMStore()->get<ESM::Pathgrid>().search(*actor.getCell()->getCell());
if (mUsePathgrid) const auto agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor);
{ constexpr float endTolerance = 0;
mPathFinder.buildPathByPathgrid( const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor);
pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(pathgrid)); const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags);
} mPathFinder.buildPath(actor, pos.asVec3(), mDestination, getPathGridGraph(pathgrid), agentBounds,
else 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, actor.getCell(), getPathGridGraph(pathgrid),
agentBounds, navigatorFlags, areaCosts, endTolerance, PathType::Full);
}
if (mPathFinder.isPathConstructed()) if (mPathFinder.isPathConstructed())
storage.setState(AiWanderStorage::Wander_Walking, !mUsePathgrid); storage.setState(AiWanderStorage::Wander_Walking, !mUsePathgrid);
@ -259,9 +278,6 @@ namespace MWMechanics
bool AiWander::reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos) bool AiWander::reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos)
{ {
if (mDistance <= 0)
storage.mCanWanderAlongPathGrid = false;
if (isPackageCompleted()) if (isPackageCompleted())
{ {
stopWalking(actor); stopWalking(actor);
@ -276,13 +292,15 @@ namespace MWMechanics
mStoredInitialActorPosition = true; mStoredInitialActorPosition = true;
} }
// Initialization to discover & store allowed node points for this actor. // Initialization to discover & store allowed positions points for this actor.
if (storage.mPopulateAvailableNodes) 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) if (canActorMoveByZAxis(actor) && mDistance > 0)
{ {
// Typically want to idle for a short time before the next wander // 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, // If the package has a wander distance but no pathgrid is available,
// randomly idle or wander near spawn point // 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 // Typically want to idle for a short time before the next wander
if (Misc::Rng::rollDice(100, prng) >= 96) if (Misc::Rng::rollDice(100, prng) >= 96)
@ -307,7 +325,7 @@ namespace MWMechanics
storage.setState(AiWanderStorage::Wander_IdleNow); storage.setState(AiWanderStorage::Wander_IdleNow);
} }
} }
else if (storage.mAllowedNodes.empty() && !storage.mIsWanderingManually) else if (storage.mAllowedPositions.empty() && !storage.mIsWanderingManually)
{ {
storage.mCanWanderAlongPathGrid = false; storage.mCanWanderAlongPathGrid = false;
} }
@ -323,9 +341,9 @@ namespace MWMechanics
// Construct a new path if there isn't one // Construct a new path if there isn't one
if (!mPathFinder.isPathConstructed()) 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 if (storage.mIsWanderingManually && storage.mState == AiWanderStorage::Wander_Walking
&& (mPathFinder.getPathSize() == 0 || isDestinationHidden(actor, mPathFinder.getPath().back()) && (mPathFinder.getPathSize() == 0 || isDestinationHidden(actor, mPathFinder.getPath().back())
|| isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back()))) || world.isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back())))
completeManualWalking(actor, storage); completeManualWalking(actor, storage);
return false; // AiWander package not yet completed 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 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 isWaterCreature = actor.getClass().isPureWaterCreature(actor);
const bool isFlyingCreature = actor.getClass().isPureFlyingCreature(actor); const bool isFlyingCreature = actor.getClass().isPureFlyingCreature(actor);
const auto world = MWBase::Environment::get().getWorld(); MWBase::World& world = *MWBase::Environment::get().getWorld();
const auto agentBounds = world->getPathfindingAgentBounds(actor); const auto agentBounds = world.getPathfindingAgentBounds(actor);
const auto navigator = world->getNavigator(); const auto navigator = world.getNavigator();
const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor);
const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags);
auto& prng = MWBase::Environment::get().getWorld()->getPrng(); Misc::Rng::Generator& prng = world.getPrng();
do do
{ {
@ -411,7 +429,7 @@ namespace MWMechanics
if (isDestinationHidden(actor, mDestination)) if (isDestinationHidden(actor, mDestination))
continue; continue;
if (isAreaOccupiedByOtherActor(actor, mDestination)) if (world.isAreaOccupiedByOtherActor(actor, mDestination))
continue; continue;
constexpr float endTolerance = 0; constexpr float endTolerance = 0;
@ -491,17 +509,17 @@ namespace MWMechanics
void AiWander::onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage) 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; storage.mCheckIdlePositionTimer += duration;
if (storage.mCheckIdlePositionTimer >= IDLE_POSITION_CHECK_INTERVAL && !isStationary()) if (storage.mCheckIdlePositionTimer >= idlePositionCheckInterval && !isStationary())
{ {
storage.mCheckIdlePositionTimer = 0; // restart timer storage.mCheckIdlePositionTimer = 0; // restart timer
static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 1.6f; 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.setState(AiWanderStorage::Wander_MoveNow);
storage.mTrimCurrentNode = false; // just in case storage.mTrimCurrentPosition = false; // just in case
return; 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(); const osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3();
for (const ESM::Pathgrid::Point& node : storage.mAllowedNodes) const float squaredDistance = distance * distance;
{ return std::ranges::find_if(storage.mAllowedPositions, [&](const osg::Vec3& v) {
osg::Vec3f point(node.mX, node.mY, node.mZ); return (actorPos - v).length2() < squaredDistance;
if ((actorPos - point).length2() < distance * distance) }) != storage.mAllowedPositions.end();
return true;
}
return false;
} }
void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration,
@ -535,7 +551,7 @@ namespace MWMechanics
// Is there no destination or are we there yet? // Is there no destination or are we there yet?
if ((!mPathFinder.isPathConstructed()) if ((!mPathFinder.isPathConstructed())
|| pathTo(actor, osg::Vec3f(mPathFinder.getPath().back()), duration, supportedMovementDirections, || pathTo(actor, osg::Vec3f(mPathFinder.getPath().back()), duration, supportedMovementDirections,
DESTINATION_TOLERANCE)) destinationTolerance))
{ {
stopWalking(actor); stopWalking(actor);
storage.setState(AiWanderStorage::Wander_ChooseAction); storage.setState(AiWanderStorage::Wander_ChooseAction);
@ -586,8 +602,8 @@ namespace MWMechanics
if (proximityToDoor(actor, distance)) if (proximityToDoor(actor, distance))
{ {
// remove allowed points then select another random destination // remove allowed points then select another random destination
storage.mTrimCurrentNode = true; storage.mTrimCurrentPosition = true;
trimAllowedNodes(storage.mAllowedNodes, mPathFinder); trimAllowedPositions(mPathFinder.getPath(), storage.mAllowedPositions);
mObstacleCheck.clear(); mObstacleCheck.clear();
stopWalking(actor); stopWalking(actor);
storage.setState(AiWanderStorage::Wander_MoveNow); 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) const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos)
{ {
auto world = MWBase::Environment::get().getWorld(); MWBase::World& world = *MWBase::Environment::get().getWorld();
auto& prng = world->getPrng(); Misc::Rng::Generator& prng = world.getPrng();
unsigned int randNode = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng); const std::size_t randomAllowedPositionIndex
const ESM::Pathgrid::Point& dest = storage.mAllowedNodes[randNode]; = static_cast<std::size_t>(Misc::Rng::rollDice(storage.mAllowedPositions.size(), prng));
const osg::Vec3f randomAllowedPosition = storage.mAllowedPositions[randomAllowedPositionIndex];
const osg::Vec3f start = actorPos.asVec3(); const osg::Vec3f start = actorPos.asVec3();
// don't take shortcuts for wandering const MWWorld::Cell& cell = *actor.getCell()->getCell();
const ESM::Pathgrid* pathgrid = world->getStore().get<ESM::Pathgrid>().search(*actor.getCell()->getCell()); const ESM::Pathgrid* pathgrid = world.getStore().get<ESM::Pathgrid>().search(cell);
const osg::Vec3f destVec3f = PathFinder::makeOsgVec3(dest); const PathgridGraph& pathgridGraph = getPathGridGraph(pathgrid);
mPathFinder.buildPathByPathgrid(start, destVec3f, actor.getCell(), getPathGridGraph(pathgrid));
if (mPathFinder.isPathConstructed()) const Misc::CoordinateConverter converter = Misc::makeCoordinateConverter(cell);
std::deque<ESM::Pathgrid::Point> 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; storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex);
mHasDestination = true; return;
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);
} }
// Choose a different node and delete this one from possible nodes because it is uncreachable:
// Drop nearest pathgrid point.
path.pop_front();
std::vector<osg::Vec3f> 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 else
storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode); storage.mAllowedPositions.push_back(storage.mCurrentPosition);
} storage.mCurrentPosition = randomAllowedPosition;
void AiWander::trimAllowedNodes(std::vector<ESM::Pathgrid::Point>& nodes, const PathFinder& pathfinder) storage.setState(AiWanderStorage::Wander_Walking);
{
// 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();
}
} }
void AiWander::stopWalking(const MWWorld::Ptr& actor) void AiWander::stopWalking(const MWWorld::Ptr& actor)
@ -742,20 +758,20 @@ namespace MWMechanics
return; return;
AiWanderStorage& storage = state.get<AiWanderStorage>(); AiWanderStorage& storage = state.get<AiWanderStorage>();
if (storage.mPopulateAvailableNodes) if (storage.mPopulateAvailablePositions)
getAllowedNodes(actor, storage); fillAllowedPositions(actor, storage);
if (storage.mAllowedNodes.empty()) if (storage.mAllowedPositions.empty())
return; return;
auto& prng = MWBase::Environment::get().getWorld()->getPrng(); auto& prng = MWBase::Environment::get().getWorld()->getPrng();
int index = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng); int index = Misc::Rng::rollDice(storage.mAllowedPositions.size(), prng);
ESM::Pathgrid::Point worldDest = storage.mAllowedNodes[index]; const osg::Vec3f worldDest = storage.mAllowedPositions[index];
const Misc::CoordinateConverter converter = Misc::makeCoordinateConverter(*actor.getCell()->getCell()); 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( const bool isPathGridOccupied
PathFinder::makeOsgVec3(worldDest), 60); = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(worldDest, 60);
// add offset only if the selected pathgrid is occupied by another actor // add offset only if the selected pathgrid is occupied by another actor
if (isPathGridOccupied) if (isPathGridOccupied)
@ -775,19 +791,17 @@ namespace MWMechanics
const ESM::Pathgrid::Point& connDest = points[randomIndex]; const ESM::Pathgrid::Point& connDest = points[randomIndex];
// add an offset towards random neighboring node // add an offset towards random neighboring node
osg::Vec3f dir = PathFinder::makeOsgVec3(connDest) - PathFinder::makeOsgVec3(dest); osg::Vec3f dir = Misc::Convert::makeOsgVec3f(connDest) - dest;
float length = dir.length(); const float length = dir.length();
dir.normalize(); dir.normalize();
for (int j = 1; j <= 3; j++) for (int j = 1; j <= 3; j++)
{ {
// move for 5-15% towards random neighboring node // move for 5-15% towards random neighboring node
dest dest = dest + dir * (j * 5 * length / 100.f);
= PathFinder::makePathgridPoint(PathFinder::makeOsgVec3(dest) + dir * (j * 5 * length / 100.f));
worldDest = converter.toWorldPoint(dest);
isOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange( isOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(
PathFinder::makeOsgVec3(worldDest), 60); converter.toWorldVec3(dest), 60);
if (!isOccupied) if (!isOccupied)
break; break;
@ -807,19 +821,18 @@ namespace MWMechanics
// place above to prevent moving inside objects, e.g. stairs, because a vector between pathgrids can be // 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. // underground. Adding 20 in adjustPosition() is not enough.
dest.mZ += 60; dest.z() += 60;
converter.toWorld(dest); converter.toWorld(dest);
state.reset(); state.reset();
osg::Vec3f pos(static_cast<float>(dest.mX), static_cast<float>(dest.mY), static_cast<float>(dest.mZ)); MWBase::Environment::get().getWorld()->moveObject(actor, dest);
MWBase::Environment::get().getWorld()->moveObject(actor, pos);
actor.getClass().adjustPosition(actor, false); actor.getClass().adjustPosition(actor, false);
} }
void AiWander::getNeighbouringNodes( 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 const ESM::Pathgrid* pathgrid
= MWBase::Environment::get().getESMStore()->get<ESM::Pathgrid>().search(*currentCell->getCell()); = MWBase::Environment::get().getESMStore()->get<ESM::Pathgrid>().search(*currentCell->getCell());
@ -827,19 +840,19 @@ namespace MWMechanics
if (pathgrid == nullptr || pathgrid->mPoints.empty()) if (pathgrid == nullptr || pathgrid->mPoints.empty())
return; return;
size_t index = PathFinder::getClosestPoint(pathgrid, PathFinder::makeOsgVec3(dest)); const size_t index = Misc::getClosestPoint(*pathgrid, dest);
getPathGridGraph(pathgrid).getNeighbouringPoints(index, points); 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 // infrequently used, therefore no benefit in caching it as a member
const MWWorld::CellStore* cellStore = actor.getCell(); const MWWorld::CellStore* cellStore = actor.getCell();
const ESM::Pathgrid* pathgrid const ESM::Pathgrid* pathgrid
= MWBase::Environment::get().getESMStore()->get<ESM::Pathgrid>().search(*cellStore->getCell()); = MWBase::Environment::get().getESMStore()->get<ESM::Pathgrid>().search(*cellStore->getCell());
storage.mAllowedNodes.clear(); storage.mAllowedPositions.clear();
// If there is no path this actor doesn't go anywhere. See: // If there is no path this actor doesn't go anywhere. See:
// https://forum.openmw.org/viewtopic.php?t=1556 // https://forum.openmw.org/viewtopic.php?t=1556
@ -860,34 +873,35 @@ namespace MWMechanics
const osg::Vec3f npcPos = converter.toLocalVec3(mInitialActorPosition); const osg::Vec3f npcPos = converter.toLocalVec3(mInitialActorPosition);
// Find closest pathgrid point // 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 // and if the point is connected to the closest current point
// NOTE: mPoints is in local coordinates // NOTE: mPoints is in local coordinates
size_t pointIndex = 0; size_t pointIndex = 0;
for (size_t counter = 0; counter < pathgrid->mPoints.size(); counter++) 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 if ((npcPos - nodePos).length2() <= mDistance * mDistance
&& getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, counter)) && 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; 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); 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, // When only one path grid point in wander distance,
@ -901,44 +915,44 @@ namespace MWMechanics
{ {
if (edge.mV0 == pointIndex) 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); 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) const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage)
{ {
osg::Vec3f vectorStart = PathFinder::makeOsgVec3(start); osg::Vec3f vectorStart = Misc::Convert::makeOsgVec3f(start);
osg::Vec3f delta = PathFinder::makeOsgVec3(end) - vectorStart; osg::Vec3f delta = Misc::Convert::makeOsgVec3f(end) - vectorStart;
float length = delta.length(); float length = delta.length();
delta.normalize(); 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 // must not travel longer than distance between waypoints or NPC goes past waypoint
distance = std::min(distance, static_cast<int>(length)); distance = std::min(distance, static_cast<unsigned>(length));
delta *= distance; 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<float>::max(); float distanceToClosestPosition = std::numeric_limits<float>::max();
size_t index = 0; 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])); const osg::Vec3f position = storage.mAllowedPositions[i];
float tempDist = (mInitialActorPosition - nodePos).length2(); const float tempDist = (mInitialActorPosition - position).length2();
if (tempDist < distanceToClosestNode) if (tempDist < distanceToClosestPosition)
{ {
index = i; index = i;
distanceToClosestNode = tempDist; distanceToClosestPosition = tempDist;
} }
} }
storage.mCurrentNode = storage.mAllowedNodes[index]; storage.mCurrentPosition = storage.mAllowedPositions[index];
storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + index); storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + index);
} }
void AiWander::writeState(ESM::AiSequence::AiSequence& sequence) const void AiWander::writeState(ESM::AiSequence::AiSequence& sequence) const
@ -970,8 +984,8 @@ namespace MWMechanics
AiWander::AiWander(const ESM::AiSequence::AiWander* wander) AiWander::AiWander(const ESM::AiSequence::AiWander* wander)
: TypedAiPackage<AiWander>(makeDefaultOptions().withRepeat(wander->mData.mShouldRepeat != 0)) : TypedAiPackage<AiWander>(makeDefaultOptions().withRepeat(wander->mData.mShouldRepeat != 0))
, mDistance(std::max(static_cast<short>(0), wander->mData.mDistance)) , mDistance(static_cast<unsigned>(std::max(static_cast<short>(0), wander->mData.mDistance)))
, mDuration(std::max(static_cast<short>(0), wander->mData.mDuration)) , mDuration(static_cast<unsigned>(std::max(static_cast<short>(0), wander->mData.mDuration)))
, mRemainingDuration(wander->mDurationData.mRemainingDuration) , mRemainingDuration(wander->mDurationData.mRemainingDuration)
, mTimeOfDay(wander->mData.mTimeOfDay) , mTimeOfDay(wander->mData.mTimeOfDay)
, mIdle(getInitialIdle(wander->mData.mIdle)) , mIdle(getInitialIdle(wander->mData.mIdle))

View file

@ -1,14 +1,15 @@
#ifndef GAME_MWMECHANICS_AIWANDER_H #ifndef GAME_MWMECHANICS_AIWANDER_H
#define GAME_MWMECHANICS_AIWANDER_H #define GAME_MWMECHANICS_AIWANDER_H
#include "typedaipackage.hpp"
#include <string_view>
#include <vector>
#include "aitemporarybase.hpp" #include "aitemporarybase.hpp"
#include "aitimer.hpp" #include "aitimer.hpp"
#include "pathfinding.hpp" #include "pathfinding.hpp"
#include "typedaipackage.hpp"
#include <components/esm3/loadpgrd.hpp>
#include <string_view>
#include <vector>
namespace ESM namespace ESM
{ {
@ -51,14 +52,13 @@ namespace MWMechanics
unsigned short mIdleAnimation; unsigned short mIdleAnimation;
std::vector<unsigned short> mBadIdles; // Idle animations that when called cause errors std::vector<unsigned short> mBadIdles; // Idle animations that when called cause errors
// do we need to calculate allowed nodes based on mDistance bool mPopulateAvailablePositions;
bool mPopulateAvailableNodes;
// allowed pathgrid nodes based on mDistance from the spawn point // allowed destination positions based on mDistance from the spawn point
std::vector<ESM::Pathgrid::Point> mAllowedNodes; std::vector<osg::Vec3f> mAllowedPositions;
ESM::Pathgrid::Point mCurrentNode; osg::Vec3f mCurrentPosition;
bool mTrimCurrentNode; bool mTrimCurrentPosition;
float mCheckIdlePositionTimer; float mCheckIdlePositionTimer;
int mStuckCount; int mStuckCount;
@ -132,7 +132,8 @@ namespace MWMechanics
bool playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); bool playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect);
bool checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); bool checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect);
int getRandomIdle() const; 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 evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage);
void doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, void doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration,
MWWorld::MovementDirectionFlags supportedMovementDirections, AiWanderStorage& storage); MWWorld::MovementDirectionFlags supportedMovementDirections, AiWanderStorage& storage);
@ -145,28 +146,27 @@ namespace MWMechanics
void wanderNearStart(const MWWorld::Ptr& actor, AiWanderStorage& storage, int wanderDistance); void wanderNearStart(const MWWorld::Ptr& actor, AiWanderStorage& storage, int wanderDistance);
bool destinationIsAtWater(const MWWorld::Ptr& actor, const osg::Vec3f& destination); bool destinationIsAtWater(const MWWorld::Ptr& actor, const osg::Vec3f& destination);
void completeManualWalking(const MWWorld::Ptr& actor, AiWanderStorage& storage); 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 // how far the actor can wander from the spawn point
const int mDuration; const unsigned mDistance;
const unsigned mDuration;
float mRemainingDuration; float mRemainingDuration;
const int mTimeOfDay; const int mTimeOfDay;
const std::vector<unsigned char> mIdle; const std::vector<unsigned char> mIdle;
bool mStoredInitialActorPosition; bool mStoredInitialActorPosition;
osg::Vec3f // Note: an original engine does not reset coordinates even when actor changes a cell
mInitialActorPosition; // Note: an original engine does not reset coordinates even when actor changes a cell osg::Vec3f mInitialActorPosition;
bool mHasDestination; bool mHasDestination;
osg::Vec3f mDestination; osg::Vec3f mDestination;
bool mUsePathgrid; bool mUsePathgrid;
void getNeighbouringNodes( 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 fillAllowedPositions(const MWWorld::Ptr& actor, AiWanderStorage& storage);
void trimAllowedNodes(std::vector<ESM::Pathgrid::Point>& nodes, const PathFinder& pathfinder);
// constants for converting idleSelect values into groupNames // constants for converting idleSelect values into groupNames
enum GroupIndex enum GroupIndex
@ -175,12 +175,12 @@ namespace MWMechanics
GroupIndex_MaxIdle = 9 GroupIndex_MaxIdle = 9
}; };
void setCurrentNodeToClosestAllowedNode(AiWanderStorage& storage); void setCurrentPositionToClosestAllowedPosition(AiWanderStorage& storage);
void addNonPathGridAllowedPoints(const ESM::Pathgrid* pathGrid, size_t pointIndex, AiWanderStorage& storage, void addNonPathGridAllowedPoints(const ESM::Pathgrid* pathGrid, size_t pointIndex, AiWanderStorage& storage,
const Misc::CoordinateConverter& converter); const Misc::CoordinateConverter& converter);
void AddPointBetweenPathGridPoints( void addPositionBetweenPathgridPoints(
const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage); const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage);
/// lookup table for converting idleSelect value to groupName /// lookup table for converting idleSelect value to groupName

View file

@ -2310,17 +2310,13 @@ namespace MWMechanics
} }
else else
{ {
// Do not play turning animation for player if rotation speed is very slow.
// Actual threshold should take framerate in account.
float rotationThreshold = (isPlayer ? 0.015f : 0.001f) * 60 * duration;
// It seems only bipedal actors use turning animations. // It seems only bipedal actors use turning animations.
// Also do not use turning animations in the first-person view and when sneaking. // Also do not use turning animations in the first-person view and when sneaking.
if (!sneak && !isFirstPersonPlayer && isBiped) if (!sneak && !isFirstPersonPlayer && isBiped)
{ {
if (effectiveRotation > rotationThreshold) if (effectiveRotation > 0.f)
movestate = inwater ? CharState_SwimTurnRight : CharState_TurnRight; movestate = inwater ? CharState_SwimTurnRight : CharState_TurnRight;
else if (effectiveRotation < -rotationThreshold) else if (effectiveRotation < 0.f)
movestate = inwater ? CharState_SwimTurnLeft : CharState_TurnLeft; movestate = inwater ? CharState_SwimTurnLeft : CharState_TurnLeft;
} }
} }
@ -2346,34 +2342,19 @@ namespace MWMechanics
vec.y() *= std::sqrt(1.0f - swimUpwardCoef * swimUpwardCoef); vec.y() *= std::sqrt(1.0f - swimUpwardCoef * swimUpwardCoef);
} }
// Player can not use smooth turning as NPCs, so we play turning animation a bit to avoid jittering if (isBiped)
if (isPlayer)
{ {
float threshold = mCurrentMovement.find("swim") == std::string::npos ? 0.4f : 0.8f; if (mTurnAnimationThreshold > 0)
float complete; mTurnAnimationThreshold -= duration;
bool animPlaying = mAnimation->getInfo(mCurrentMovement, &complete);
if (movestate == CharState_None && jumpstate == JumpState_None && isTurning())
{
if (animPlaying && complete < threshold)
movestate = mMovementState;
}
}
else
{
if (isBiped)
{
if (mTurnAnimationThreshold > 0)
mTurnAnimationThreshold -= duration;
if (movestate == CharState_TurnRight || movestate == CharState_TurnLeft if (movestate == CharState_TurnRight || movestate == CharState_TurnLeft
|| movestate == CharState_SwimTurnRight || movestate == CharState_SwimTurnLeft) || movestate == CharState_SwimTurnRight || movestate == CharState_SwimTurnLeft)
{ {
mTurnAnimationThreshold = 0.05f; mTurnAnimationThreshold = 0.05f;
} }
else if (movestate == CharState_None && isTurning() && mTurnAnimationThreshold > 0) else if (movestate == CharState_None && isTurning() && mTurnAnimationThreshold > 0)
{ {
movestate = mMovementState; movestate = mMovementState;
}
} }
} }
@ -2402,11 +2383,10 @@ namespace MWMechanics
if (isTurning()) if (isTurning())
{ {
// Adjust animation speed from 1.0 to 1.5 multiplier
if (duration > 0) if (duration > 0)
{ {
float turnSpeed = std::min(1.5f, std::abs(rot.z()) / duration / static_cast<float>(osg::PI)); float turnSpeed = std::min(1.5f, std::abs(rot.z()) / duration / static_cast<float>(osg::PI));
mAnimation->adjustSpeedMult(mCurrentMovement, std::max(turnSpeed, 1.0f)); mAnimation->adjustSpeedMult(mCurrentMovement, turnSpeed);
} }
} }
else if (mMovementState != CharState_None && mAdjustMovementAnimSpeed) else if (mMovementState != CharState_None && mAdjustMovementAnimSpeed)

View file

@ -106,21 +106,6 @@ namespace MWMechanics
return visitor.mResult; return visitor.mResult;
} }
bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, bool ignorePlayer,
std::vector<MWWorld::Ptr>* 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() ObstacleCheck::ObstacleCheck()
: mEvadeDirectionIndex(std::size(evadeDirections) - 1) : mEvadeDirectionIndex(std::size(evadeDirections) - 1)
{ {

View file

@ -5,8 +5,6 @@
#include <osg/Vec3f> #include <osg/Vec3f>
#include <vector>
namespace MWWorld namespace MWWorld
{ {
class Ptr; class Ptr;
@ -24,9 +22,6 @@ namespace MWMechanics
/** \return Pointer to the door, or empty pointer if none exists **/ /** \return Pointer to the door, or empty pointer if none exists **/
const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist); 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<MWWorld::Ptr>* occupyingActors = nullptr);
class ObstacleCheck class ObstacleCheck
{ {
public: public:

View file

@ -10,6 +10,7 @@
#include <components/detournavigator/navigatorutils.hpp> #include <components/detournavigator/navigatorutils.hpp>
#include <components/misc/coordinateconverter.hpp> #include <components/misc/coordinateconverter.hpp>
#include <components/misc/math.hpp> #include <components/misc/math.hpp>
#include <components/misc/pathgridutils.hpp>
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
@ -38,7 +39,7 @@ namespace
// points to a quadtree may help // points to a quadtree may help
for (size_t counter = 0; counter < grid->mPoints.size(); counter++) 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) if (potentialDistBetween < closestDistanceReachable)
{ {
// found a closer one // found a closer one
@ -197,7 +198,7 @@ namespace MWMechanics
// point right behind the wall that is closer than any pathgrid // point right behind the wall that is closer than any pathgrid
// point outside the wall // point outside the wall
osg::Vec3f startPointInLocalCoords(converter.toLocalVec3(startPoint)); 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)); osg::Vec3f endPointInLocalCoords(converter.toLocalVec3(endPoint));
std::pair<size_t, bool> endNode std::pair<size_t, bool> endNode
@ -206,8 +207,8 @@ namespace MWMechanics
// if it's shorter for actor to travel from start to end, than to travel from either // 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. // start or end to nearest pathgrid point, just travel from start to end.
float startToEndLength2 = (endPointInLocalCoords - startPointInLocalCoords).length2(); float startToEndLength2 = (endPointInLocalCoords - startPointInLocalCoords).length2();
float endTolastNodeLength2 = distanceSquared(pathgrid->mPoints[endNode.first], endPointInLocalCoords); float endTolastNodeLength2 = Misc::distanceSquared(pathgrid->mPoints[endNode.first], endPointInLocalCoords);
float startTo1stNodeLength2 = distanceSquared(pathgrid->mPoints[startNode], startPointInLocalCoords); float startTo1stNodeLength2 = Misc::distanceSquared(pathgrid->mPoints[startNode], startPointInLocalCoords);
if ((startToEndLength2 < startTo1stNodeLength2) || (startToEndLength2 < endTolastNodeLength2)) if ((startToEndLength2 < startTo1stNodeLength2) || (startToEndLength2 < endTolastNodeLength2))
{ {
*out++ = endPoint; *out++ = endPoint;
@ -223,7 +224,7 @@ namespace MWMechanics
{ {
ESM::Pathgrid::Point temp(pathgrid->mPoints[startNode]); ESM::Pathgrid::Point temp(pathgrid->mPoints[startNode]);
converter.toWorld(temp); converter.toWorld(temp);
*out++ = makeOsgVec3(temp); *out++ = Misc::Convert::makeOsgVec3f(temp);
} }
else else
{ {
@ -234,8 +235,8 @@ namespace MWMechanics
if (path.size() > 1) if (path.size() > 1)
{ {
ESM::Pathgrid::Point secondNode = *(++path.begin()); ESM::Pathgrid::Point secondNode = *(++path.begin());
osg::Vec3f firstNodeVec3f = makeOsgVec3(pathgrid->mPoints[startNode]); osg::Vec3f firstNodeVec3f = Misc::Convert::makeOsgVec3f(pathgrid->mPoints[startNode]);
osg::Vec3f secondNodeVec3f = makeOsgVec3(secondNode); osg::Vec3f secondNodeVec3f = Misc::Convert::makeOsgVec3f(secondNode);
osg::Vec3f toSecondNodeVec3f = secondNodeVec3f - firstNodeVec3f; osg::Vec3f toSecondNodeVec3f = secondNodeVec3f - firstNodeVec3f;
osg::Vec3f toStartPointVec3f = startPointInLocalCoords - firstNodeVec3f; osg::Vec3f toStartPointVec3f = startPointInLocalCoords - firstNodeVec3f;
if (toSecondNodeVec3f * toStartPointVec3f > 0) if (toSecondNodeVec3f * toStartPointVec3f > 0)
@ -259,7 +260,7 @@ namespace MWMechanics
// convert supplied path to world coordinates // convert supplied path to world coordinates
std::transform(path.begin(), path.end(), out, [&](ESM::Pathgrid::Point& point) { std::transform(path.begin(), path.end(), out, [&](ESM::Pathgrid::Point& point) {
converter.toWorld(point); converter.toWorld(point);
return makeOsgVec3(point); return Misc::Convert::makeOsgVec3f(point);
}); });
} }
@ -359,26 +360,16 @@ namespace MWMechanics
mConstructed = true; 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, void PathFinder::buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint,
const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, 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<const osg::Vec3f> checkpoints)
{ {
mPath.clear(); mPath.clear();
// If it's not possible to build path over navmesh due to disabled navmesh generation fallback to straight path // 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, 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) if (status != DetourNavigator::Status::Success)
mPath.clear(); mPath.clear();
@ -390,19 +381,19 @@ namespace MWMechanics
} }
void PathFinder::buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, void PathFinder::buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) PathType pathType, std::span<const osg::Vec3f> checkpoints)
{ {
mPath.clear(); mPath.clear();
mCell = cell; mCell = actor.getCell();
DetourNavigator::Status status = DetourNavigator::Status::NavMeshNotFound; DetourNavigator::Status status = DetourNavigator::Status::NavMeshNotFound;
if (!actor.getClass().isPureWaterCreature(actor) && !actor.getClass().isPureFlyingCreature(actor)) if (!actor.getClass().isPureWaterCreature(actor) && !actor.getClass().isPureFlyingCreature(actor))
{ {
status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, flags, areaCosts, endTolerance, 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) if (status != DetourNavigator::Status::Success)
mPath.clear(); mPath.clear();
} }
@ -411,7 +402,7 @@ namespace MWMechanics
&& (flags & DetourNavigator::Flag_usePathgrid) == 0) && (flags & DetourNavigator::Flag_usePathgrid) == 0)
{ {
status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, 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)); std::back_inserter(mPath));
if (status != DetourNavigator::Status::Success) if (status != DetourNavigator::Status::Success)
mPath.clear(); mPath.clear();
@ -429,12 +420,13 @@ namespace MWMechanics
DetourNavigator::Status PathFinder::buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, DetourNavigator::Status PathFinder::buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor,
const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
PathType pathType, std::back_insert_iterator<std::deque<osg::Vec3f>> out) PathType pathType, std::span<const osg::Vec3f> checkpoints,
std::back_insert_iterator<std::deque<osg::Vec3f>> out)
{ {
const auto world = MWBase::Environment::get().getWorld(); const MWBase::World& world = *MWBase::Environment::get().getWorld();
const auto navigator = world->getNavigator(); const DetourNavigator::Navigator& navigator = *world.getNavigator();
const auto status = DetourNavigator::findPath( const DetourNavigator::Status status = DetourNavigator::findPath(
*navigator, agentBounds, startPoint, endPoint, flags, areaCosts, endTolerance, out); navigator, agentBounds, startPoint, endPoint, flags, areaCosts, endTolerance, checkpoints, out);
if (pathType == PathType::Partial && status == DetourNavigator::Status::PartialPath) if (pathType == PathType::Partial && status == DetourNavigator::Status::PartialPath)
return DetourNavigator::Status::Success; return DetourNavigator::Status::Success;
@ -451,9 +443,9 @@ namespace MWMechanics
} }
void PathFinder::buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, void PathFinder::buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint,
const osg::Vec3f& endPoint, const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const osg::Vec3f& endPoint, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) PathType pathType)
{ {
const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
const auto maxDistance const auto maxDistance
@ -461,9 +453,9 @@ namespace MWMechanics
const auto startToEnd = endPoint - startPoint; const auto startToEnd = endPoint - startPoint;
const auto distance = startToEnd.length(); const auto distance = startToEnd.length();
if (distance <= maxDistance) if (distance <= maxDistance)
return buildPath(actor, startPoint, endPoint, cell, pathgridGraph, agentBounds, flags, areaCosts, return buildPath(
endTolerance, pathType); actor, startPoint, endPoint, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType);
const auto end = startPoint + startToEnd * maxDistance / distance; 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);
} }
} }

View file

@ -4,12 +4,13 @@
#include <cassert> #include <cassert>
#include <deque> #include <deque>
#include <iterator> #include <iterator>
#include <span>
#include <osg/Vec3f>
#include <components/detournavigator/areatype.hpp> #include <components/detournavigator/areatype.hpp>
#include <components/detournavigator/flags.hpp> #include <components/detournavigator/flags.hpp>
#include <components/detournavigator/status.hpp> #include <components/detournavigator/status.hpp>
#include <components/esm/position.hpp>
#include <components/esm3/loadpgrd.hpp>
namespace MWWorld namespace MWWorld
{ {
@ -102,23 +103,20 @@ namespace MWMechanics
void buildStraightPath(const osg::Vec3f& endPoint); 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, void buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint,
const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
PathType pathType); PathType pathType, std::span<const osg::Vec3f> checkpoints = {});
void buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, void buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType); PathType pathType, std::span<const osg::Vec3f> checkpoints = {});
void buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, void buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType); PathType pathType);
/// Remove front point if exist and within tolerance /// Remove front point if exist and within tolerance
void update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance, void update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance,
@ -145,61 +143,6 @@ namespace MWMechanics
mPath.push_back(point); 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<int>(v[0]), static_cast<int>(v[1]), static_cast<int>(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<int>(p.pos[0]), static_cast<int>(p.pos[1]), static_cast<int>(p.pos[2]));
}
static osg::Vec3f makeOsgVec3(const ESM::Pathgrid::Point& p)
{
return osg::Vec3f(static_cast<float>(p.mX), static_cast<float>(p.mY), static_cast<float>(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: private:
bool mConstructed = false; bool mConstructed = false;
std::deque<osg::Vec3f> mPath; std::deque<osg::Vec3f> mPath;
@ -211,7 +154,8 @@ namespace MWMechanics
[[nodiscard]] DetourNavigator::Status buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, [[nodiscard]] DetourNavigator::Status buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor,
const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
PathType pathType, std::back_insert_iterator<std::deque<osg::Vec3f>> out); PathType pathType, std::span<const osg::Vec3f> checkpoints,
std::back_insert_iterator<std::deque<osg::Vec3f>> out);
}; };
} }

View file

@ -215,10 +215,6 @@ namespace MWMechanics
bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration);
effect.mDuration = hasDuration ? static_cast<float>(enam.mData.mDuration) : 1.f; effect.mDuration = hasDuration ? static_cast<float>(enam.mData.mDuration) : 1.f;
bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce;
if (!appliedOnce)
effect.mDuration = std::max(1.f, effect.mDuration);
effect.mTimeLeft = effect.mDuration; effect.mTimeLeft = effect.mDuration;
// add to list of active effects, to apply in next frame // add to list of active effects, to apply in next frame

View file

@ -377,8 +377,11 @@ namespace
MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}"); MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}");
return MWMechanics::MagicApplicationResult::Type::REMOVED; return MWMechanics::MagicApplicationResult::Type::REMOVED;
} }
effect.mMinMagnitude *= magnitudeMult; else if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude))
effect.mMaxMagnitude *= magnitudeMult; {
effect.mMinMagnitude *= magnitudeMult;
effect.mMaxMagnitude *= magnitudeMult;
}
} }
return MWMechanics::MagicApplicationResult::Type::APPLIED; return MWMechanics::MagicApplicationResult::Type::APPLIED;
} }
@ -1011,11 +1014,13 @@ namespace MWMechanics
else else
{ {
// Morrowind.exe doesn't apply magic effects while the menu is open, we do because we like to see stats // Morrowind.exe doesn't apply magic effects while the menu is open, we do because we like to see stats
// updated instantly. We don't want to teleport instantly though // updated instantly. We don't want to teleport instantly though. Nor do we want to force players to drink
// invisibility potions in the "right" order
if (!dt if (!dt
&& (effect.mEffectId == ESM::MagicEffect::Recall && (effect.mEffectId == ESM::MagicEffect::Recall
|| effect.mEffectId == ESM::MagicEffect::DivineIntervention || effect.mEffectId == ESM::MagicEffect::DivineIntervention
|| effect.mEffectId == ESM::MagicEffect::AlmsiviIntervention)) || effect.mEffectId == ESM::MagicEffect::AlmsiviIntervention
|| effect.mEffectId == ESM::MagicEffect::Invisibility))
return { MagicApplicationResult::Type::APPLIED, receivedMagicDamage, affectedHealth }; return { MagicApplicationResult::Type::APPLIED, receivedMagicDamage, affectedHealth };
auto& stats = target.getClass().getCreatureStats(target); auto& stats = target.getClass().getCreatureStats(target);
auto& magnitudes = stats.getMagicEffects(); auto& magnitudes = stats.getMagicEffects();

View file

@ -10,7 +10,7 @@
namespace MWPhysics namespace MWPhysics
{ {
// https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_collision_detection // 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& aabbMin, const btVector3& aabbMax, const btVector3& position, const btScalar radius)
{ {
const btVector3 nearest(std::clamp(position.x(), aabbMin.x(), aabbMax.x()), const btVector3 nearest(std::clamp(position.x(), aabbMin.x(), aabbMax.x()),
@ -18,35 +18,28 @@ namespace MWPhysics
return nearest.distance(position) < radius; return nearest.distance(position) < radius;
} }
template <class Ignore, class OnCollision>
class HasSphereCollisionCallback final : public btBroadphaseAabbCallback class HasSphereCollisionCallback final : public btBroadphaseAabbCallback
{ {
public: public:
HasSphereCollisionCallback(const btVector3& position, const btScalar radius, const int mask, const int group, explicit HasSphereCollisionCallback(const btVector3& position, const btScalar radius, const int mask,
const Ignore& ignore, OnCollision* onCollision) const int group, const btCollisionObject* ignore)
: mPosition(position) : mPosition(position)
, mRadius(radius) , mRadius(radius)
, mIgnore(ignore) , mIgnore(ignore)
, mCollisionFilterMask(mask) , mCollisionFilterMask(mask)
, mCollisionFilterGroup(group) , mCollisionFilterGroup(group)
, mOnCollision(onCollision)
{ {
} }
bool process(const btBroadphaseProxy* proxy) override bool process(const btBroadphaseProxy* proxy) override
{ {
if (mResult && mOnCollision == nullptr) if (mResult)
return false; return false;
const auto collisionObject = static_cast<btCollisionObject*>(proxy->m_clientObject); const auto collisionObject = static_cast<btCollisionObject*>(proxy->m_clientObject);
if (mIgnore(collisionObject) || !needsCollision(*proxy) if (mIgnore == collisionObject || !needsCollision(*proxy)
|| !testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius)) || !testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius))
return true; return true;
mResult = true; mResult = true;
if (mOnCollision != nullptr)
{
(*mOnCollision)(collisionObject);
return true;
}
return !mResult; return !mResult;
} }
@ -55,10 +48,9 @@ namespace MWPhysics
private: private:
btVector3 mPosition; btVector3 mPosition;
btScalar mRadius; btScalar mRadius;
Ignore mIgnore; const btCollisionObject* mIgnore;
int mCollisionFilterMask; int mCollisionFilterMask;
int mCollisionFilterGroup; int mCollisionFilterGroup;
OnCollision* mOnCollision;
bool mResult = false; bool mResult = false;
bool needsCollision(const btBroadphaseProxy& proxy) const bool needsCollision(const btBroadphaseProxy& proxy) const

View file

@ -849,36 +849,18 @@ namespace MWPhysics
mWaterCollisionObject.get(), CollisionType_Water, CollisionType_Actor | CollisionType_Projectile); mWaterCollisionObject.get(), CollisionType_Water, CollisionType_Actor | CollisionType_Projectile);
} }
bool PhysicsSystem::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, bool PhysicsSystem::isAreaOccupiedByOtherActor(
std::span<const MWWorld::ConstPtr> ignore, std::vector<MWWorld::Ptr>* occupyingActors) const const MWWorld::LiveCellRefBase* actor, const osg::Vec3f& position, const float radius) const
{ {
std::vector<const btCollisionObject*> ignoredObjects; const btCollisionObject* ignoredObject = nullptr;
ignoredObjects.reserve(ignore.size()); if (const auto it = mActors.find(actor); it != mActors.end())
for (const auto& v : ignore) ignoredObject = it->second->getCollisionObject();
if (const auto it = mActors.find(v.mRef); it != mActors.end()) const btVector3 bulletPosition = Misc::Convert::toBullet(position);
ignoredObjects.push_back(it->second->getCollisionObject()); const btVector3 aabbMin = bulletPosition - btVector3(radius, radius, radius);
std::sort(ignoredObjects.begin(), ignoredObjects.end()); const btVector3 aabbMax = bulletPosition + btVector3(radius, radius, radius);
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 int mask = MWPhysics::CollisionType_Actor; const int mask = MWPhysics::CollisionType_Actor;
const int group = MWPhysics::CollisionType_AnyPhysical; const int group = MWPhysics::CollisionType_AnyPhysical;
if (occupyingActors == nullptr) HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoredObject);
{
HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter,
static_cast<void (*)(const btCollisionObject*)>(nullptr));
mTaskScheduler->aabbTest(aabbMin, aabbMax, callback);
return callback.getResult();
}
const auto onCollision = [&](const btCollisionObject* object) {
if (PtrHolder* holder = static_cast<PtrHolder*>(object->getUserPointer()))
occupyingActors->push_back(holder->getPtr());
};
HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter, &onCollision);
mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); mTaskScheduler->aabbTest(aabbMin, aabbMax, callback);
return callback.getResult(); return callback.getResult();
} }

View file

@ -281,8 +281,8 @@ namespace MWPhysics
std::for_each(mAnimatedObjects.begin(), mAnimatedObjects.end(), function); std::for_each(mAnimatedObjects.begin(), mAnimatedObjects.end(), function);
} }
bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, bool isAreaOccupiedByOtherActor(
std::span<const MWWorld::ConstPtr> ignore, std::vector<MWWorld::Ptr>* occupyingActors) const; const MWWorld::LiveCellRefBase* actor, const osg::Vec3f& position, float radius) const;
void reportStats(unsigned int frameNumber, osg::Stats& stats) const; void reportStats(unsigned int frameNumber, osg::Stats& stats) const;
void reportCollision(const btVector3& position, const btVector3& normal); void reportCollision(const btVector3& position, const btVector3& normal);

View file

@ -566,10 +566,10 @@ namespace MWWorld
std::vector<Ref> refs; std::vector<Ref> refs;
std::set<ESM::RefId> keyIDs; std::set<ESM::RefId> keyIDs;
std::vector<ESM::RefId> refIDs; std::vector<ESM::RefId> refIDs;
Store<ESM::Cell> Cells = get<ESM::Cell>(); const Store<ESM::Cell>& cells = get<ESM::Cell>();
for (auto it = Cells.intBegin(); it != Cells.intEnd(); ++it) for (auto it = cells.intBegin(); it != cells.intEnd(); ++it)
readRefs(*it, refs, refIDs, keyIDs, readers); readRefs(*it, refs, refIDs, keyIDs, readers);
for (auto it = Cells.extBegin(); it != Cells.extEnd(); ++it) for (auto it = cells.extBegin(); it != cells.extEnd(); ++it)
readRefs(*it, refs, refIDs, keyIDs, readers); readRefs(*it, refs, refIDs, keyIDs, readers);
const auto lessByRefNum = [](const Ref& l, const Ref& r) { return l.mRefNum < r.mRefNum; }; const auto lessByRefNum = [](const Ref& l, const Ref& r) { return l.mRefNum < r.mRefNum; };
std::stable_sort(refs.begin(), refs.end(), lessByRefNum); std::stable_sort(refs.begin(), refs.end(), lessByRefNum);

View file

@ -3871,10 +3871,11 @@ namespace MWWorld
return btRayAabb(localFrom, localTo, aabbMin, aabbMax, hitDistance, hitNormal); return btRayAabb(localFrom, localTo, aabbMin, aabbMax, hitDistance, hitNormal);
} }
bool World::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, bool World::isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const
std::span<const MWWorld::ConstPtr> ignore, std::vector<MWWorld::Ptr>* occupyingActors) 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 void World::reportStats(unsigned int frameNumber, osg::Stats& stats) const

View file

@ -664,8 +664,7 @@ namespace MWWorld
bool hasCollisionWithDoor( bool hasCollisionWithDoor(
const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const override; const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const override;
bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const override;
std::span<const MWWorld::ConstPtr> ignore, std::vector<MWWorld::Ptr>* occupyingActors) const override;
void reportStats(unsigned int frameNumber, osg::Stats& stats) const override; void reportStats(unsigned int frameNumber, osg::Stats& stats) const override;

View file

@ -7,6 +7,7 @@
#include <components/esm3/loadnpc.hpp> #include <components/esm3/loadnpc.hpp>
#include <components/esm3/readerscache.hpp> #include <components/esm3/readerscache.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h> #include <gtest/gtest.h>
namespace MWWorld namespace MWWorld
@ -36,7 +37,7 @@ namespace MWWorld
LiveCellRef<ESM::NPC> liveCellRef(cellRef, &npc); LiveCellRef<ESM::NPC> liveCellRef(cellRef, &npc);
liveCellRef.mData.setDeletedByContentFile(true); liveCellRef.mData.setDeletedByContentFile(true);
Ptr ptr(&liveCellRef); Ptr ptr(&liveCellRef);
EXPECT_EQ(ptr.toString(), "deleted object0xd00002a (NPC, \"player\")"); EXPECT_THAT(ptr.toString(), StrCaseEq("deleted object0xd00002a (NPC, \"player\")"));
} }
TEST(MWWorldPtrTest, toStringShouldReturnHumanReadableTextRepresentationOfPtr) TEST(MWWorldPtrTest, toStringShouldReturnHumanReadableTextRepresentationOfPtr)
@ -53,7 +54,7 @@ namespace MWWorld
cellRef.mRefNum = ESM::RefNum{ .mIndex = 0x2a, .mContentFile = 0xd }; cellRef.mRefNum = ESM::RefNum{ .mIndex = 0x2a, .mContentFile = 0xd };
LiveCellRef<ESM::NPC> liveCellRef(cellRef, &npc); LiveCellRef<ESM::NPC> liveCellRef(cellRef, &npc);
Ptr ptr(&liveCellRef); Ptr ptr(&liveCellRef);
EXPECT_EQ(ptr.toString(), "object0xd00002a (NPC, \"player\")"); EXPECT_THAT(ptr.toString(), StrCaseEq("object0xd00002a (NPC, \"player\")"));
} }
TEST(MWWorldPtrTest, underlyingLiveCellRefShouldBeDeregisteredOnDestruction) TEST(MWWorldPtrTest, underlyingLiveCellRefShouldBeDeregisteredOnDestruction)

View file

@ -13,7 +13,6 @@
#include <osg/Vec3f> #include <osg/Vec3f>
#include <array>
#include <cassert> #include <cassert>
#include <functional> #include <functional>
#include <iterator> #include <iterator>
@ -64,6 +63,25 @@ namespace DetourNavigator
std::reference_wrapper<const RecastSettings> mSettings; std::reference_wrapper<const RecastSettings> mSettings;
}; };
template <class T>
class ToNavMeshCoordinatesSpan
{
public:
explicit ToNavMeshCoordinatesSpan(std::span<T> 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<T> mSpan;
const RecastSettings& mSettings;
};
inline std::optional<std::size_t> findPolygonPath(const dtNavMeshQuery& navMeshQuery, const dtPolyRef startRef, inline std::optional<std::size_t> findPolygonPath(const dtNavMeshQuery& navMeshQuery, const dtPolyRef startRef,
const dtPolyRef endRef, const osg::Vec3f& startPos, const osg::Vec3f& endPos, const dtQueryFilter& queryFilter, const dtPolyRef endRef, const osg::Vec3f& startPos, const osg::Vec3f& endPos, const dtQueryFilter& queryFilter,
std::span<dtPolyRef> pathBuffer) std::span<dtPolyRef> pathBuffer)
@ -79,7 +97,7 @@ namespace DetourNavigator
} }
Status makeSmoothPath(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& start, const osg::Vec3f& end, Status makeSmoothPath(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& start, const osg::Vec3f& end,
std::span<dtPolyRef> polygonPath, std::size_t polygonPathSize, std::size_t maxSmoothPathSize, std::span<dtPolyRef> polygonPath, std::size_t polygonPathSize, std::size_t maxSmoothPathSize, bool skipFirst,
std::output_iterator<osg::Vec3f> auto& out) std::output_iterator<osg::Vec3f> auto& out)
{ {
assert(polygonPathSize <= polygonPath.size()); assert(polygonPathSize <= polygonPath.size());
@ -95,7 +113,7 @@ namespace DetourNavigator
dtStatusFailed(status)) dtStatusFailed(status))
return Status::FindStraightPathFailed; 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<std::size_t>(i) * 3]); *out++ = Misc::Convert::makeOsgVec3f(&cornerVertsBuffer[static_cast<std::size_t>(i) * 3]);
return Status::Success; return Status::Success;
@ -103,7 +121,8 @@ namespace DetourNavigator
Status findSmoothPath(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& halfExtents, const osg::Vec3f& start, 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, const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, const DetourSettings& settings,
float endTolerance, std::output_iterator<osg::Vec3f> auto out) float endTolerance, const ToNavMeshCoordinatesSpan<const osg::Vec3f>& checkpoints,
std::output_iterator<osg::Vec3f> auto out)
{ {
dtQueryFilter queryFilter; dtQueryFilter queryFilter;
queryFilter.setIncludeFlags(includeFlags); queryFilter.setIncludeFlags(includeFlags);
@ -131,29 +150,66 @@ namespace DetourNavigator
return Status::EndPolygonNotFound; return Status::EndPolygonNotFound;
std::vector<dtPolyRef> polygonPath(settings.mMaxPolygonPathSize); std::vector<dtPolyRef> polygonPath(settings.mMaxPolygonPathSize);
const auto polygonPathSize std::span<dtPolyRef> polygonPathBuffer = polygonPath;
= findPolygonPath(navMeshQuery, startRef, endRef, startNavMeshPos, endNavMeshPos, queryFilter, 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<std::size_t> 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<std::size_t> toEndPathSize = findPolygonPath(
navMeshQuery, currentRef, endRef, currentNavMeshPos, endNavMeshPos, queryFilter, polygonPathBuffer);
if (!toEndPathSize.has_value())
return Status::FindPathOverPolygonsFailed; return Status::FindPathOverPolygonsFailed;
if (*polygonPathSize == 0) if (*toEndPathSize == 0)
return Status::Success; return currentRef == endRef ? Status::Success : Status::PartialPath;
osg::Vec3f targetNavMeshPos; osg::Vec3f targetNavMeshPos;
if (const dtStatus status = navMeshQuery.closestPointOnPoly( if (const dtStatus status = navMeshQuery.closestPointOnPoly(
polygonPath[*polygonPathSize - 1], end.ptr(), targetNavMeshPos.ptr(), nullptr); polygonPath[*toEndPathSize - 1], end.ptr(), targetNavMeshPos.ptr(), nullptr);
dtStatusFailed(status)) dtStatusFailed(status))
return Status::TargetPolygonNotFound; return Status::TargetPolygonNotFound;
const bool partialPath = polygonPath[*polygonPathSize - 1] != endRef; const Status smoothStatus = makeSmoothPath(navMeshQuery, currentNavMeshPos, targetNavMeshPos, polygonPath,
const Status smoothStatus = makeSmoothPath(navMeshQuery, startNavMeshPos, targetNavMeshPos, polygonPath, *toEndPathSize, settings.mMaxSmoothPathSize, skipFirst, out);
*polygonPathSize, settings.mMaxSmoothPathSize, out);
if (smoothStatus != Status::Success) if (smoothStatus != Status::Success)
return smoothStatus; return smoothStatus;
return partialPath ? Status::PartialPath : Status::Success; return polygonPath[*toEndPathSize - 1] == endRef ? Status::Success : Status::PartialPath;
} }
} }

View file

@ -11,6 +11,7 @@
#include <iterator> #include <iterator>
#include <optional> #include <optional>
#include <span>
namespace DetourNavigator namespace DetourNavigator
{ {
@ -21,13 +22,13 @@ namespace DetourNavigator
* @param end path at given point. * @param end path at given point.
* @param includeFlags setup allowed navmesh areas. * @param includeFlags setup allowed navmesh areas.
* @param out the beginning of the destination range. * @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. * @return Status.
* Equal to out if no path is found.
*/ */
inline Status findPath(const Navigator& navigator, const AgentBounds& agentBounds, const osg::Vec3f& start, 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, const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, float endTolerance,
std::output_iterator<osg::Vec3f> auto out) std::span<const osg::Vec3f> checkpoints, std::output_iterator<osg::Vec3f> auto out)
{ {
const auto navMesh = navigator.getNavMesh(agentBounds); const auto navMesh = navigator.getNavMesh(agentBounds);
if (navMesh == nullptr) if (navMesh == nullptr)
@ -37,7 +38,8 @@ namespace DetourNavigator
const auto locked = navMesh->lock(); const auto locked = navMesh->lock();
return findSmoothPath(locked->getQuery(), toNavMeshCoordinates(settings.mRecast, agentBounds.mHalfExtents), return findSmoothPath(locked->getQuery(), toNavMeshCoordinates(settings.mRecast, agentBounds.mHalfExtents),
toNavMeshCoordinates(settings.mRecast, start), toNavMeshCoordinates(settings.mRecast, end), includeFlags, toNavMeshCoordinates(settings.mRecast, start), toNavMeshCoordinates(settings.mRecast, end), includeFlags,
areaCosts, settings.mDetour, endTolerance, outTransform); areaCosts, settings.mDetour, endTolerance, ToNavMeshCoordinatesSpan(checkpoints, settings.mRecast),
outTransform);
} }
/** /**

View file

@ -15,7 +15,9 @@ namespace ESMTerrain
inline std::pair<std::size_t, std::size_t> toCellAndLocal( inline std::pair<std::size_t, std::size_t> toCellAndLocal(
std::size_t begin, std::size_t global, std::size_t cellSize) std::size_t begin, std::size_t global, std::size_t cellSize)
{ {
// NOLINTBEGIN(clang-analyzer-core.UndefinedBinaryOperatorResult)
std::size_t cell = global / (cellSize - 1); std::size_t cell = global / (cellSize - 1);
// NOLINTEND(clang-analyzer-core.UndefinedBinaryOperatorResult)
std::size_t local = global & (cellSize - 2); std::size_t local = global & (cellSize - 2);
if (global != begin && local == 0) if (global != begin && local == 0)
{ {

View file

@ -59,10 +59,18 @@ namespace Misc
point.y() -= static_cast<float>(mCellY); point.y() -= static_cast<float>(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 osg::Vec3f toLocalVec3(const osg::Vec3f& point) const
{ {
return osg::Vec3f( osg::Vec3f result = point;
point.x() - static_cast<float>(mCellX), point.y() - static_cast<float>(mCellY), point.z()); toLocal(result);
return result;
} }
private: private:

View file

@ -0,0 +1,53 @@
#ifndef OPENMW_COMPONENTS_MISC_PATHGRIDUTILS_H
#define OPENMW_COMPONENTS_MISC_PATHGRIDUTILS_H
#include "convert.hpp"
#include <components/esm3/loadpgrd.hpp>
#include <osg/Vec3f>
#include <stdexcept>
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

View file

@ -229,7 +229,7 @@
-- * `boneName` - name of the bone to attach the vfx to. (default: "") -- * `boneName` - name of the bone to attach the vfx to. (default: "")
-- * `particleTextureOverride` - name of the particle texture to use. (default: "") -- * `particleTextureOverride` - name of the particle texture to use. (default: "")
-- * `vfxId` - a string ID that can be used to remove the effect later, using #removeVfx, and to avoid duplicate effects. The default value of "" can have duplicates. To avoid interaction with the engine, use unique identifiers unrelated to magic effect IDs. The engine uses this identifier to add and remove magic effects based on what effects are active on the actor. If this is set equal to the @{openmw.core#MagicEffectId} identifier of the magic effect being added, for example core.magic.EFFECT_TYPE.FireDamage, then the engine will remove it once the fire damage effect on the actor reaches 0. (Default: ""). -- * `vfxId` - a string ID that can be used to remove the effect later, using #removeVfx, and to avoid duplicate effects. The default value of "" can have duplicates. To avoid interaction with the engine, use unique identifiers unrelated to magic effect IDs. The engine uses this identifier to add and remove magic effects based on what effects are active on the actor. If this is set equal to the @{openmw.core#MagicEffectId} identifier of the magic effect being added, for example core.magic.EFFECT_TYPE.FireDamage, then the engine will remove it once the fire damage effect on the actor reaches 0. (Default: "").
-- * `useAmbientLighting` - boolean, vfx get a white ambient light attached in Morrowind. If false don't attach this. (default: true) -- * `useAmbientLight` - boolean, vfx get a white ambient light attached in Morrowind. If false don't attach this. (default: true)
-- --
-- @usage local mgef = core.magic.effects.records[myEffectName] -- @usage local mgef = core.magic.effects.records[myEffectName]
-- anim.addVfx(self, 'VFX_Hands', {boneName = 'Bip01 L Hand', particleTextureOverride = mgef.particle, loop = mgef.continuousVfx, vfxId = mgef.id..'_myuniquenamehere'}) -- anim.addVfx(self, 'VFX_Hands', {boneName = 'Bip01 L Hand', particleTextureOverride = mgef.particle, loop = mgef.continuousVfx, vfxId = mgef.id..'_myuniquenamehere'})

View file

@ -180,6 +180,7 @@
-- @field [parent=#FindPathOptions] #AreaCosts areaCosts a table defining relative cost for each type of area. -- @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 -- @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). -- 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} -- A table of parameters for @{#nearby.findRandomPointAroundCircle} and @{#nearby.castNavigationRay}

View file

@ -195,7 +195,7 @@
-- * `mwMagicVfx` - Boolean that if true causes the textureOverride parameter to only affect nodes with the Nif::RC_NiTexturingProperty property set. (default: true). -- * `mwMagicVfx` - Boolean that if true causes the textureOverride parameter to only affect nodes with the Nif::RC_NiTexturingProperty property set. (default: true).
-- * `particleTextureOverride` - Name of a particle texture that should override this effect's default texture. (default: "") -- * `particleTextureOverride` - Name of a particle texture that should override this effect's default texture. (default: "")
-- * `scale` - A number that scales the size of the vfx (Default: 1) -- * `scale` - A number that scales the size of the vfx (Default: 1)
-- * `useAmbientLighting` - boolean, vfx get a white ambient light attached in Morrowind. If false don't attach this. (default: 1) -- * `useAmbientLight` - boolean, vfx get a white ambient light attached in Morrowind. If false don't attach this. (default: true)
-- --
-- @usage -- Spawn a sanctuary effect near the player -- @usage -- Spawn a sanctuary effect near the player
-- local effect = core.magic.effects.records[core.magic.EFFECT_TYPE.Sanctuary] -- local effect = core.magic.effects.records[core.magic.EFFECT_TYPE.Sanctuary]

View file

@ -315,6 +315,7 @@ registerPlayerTest('player rotation')
registerPlayerTest('player forward running') registerPlayerTest('player forward running')
registerPlayerTest('player diagonal walking') registerPlayerTest('player diagonal walking')
registerPlayerTest('findPath') registerPlayerTest('findPath')
registerPlayerTest('findPath with checkpoints')
registerPlayerTest('findRandomPointAroundCircle') registerPlayerTest('findRandomPointAroundCircle')
registerPlayerTest('castNavigationRay') registerPlayerTest('castNavigationRay')
registerPlayerTest('findNearestNavMeshPosition') registerPlayerTest('findNearestNavMeshPosition')

View file

@ -82,6 +82,7 @@ registerGlobalTest('player rotation', 'rotating player should not lead to nan ro
registerGlobalTest('player forward running') registerGlobalTest('player forward running')
registerGlobalTest('player diagonal walking') registerGlobalTest('player diagonal walking')
registerGlobalTest('findPath') registerGlobalTest('findPath')
registerGlobalTest('findPath with checkpoints')
registerGlobalTest('findRandomPointAroundCircle') registerGlobalTest('findRandomPointAroundCircle')
registerGlobalTest('castNavigationRay') registerGlobalTest('castNavigationRay')
registerGlobalTest('findNearestNavMeshPosition') registerGlobalTest('findNearestNavMeshPosition')

View file

@ -179,6 +179,39 @@ testing.registerLocalTest('findPath',
testing.expectEqual(status, nearby.FIND_PATH_STATUS.Success, 'Status') testing.expectEqual(status, nearby.FIND_PATH_STATUS.Success, 'Status')
testing.expectLessOrEqual((path[#path] - dst):length(), 1, testing.expectLessOrEqual((path[#path] - dst):length(), 1,
'Last path point ' .. testing.formatActualExpected(path[#path], dst)) '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) end)
testing.registerLocalTest('findRandomPointAroundCircle', testing.registerLocalTest('findRandomPointAroundCircle',