Merge branch 'lua_pathfinding_bindings' into 'master'

Add bindings for navigator utils functions (#6690)

See merge request OpenMW/openmw!2128
check_span
Petr Mikheev 2 years ago
commit 643e33c11a

@ -54,7 +54,7 @@ namespace MWLua
{
auto* lua = context.mLua;
sol::table api(lua->sol(), sol::create);
api["API_REVISION"] = 27;
api["API_REVISION"] = 28;
api["quit"] = [lua]()
{
Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback();

@ -1,6 +1,9 @@
#include "luabindings.hpp"
#include <components/lua/luastate.hpp>
#include <components/detournavigator/navigator.hpp>
#include <components/detournavigator/navigatorutils.hpp>
#include <components/settings/settings.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
@ -122,6 +125,144 @@ namespace MWLua
api["containers"] = LObjectList{worldView->getContainersInScene()};
api["doors"] = LObjectList{worldView->getDoorsInScene()};
api["items"] = LObjectList{worldView->getItemsInScene()};
api["NAVIGATOR_FLAGS"] = LuaUtil::makeStrictReadOnly(
context.mLua->tableFromPairs<std::string_view, DetourNavigator::Flag>({
{"Walk", DetourNavigator::Flag_walk},
{"Swim", DetourNavigator::Flag_swim},
{"OpenDoor", DetourNavigator::Flag_openDoor},
{"UsePathgrid", DetourNavigator::Flag_usePathgrid},
}));
api["COLLISION_SHAPE_TYPE"] = LuaUtil::makeStrictReadOnly(
context.mLua->tableFromPairs<std::string_view, DetourNavigator::CollisionShapeType>({
{"Aabb", DetourNavigator::CollisionShapeType::Aabb},
{"RotatingBox", DetourNavigator::CollisionShapeType::RotatingBox},
}));
api["FIND_PATH_STATUS"] = LuaUtil::makeStrictReadOnly(
context.mLua->tableFromPairs<std::string_view, DetourNavigator::Status>({
{"Success", DetourNavigator::Status::Success},
{"PartialPath", DetourNavigator::Status::PartialPath},
{"NavMeshNotFound", DetourNavigator::Status::NavMeshNotFound},
{"StartPolygonNotFound", DetourNavigator::Status::StartPolygonNotFound},
{"EndPolygonNotFound", DetourNavigator::Status::EndPolygonNotFound},
{"MoveAlongSurfaceFailed", DetourNavigator::Status::MoveAlongSurfaceFailed},
{"FindPathOverPolygonsFailed", DetourNavigator::Status::FindPathOverPolygonsFailed},
{"GetPolyHeightFailed", DetourNavigator::Status::GetPolyHeightFailed},
{"InitNavMeshQueryFailed", DetourNavigator::Status::InitNavMeshQueryFailed},
}));
static const DetourNavigator::AgentBounds defaultAgentBounds {
DetourNavigator::defaultCollisionShapeType,
Settings::Manager::getVector3("default actor pathfind half extents", "Game"),
};
static const float defaultStepSize = 2 * std::max(defaultAgentBounds.mHalfExtents.x(), defaultAgentBounds.mHalfExtents.y());
static constexpr DetourNavigator::Flags defaultIncludeFlags = DetourNavigator::Flag_walk
| DetourNavigator::Flag_swim
| DetourNavigator::Flag_openDoor
| DetourNavigator::Flag_usePathgrid;
api["findPath"] = [] (const osg::Vec3f& source, const osg::Vec3f& destination,
const sol::optional<sol::table>& options)
{
DetourNavigator::AgentBounds agentBounds = defaultAgentBounds;
float stepSize = defaultStepSize;
DetourNavigator::Flags includeFlags = defaultIncludeFlags;
DetourNavigator::AreaCosts areaCosts {};
float destinationTolerance = 1;
if (options.has_value())
{
if (const auto& t = options->get<sol::optional<sol::table>>("agentBounds"))
{
if (const auto& v = t->get<sol::optional<DetourNavigator::CollisionShapeType>>("shapeType"))
agentBounds.mShapeType = *v;
if (const auto& v = t->get<sol::optional<osg::Vec3f>>("halfExtents"))
{
agentBounds.mHalfExtents = *v;
stepSize = 2 * std::max(v->x(), v->y());
}
}
if (const auto& v = options->get<sol::optional<float>>("stepSize"))
stepSize = *v;
if (const auto& v = options->get<sol::optional<DetourNavigator::Flags>>("includeFlags"))
includeFlags = *v;
if (const auto& t = options->get<sol::optional<sol::table>>("areaCosts"))
{
if (const auto& v = t->get<sol::optional<float>>("water"))
areaCosts.mWater = *v;
if (const auto& v = t->get<sol::optional<float>>("door"))
areaCosts.mDoor = *v;
if (const auto& v = t->get<sol::optional<float>>("pathgrid"))
areaCosts.mPathgrid = *v;
if (const auto& v = t->get<sol::optional<float>>("ground"))
areaCosts.mGround = *v;
}
if (const auto& v = options->get<sol::optional<float>>("destinationTolerance"))
destinationTolerance = *v;
}
std::vector<osg::Vec3f> result;
const DetourNavigator::Status status = DetourNavigator::findPath(
*MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, stepSize, source,
destination, includeFlags, areaCosts, destinationTolerance, std::back_inserter(result));
return std::make_tuple(status, std::move(result));
};
api["findRandomPointAroundCircle"] = [] (const osg::Vec3f& position, float maxRadius,
const sol::optional<sol::table>& options)
{
DetourNavigator::AgentBounds agentBounds = defaultAgentBounds;
DetourNavigator::Flags includeFlags = defaultIncludeFlags;
if (options.has_value())
{
if (const auto& t = options->get<sol::optional<sol::table>>("agentBounds"))
{
if (const auto& v = t->get<sol::optional<DetourNavigator::CollisionShapeType>>("shapeType"))
agentBounds.mShapeType = *v;
if (const auto& v = t->get<sol::optional<osg::Vec3f>>("halfExtents"))
agentBounds.mHalfExtents = *v;
}
if (const auto& v = options->get<sol::optional<DetourNavigator::Flags>>("includeFlags"))
includeFlags = *v;
}
constexpr auto getRandom = []
{
return Misc::Rng::rollProbability(MWBase::Environment::get().getWorld()->getPrng());
};
return DetourNavigator::findRandomPointAroundCircle(*MWBase::Environment::get().getWorld()->getNavigator(),
agentBounds, position, maxRadius, includeFlags, getRandom);
};
api["castNavigationRay"] = [] (const osg::Vec3f& from, const osg::Vec3f& to,
const sol::optional<sol::table>& options)
{
DetourNavigator::AgentBounds agentBounds = defaultAgentBounds;
DetourNavigator::Flags includeFlags = defaultIncludeFlags;
if (options.has_value())
{
if (const auto& t = options->get<sol::optional<sol::table>>("agentBounds"))
{
if (const auto& v = t->get<sol::optional<DetourNavigator::CollisionShapeType>>("shapeType"))
agentBounds.mShapeType = *v;
if (const auto& v = t->get<sol::optional<osg::Vec3f>>("halfExtents"))
agentBounds.mHalfExtents = *v;
}
if (const auto& v = options->get<sol::optional<DetourNavigator::Flags>>("includeFlags"))
includeFlags = *v;
}
return DetourNavigator::raycast(*MWBase::Environment::get().getWorld()->getNavigator(),
agentBounds, from, to, includeFlags);
};
return LuaUtil::makeReadOnly(api);
}
}

@ -1,6 +1,7 @@
#include "types.hpp"
#include <components/lua/luastate.hpp>
#include <components/detournavigator/agentbounds.hpp>
#include <apps/openmw/mwmechanics/drawstate.hpp>
#include <apps/openmw/mwmechanics/creaturestats.hpp>
@ -243,6 +244,14 @@ namespace MWLua
}
context.mLuaManager->addAction(std::make_unique<SetEquipmentAction>(context.mLua, obj.id(), std::move(eqp)));
};
actor["getPathfindingAgentBounds"] = [context](const LObject& o)
{
const DetourNavigator::AgentBounds agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(o.ptr());
sol::table result = context.mLua->newTable();
result["shapeType"] = agentBounds.mShapeType;
result["halfExtents"] = agentBounds.mHalfExtents;
return result;
};
addActorStatsBindings(actor, context);
}

@ -3931,7 +3931,7 @@ namespace MWWorld
DetourNavigator::AgentBounds World::getPathfindingAgentBounds(const MWWorld::ConstPtr& actor) const
{
const MWPhysics::Actor* physicsActor = mPhysics->getActor(actor);
if (physicsActor == nullptr || (actor.isInCell() && actor.getCell()->isExterior()))
if (physicsActor == nullptr || !actor.isInCell() || actor.getCell()->isExterior())
return DetourNavigator::AgentBounds {DetourNavigator::defaultCollisionShapeType, mDefaultHalfExtents};
else
return DetourNavigator::AgentBounds {physicsActor->getCollisionShapeType(), physicsActor->getHalfExtents()};

@ -256,7 +256,7 @@ namespace DetourNavigator
template <class OutputIterator>
Status findSmoothPath(const dtNavMesh& navMesh, const osg::Vec3f& halfExtents, const float stepSize,
const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts,
const Settings& settings, float endTolerance, OutputIterator& out)
const Settings& settings, float endTolerance, OutputIterator out)
{
dtNavMeshQuery navMeshQuery;
if (!initNavMeshQuery(navMeshQuery, navMesh, settings.mDetour.mMaxNavMeshQueryNodes))

@ -24,7 +24,7 @@ namespace DetourNavigator
template <class OutputIterator>
inline Status findPath(const Navigator& navigator, const AgentBounds& agentBounds, const float stepSize,
const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts,
float endTolerance, OutputIterator& out)
float endTolerance, OutputIterator out)
{
static_assert(
std::is_same<
@ -45,7 +45,7 @@ namespace DetourNavigator
/**
* @brief findRandomPointAroundCircle returns random location on navmesh within the reach of specified location.
* @param agentBounds allows to find navmesh for given actor.
* @param start path from given point.
* @param start is a position where the search starts.
* @param maxRadius limit maximum distance from start.
* @param includeFlags setup allowed surfaces for actor to walk.
* @return not empty optional with position if point is found and empty optional if point is not found.

@ -88,5 +88,127 @@
-- @param openmw.util#Vector3 from Start point of the ray.
-- @param openmw.util#Vector3 to End point of the ray.
return nil
---
-- @type NAVIGATOR_FLAGS
-- @field [parent=#NAVIGATOR_FLAGS] #number Walk allow agent to walk on the ground area;
-- @field [parent=#NAVIGATOR_FLAGS] #number Swim allow agent to swim on the water surface;
-- @field [parent=#NAVIGATOR_FLAGS] #number OpenDoor allow agent to open doors on the way;
-- @field [parent=#NAVIGATOR_FLAGS] #number UsePathgrid allow agent to use predefined pathgrid imported from ESM files.
---
-- @type COLLISION_SHAPE_TYPE
-- @field [parent=#CCOLLISION_SHAPE_TYPE] #number Aabb Axis-Aligned Bounding Box is used for NPC and symmetric
-- Creatures;
-- @field [parent=#COLLISION_SHAPE_TYPE] #number RotatingBox is used for Creatures with big difference in width and
-- height.
---
-- @type FIND_PATH_STATUS
-- @field [parent=#FIND_PATH_STATUS] #number Success Path is found;
-- @field [parent=#FIND_PATH_STATUS] #number PartialPath Last path point is not a destination but a nearest position
-- among found;
-- @field [parent=#FIND_PATH_STATUS] #number NavMeshNotFound Provided `agentBounds` don't have corresponding navigation
-- mesh. For interior cells it means an agent with such `agentBounds` is present on the scene. For exterior cells only
-- default `agentBounds` is supported;
-- @field [parent=#FIND_PATH_STATUS] #number StartPolygonNotFound `source` position is too far from available
-- navigation mesh. The status may appear when navigation mesh is not fully generated or position is outside of covered
-- area;
-- @field [parent=#FIND_PATH_STATUS] #number EndPolygonNotFound `destination` position is too far from available
-- navigation mesh. The status may appear when navigation mesh is not fully generated or position is outside of covered
-- area;
-- @field [parent=#FIND_PATH_STATUS] #number MoveAlongSurfaceFailed Found path couldn't be smoothed due to imperfect
-- algorithm implementation or bad navigation mesh data;
-- @field [parent=#FIND_PATH_STATUS] #number FindPathOverPolygonsFailed Path over navigation mesh from `source` to
-- `destination` does not exist or navigation mesh is not fully generated to provide the path;
-- @field [parent=#FIND_PATH_STATUS] #number GetPolyHeightFailed Found path couldn't be smoothed due to imperfect
-- algorithm implementation or bad navigation mesh data;
-- @field [parent=#FIND_PATH_STATUS] #number InitNavMeshQueryFailed Couldn't initialize required data due to bad input
-- or bad navigation mesh data.
---
-- Find path over navigation mesh from source to destination with given options. Result is unstable since navigation
-- mesh generation is asynchronous.
-- @function [parent=#nearby] findPath
-- @param openmw.util#Vector3 source Initial path position.
-- @param openmw.util#Vector3 destination Final path position.
-- @param #table options An optional table with additional optional arguments. Can contain:
--
-- * `agentBounds` - a table identifying which navmesh to use, can contain:
--
-- * `shapeType` - one of @{#COLLISION_SHAPE_TYPE} values;
-- * `halfExtents` - @{openmw.util#Vector3} defining agent bounds size;
-- * `stepSize` - a floating point number to define frequency of path points
-- (default: `2 * math.max(halfExtents:x, halfExtents:y)`)
-- * `includeFlags` - allowed areas for agent to move, a sum of @{#NAVIGATOR_FLAGS} values
-- (default: @{#NAVIGATOR_FLAGS.Walk} + @{#NAVIGATOR_FLAGS.Swim} +
-- @{#NAVIGATOR_FLAGS.OpenDoor} + @{#NAVIGATOR_FLAGS.UsePathgrid});
-- * `areaCosts` - a table defining relative cost for each type of area, can contain:
--
-- * `ground` - a floating point number >= 0, used in combination with @{#NAVIGATOR_FLAGS.Walk} (default: 1);
-- * `water` - a floating point number >= 0, used in combination with @{#NAVIGATOR_FLAGS.Swim} (default: 1);
-- * `door` - a floating point number >= 0, used in combination with @{#NAVIGATOR_FLAGS.OpenDoor} (default: 2);
-- * `pathgrid` - a floating point number >= 0, used in combination with @{#NAVIGATOR_FLAGS.UsePathgrid}
-- (default: 1);
-- * `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);
-- @return @{#FIND_PATH_STATUS}, a collection of @{openmw.util#Vector3}
-- @usage local status, path = nearby.findPath(source, destination)
-- @usage local status, path = nearby.findPath(source, destination, {
-- includeFlags = nearby.NAVIGATOR_FLAGS.Walk + nearby.NAVIGATOR_FLAGS.OpenDoor,
-- areaCosts = {
-- door = 1.5,
-- },
-- })
-- @usage local status, path = nearby.findPath(source, destination, {
-- agentBounds = Actor.getPathfindingAgentBounds(self),
-- })
---
-- Returns random location on navigation mesh within the reach of specified location.
-- The location is not exactly constrained by the circle, but it limits the area.
-- @function [parent=#nearby] findRandomPointAroundCircle
-- @param openmw.util#Vector3 position Center of the search circle.
-- @param #number maxRadius Approximate maximum search distance.
-- @param #table options An optional table with additional optional arguments. Can contain:
--
-- * `agentBounds` - a table identifying which navmesh to use, can contain:
--
-- * `shapeType` - one of @{#COLLISION_SHAPE_TYPE} values;
-- * `halfExtents` - @{openmw.util#Vector3} defining agent bounds size;
-- * `includeFlags` - allowed areas for agent to move, a sum of @{#NAVIGATOR_FLAGS} values
-- (default: @{#NAVIGATOR_FLAGS.Walk} + @{#NAVIGATOR_FLAGS.Swim} +
-- @{#NAVIGATOR_FLAGS.OpenDoor} + @{#NAVIGATOR_FLAGS.UsePathgrid});
-- @return @{openmw.util#Vector3} or nil
-- @usage local position = nearby.findRandomPointAroundCircle(position, maxRadius)
-- @usage local position = nearby.findRandomPointAroundCircle(position, maxRadius, {
-- includeFlags = nearby.NAVIGATOR_FLAGS.Walk,
-- })
-- @usage local position = nearby.findRandomPointAroundCircle(position, maxRadius, {
-- agentBounds = Actor.getPathfindingAgentBounds(self),
-- })
---
-- Finds a nearest to the ray target position starting from the initial position with resulting curve drawn on the
-- navigation mesh surface.
-- @function [parent=#nearby] castNavigationRay
-- @param openmw.util#Vector3 from Initial ray position.
-- @param openmw.util#Vector3 to Target ray position.
-- @param #table options An optional table with additional optional arguments. Can contain:
--
-- * `agentBounds` - a table identifying which navmesh to use, can contain:
--
-- * `shapeType` - one of @{#COLLISION_SHAPE_TYPE} values;
-- * `halfExtents` - @{openmw.util#Vector3} defining agent bounds size;
-- * `includeFlags` - allowed areas for agent to move, a sum of @{#NAVIGATOR_FLAGS} values
-- (default: @{#NAVIGATOR_FLAGS.Walk} + @{#NAVIGATOR_FLAGS.Swim} +
-- @{#NAVIGATOR_FLAGS.OpenDoor} + @{#NAVIGATOR_FLAGS.UsePathgrid});
-- @return @{openmw.util#Vector3} or nil
-- @usage local position = nearby.castNavigationRay(from, to)
-- @usage local position = nearby.castNavigationRay(from, to, {
-- includeFlags = nearby.NAVIGATOR_FLAGS.Swim,
-- })
-- @usage local position = nearby.castNavigationRay(from, to, {
-- agentBounds = Actor.getPathfindingAgentBounds(self),
-- })
return nil

@ -9,6 +9,12 @@
--- Common functions for Creature, NPC, and Player.
-- @type Actor
---
-- Agent bounds to be used for pathfinding functions.
-- @function [parent=#Actor] getPathfindingAgentBounds
-- @param openmw.core#GameObject actor
-- @return #table with `shapeType` and `halfExtents`
---
-- Whether the object is an actor.
-- @function [parent=#Actor] objectIsInstance

@ -4,6 +4,7 @@ local util = require('openmw.util')
local core = require('openmw.core')
local input = require('openmw.input')
local types = require('openmw.types')
local nearby = require('openmw.nearby')
input.setControlSwitch(input.CONTROL_SWITCH.Fighting, false)
input.setControlSwitch(input.CONTROL_SWITCH.Jumping, false)
@ -64,6 +65,51 @@ testing.registerLocalTest('playerDiagonalWalking',
testing.expectEqualWithDelta(direction.y, -0.707, 0.1, 'Walk diagonally, Y coord')
end)
testing.registerLocalTest('findPath',
function()
local src = util.vector3(4096, 4096, 867.237)
local dst = util.vector3(4500, 4500, 700.216)
local options = {
agentBounds = types.Actor.getPathfindingAgentBounds(self),
stepSize = 50,
includeFlags = nearby.NAVIGATOR_FLAGS.Walk + nearby.NAVIGATOR_FLAGS.Swim,
areaCosts = {
water = 1,
door = 2,
ground = 1,
pathgrid = 1,
},
destinationTolerance = 1,
}
local status, path = nearby.findPath(src, dst)
testing.expectEqual(status, nearby.FIND_PATH_STATUS.Success, 'Status')
testing.expectLessOrEqual((path[path:size()] - dst):length(), 1, 'Last path point')
end)
testing.registerLocalTest('findRandomPointAroundCircle',
function()
local position = util.vector3(4096, 4096, 867.237)
local maxRadius = 100
local options = {
agentBounds = types.Actor.getPathfindingAgentBounds(self),
includeFlags = nearby.NAVIGATOR_FLAGS.Walk,
}
local result = nearby.findRandomPointAroundCircle(position, maxRadius, options)
testing.expectGreaterThan((result - position):length(), 1, 'Random point')
end)
testing.registerLocalTest('castNavigationRay',
function()
local src = util.vector3(4096, 4096, 867.237)
local dst = util.vector3(4500, 4500, 700.216)
local options = {
agentBounds = types.Actor.getPathfindingAgentBounds(self),
includeFlags = nearby.NAVIGATOR_FLAGS.Walk + nearby.NAVIGATOR_FLAGS.Swim,
}
local result = nearby.castNavigationRay(src, dst, options)
testing.expectLessOrEqual((result - dst):length(), 1, 'Navigation hit point')
end)
return {
engineHandlers = {
onUpdate = testing.updateLocal,

@ -70,6 +70,18 @@ tests = {
initPlayer()
testing.runLocalTest(player, 'playerDiagonalWalking')
end},
{'findPath', function()
initPlayer()
testing.runLocalTest(player, 'findPath')
end},
{'findRandomPointAroundCircle', function()
initPlayer()
testing.runLocalTest(player, 'findRandomPointAroundCircle')
end},
{'castNavigationRay', function()
initPlayer()
testing.runLocalTest(player, 'castNavigationRay')
end},
{'teleport', testTeleport},
}
@ -80,4 +92,3 @@ return {
},
eventHandlers = testing.eventHandlers,
}

@ -58,6 +58,24 @@ function M.expectGreaterOrEqual(v1, v2, msg)
end
end
function M.expectGreaterThan(v1, v2, msg)
if not (v1 > v2) then
error(string.format('%s: %s > %s', msg or '', v1, v2), 2)
end
end
function M.expectLessOrEqual(v1, v2, msg)
if not (v1 <= v2) then
error(string.format('%s: %s <= %s', msg or '', v1, v2), 2)
end
end
function M.expectEqual(v1, v2, msg)
if not (v1 == v2) then
error(string.format('%s: %s ~= %s', msg or '', v1, v2), 2)
end
end
local localTests = {}
local localTestRunner = nil
@ -96,4 +114,3 @@ M.eventHandlers = {
}
return M

Loading…
Cancel
Save