Add new advanced pathfinder mod
This commit is contained in:
parent
7ae5377498
commit
45d6db7c7b
73
mods/rp_pathfinder/API.md
Normal file
73
mods/rp_pathfinder/API.md
Normal file
@ -0,0 +1,73 @@
|
||||
# `rp_pathfinder` API
|
||||
|
||||
This file explains how to use the `rp_pathfinder` pathfinder. You use it
|
||||
by calling `rp_pathfinder.find_path`.
|
||||
|
||||
## `rp_pathfinder.find_path(pos1, pos2, searchdistance, options, timeout)`
|
||||
|
||||
Finds the shortest path to walk on between two positions, using the A* algorithm.
|
||||
Walks on 'walkable' nodes while avoiding 'blocked' nodes and simulates
|
||||
jumps and drops (like a player would do). There are many options to customize
|
||||
the search.
|
||||
|
||||
Nodes that are 'walkable' and have node damage are considered 'blocked'.
|
||||
|
||||
By default, the search walks through nodes along the 4 cardinal directions,
|
||||
does not check for height clearance, ignores climbable nodes, `disable_jump`
|
||||
restrictions and does not cut corners.
|
||||
|
||||
### Parameters
|
||||
|
||||
* `pos1`: start position
|
||||
* `pos2`: target position
|
||||
* `searchdistance`: maximum distance from the search positions to search in.
|
||||
In detail: Path must be completely inside a cuboid. The minimum
|
||||
`searchdistance` of 1 will confine search between `pos1` and `pos2`.
|
||||
Larger values will increase the size of this cuboid in all directions.
|
||||
* `options`: Table to specify pathfinding options (each of these is optional):
|
||||
* `max_jump`: Maximum allowed nodes to jump (default: 0)
|
||||
* `max_drop`: Maximum allowed nodes to fall (default: 0)
|
||||
* `climb`: If true, can climb climbable nodes up and down (default: false)
|
||||
* `respect_climb_restriction`: If true, will respect `disable_jump` and `disable_descend`
|
||||
at climbable nodes (default: true)
|
||||
* `respect_disable_jump`: If true, can't jump at nodes with `disable_jump` group (default: false)
|
||||
* `clear_height`: How many consecutive nodes stacked on top of each other need
|
||||
to be 'passable' at each path position. At 1 (default), can walk through any 1-node high
|
||||
hole in the wall, at 2, the holes need to be at least 2 nodes tall, and so on. Useful
|
||||
to find paths for tall mobs.
|
||||
* `handler_walkable`: A function that takes a node table and returns
|
||||
true if the node can be walked on top
|
||||
(default: all nodes with `walkable=true` are walkable)
|
||||
* `handler_blocking`: A function that takes a node table and returns
|
||||
true if the node shall block the path
|
||||
(default: same as `handler_walkable`)
|
||||
* `use_vmanip`: If true, nodes will be queried using a LuaVoxelManip;
|
||||
otherwise, `minetest.get_node` will be used.
|
||||
* `timeout`: Abort search if pathfinder ran for longer than this time (in seconds)
|
||||
|
||||
### Return value
|
||||
|
||||
On success, returns a list of positions of the path.
|
||||
|
||||
On failure, returns `nil, <reason>`, where `<reason>` is one of:
|
||||
|
||||
* `"no_path"`: No path exists within the searched area
|
||||
* `"pos1_blocked"`: The node at `pos1` is blocked
|
||||
* `"pos2_blocked"`: The node at `pos2` is blocked
|
||||
* `"path_complexity_reached"`: The path search became too complex
|
||||
* `"timeout"`: Search was aborted because the time ran out
|
||||
|
||||
### Asynchronous usage
|
||||
|
||||
By default, this function can not be called in an async environment because it keeps calling `minetest.get_node`,
|
||||
which is not permitted. If you set `use_vmanip` to `true`, this function is safe to be used in an async environment.
|
||||
|
||||
### Performance notes
|
||||
|
||||
This function is less performant than the built-in `A*` pathfinder from Minetest,
|
||||
but it has more features.
|
||||
|
||||
For long-distance destinations, calling this function asynchronously is a good idea so the lowered
|
||||
performance doesn't lock up the server.
|
||||
|
||||
|
9
mods/rp_pathfinder/LICENSE.txt
Normal file
9
mods/rp_pathfinder/LICENSE.txt
Normal file
@ -0,0 +1,9 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright © 2024 Wuzzy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
25
mods/rp_pathfinder/README.md
Normal file
25
mods/rp_pathfinder/README.md
Normal file
@ -0,0 +1,25 @@
|
||||
# Advanced Pathfinder for Repixture (`rp_pathfinder`)
|
||||
|
||||
This mod provides an advanced pathfinding algorithm that finds
|
||||
an optimal path between two positions in the world. It uses the
|
||||
`A*` search algorithm.
|
||||
|
||||
It has more features than the built-in pathfinder of Minetest,
|
||||
but it is also less performant. It was created to aid mobs (creatures,
|
||||
animals, monsters) to walk through their world.
|
||||
|
||||
Features:
|
||||
|
||||
* `A*` search algorithm
|
||||
* Simulate walking, jumping and falling
|
||||
* Simulate climbing (optional)
|
||||
* Respect `disable_jump` and `disable_descend` restrictions (optional)
|
||||
* Check height clearance (useful for tall mobs)
|
||||
* Specify which nodes can be walked on, and which ones should be avoided
|
||||
|
||||
See `API.md` to learn how to use it.
|
||||
|
||||
## Licensing
|
||||
|
||||
This mod is free software, released under the terms of the MIT License.
|
||||
See `LICENSE.txt`.
|
463
mods/rp_pathfinder/init.lua
Normal file
463
mods/rp_pathfinder/init.lua
Normal file
@ -0,0 +1,463 @@
|
||||
-- Maximum allowed elements in the open list before aborting
|
||||
local MAX_OPEN = 700
|
||||
|
||||
rp_pathfinder = {}
|
||||
|
||||
-- Returns true if node is walkable
|
||||
local function walkable_default(node)
|
||||
local def = minetest.registered_nodes[node.name]
|
||||
if not def or def.walkable then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns true is node is "blocking"
|
||||
local blocking_default = walkable_default
|
||||
|
||||
-- Returns true if node is climbable
|
||||
-- * node: Node table
|
||||
-- * dir: Check for vertical climb restriction:
|
||||
-- * 1: Check if can climb up (no 'disable_jump' group)
|
||||
-- * -1: Check if can climb down (no 'disable_descend' group)
|
||||
-- * nil: Ignore climb restrictions
|
||||
local function climbable(node, dir)
|
||||
local def = minetest.registered_nodes[node.name]
|
||||
if not def then
|
||||
return false
|
||||
elseif def.climbable then
|
||||
if dir then
|
||||
if dir == 1 then
|
||||
return minetest.get_item_group(node.name, "disable_jump") == 0
|
||||
elseif dir == -1 then
|
||||
return minetest.get_item_group(node.name, "disable_descend") == 0
|
||||
else
|
||||
error("[rp_pathfinder] climbable: invalid dir argument!")
|
||||
end
|
||||
else
|
||||
return true
|
||||
end
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns true player can jump from node
|
||||
local function jumpable(node)
|
||||
return minetest.get_item_group(node.name, "disable_jump") == 0
|
||||
end
|
||||
|
||||
-- 2D distance heuristic between pos1 and pos2
|
||||
local function get_distance_2d(pos1, pos2)
|
||||
local distX = math.abs(pos1.x - pos2.x)
|
||||
local distZ = math.abs(pos1.z - pos2.z)
|
||||
|
||||
-- Manhattan distance
|
||||
return distX + distZ
|
||||
end
|
||||
|
||||
-- 3D distance heuristic between pos1 and pos2
|
||||
local function get_distance_3d(pos1, pos2)
|
||||
local distX = math.abs(pos1.x - pos2.x)
|
||||
local distY = math.abs(pos1.y - pos2.y)
|
||||
local distZ = math.abs(pos1.z - pos2.z)
|
||||
|
||||
-- Manhattan distance
|
||||
return distX + distY + distZ
|
||||
end
|
||||
|
||||
-- Checks nodes above pos to be non-blocking.
|
||||
-- Returns true if all nodes are non-blocking,
|
||||
-- false otherwise
|
||||
--
|
||||
-- * pos: Start position (will not be checked)
|
||||
-- * nodes_above: number of nodes above pos to check
|
||||
-- * nh: Node handers table
|
||||
-- * get_node: Note getter function
|
||||
local function check_height_clearance(pos, nodes_above, nh, get_node)
|
||||
if nodes_above <= 0 then
|
||||
-- Trivial: No nodes need to be checked
|
||||
return true
|
||||
end
|
||||
local npos = table.copy(pos)
|
||||
local nnode
|
||||
local height = 0
|
||||
repeat
|
||||
height = height + 1
|
||||
npos.y = npos.y + 1
|
||||
nnode = get_node(npos)
|
||||
if nh.blocking(nnode) then
|
||||
return false
|
||||
end
|
||||
until height >= nodes_above
|
||||
return true
|
||||
end
|
||||
|
||||
local function vertical_walk(start_pos, vdir, max_height, stop_func, stop_value, get_node)
|
||||
local pos = table.copy(start_pos)
|
||||
local height = 0
|
||||
local ok = false
|
||||
|
||||
while height < max_height do
|
||||
pos.y = pos.y + vdir
|
||||
local node = get_node(pos)
|
||||
height = height + 1
|
||||
if stop_func(node) == stop_value then
|
||||
ok = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if ok then
|
||||
return pos, height
|
||||
end
|
||||
end
|
||||
|
||||
local function get_neighbor_floor_pos(neighbor_pos, current_pos, clear_height, jump_height, drop_height, climb, nh, get_node)
|
||||
local npos = table.copy(neighbor_pos)
|
||||
local nnode = get_node(npos)
|
||||
-- Climb
|
||||
if climb then
|
||||
-- If neighbor is climbable
|
||||
if climbable(nnode) and not nh.blocking(nnode) then
|
||||
return npos
|
||||
-- If node *below* neighbor is climbable
|
||||
elseif not nh.blocking(nnode) then
|
||||
local bpos = vector.offset(npos, 0, -1, 0)
|
||||
local bnode = get_node(bpos)
|
||||
if climbable(bnode) and not nh.blocking(bnode) then
|
||||
return bpos
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Drop down
|
||||
if not nh.walkable(nnode) then
|
||||
drop_height = drop_height + 1
|
||||
local blocking_or_walkable = function(node)
|
||||
return nh.blocking(node) or nh.walkable(node)
|
||||
end
|
||||
-- Get the first blocking or walkable node below neighbor
|
||||
local floor = vertical_walk(npos, -1, drop_height, blocking_or_walkable, true, get_node)
|
||||
if not floor then
|
||||
return nil
|
||||
end
|
||||
local fnode = get_node(floor)
|
||||
if nh.blocking(fnode) and not nh.walkable(fnode) then
|
||||
-- If node is blocking but not walkable, we must not take it;
|
||||
-- its a potential danger
|
||||
return nil
|
||||
else
|
||||
floor.y = floor.y + 1
|
||||
return floor
|
||||
end
|
||||
-- Jump
|
||||
else
|
||||
-- Get the first non-walkable node above the neighbor
|
||||
local target_pos, height = vertical_walk(npos, 1, jump_height, nh.walkable, false, get_node)
|
||||
|
||||
-- Also check the nodes above current pos for any blocking nodes,
|
||||
-- since this is where the player has to jump
|
||||
if target_pos then
|
||||
-- Also take height clearance into account
|
||||
height = height + (clear_height - 1)
|
||||
local jump_blocking_pos = vertical_walk(current_pos, 1, height, nh.blocking, true, get_node)
|
||||
if not jump_blocking_pos then
|
||||
return target_pos
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 4 neighbors: the 4 cardinal directions
|
||||
local neighbor_dirs_2d = {
|
||||
{ x = -1, y = 0, z = 0 },
|
||||
{ x = 0, y = 0, z = -1 },
|
||||
{ x = 0, y = 0, z = 1 },
|
||||
{ x = 1, y = 0, z = 0 },
|
||||
}
|
||||
|
||||
-- Reverse a list of values
|
||||
local reverse_list = function(list)
|
||||
local reverse_list = {}
|
||||
for i=#list, 1, -1 do
|
||||
local elem = list[i]
|
||||
table.insert(reverse_list, elem)
|
||||
end
|
||||
return reverse_list
|
||||
end
|
||||
|
||||
-- Constructs the final path if found
|
||||
local function build_finished_path(closed_set, start_hash, final_hash)
|
||||
-- Basically, we walk backwards from the final node until we reached the start node
|
||||
local path = {}
|
||||
-- Start from the end ...
|
||||
local index = final_hash
|
||||
while start_hash ~= index do
|
||||
if not closed_set[index] then
|
||||
-- If this happens, this must be an error in the algorithm
|
||||
-- as the pathfinder has botched the closed_set somehow.
|
||||
error("rp_pathfinder: closed_set["..index.."] is nil in build_finished_path!")
|
||||
end
|
||||
table.insert(path, closed_set[index].pos)
|
||||
-- ... go to the previous node ...
|
||||
index = closed_set[index].parent
|
||||
end
|
||||
table.insert(path, closed_set[index].pos)
|
||||
local reverse_path = reverse_list(path)
|
||||
return reverse_path
|
||||
end
|
||||
|
||||
-- The main pathfinding function (see API.md)
|
||||
function rp_pathfinder.find_path(pos1, pos2, searchdistance, options, timeout)
|
||||
-- Keep track of time
|
||||
local start_time = minetest.get_us_time()
|
||||
|
||||
-- round positions if not done by former functions
|
||||
pos1 = vector.round(pos1)
|
||||
pos2 = vector.round(pos2)
|
||||
|
||||
-- Trivial: pos1 and pos2 are the same
|
||||
if vector.equals(pos1, pos2) then
|
||||
return { pos1 }
|
||||
end
|
||||
|
||||
local min_pos = {
|
||||
x = math.min(pos1.x, pos2.x) - searchdistance,
|
||||
y = math.min(pos1.y, pos2.y) - searchdistance,
|
||||
z = math.min(pos1.z, pos2.z) - searchdistance,
|
||||
}
|
||||
local max_pos = {
|
||||
x = math.max(pos1.x, pos2.x) + searchdistance,
|
||||
y = math.max(pos1.y, pos2.y) + searchdistance,
|
||||
z = math.max(pos1.z, pos2.z) + searchdistance,
|
||||
}
|
||||
|
||||
-- Options
|
||||
if not options then
|
||||
options = {}
|
||||
end
|
||||
local clear_height = math.max(1, options.clear_height or 1)
|
||||
local max_drop = options.max_drop or 0
|
||||
local max_jump = options.max_jump or 0
|
||||
local respect_disable_jump = options.respect_disable_jump or false
|
||||
local respect_climb_restrictions = options.respect_climb_restrictions
|
||||
if respect_climb_restrictions == nil then
|
||||
respect_climb_restrictions = true
|
||||
end
|
||||
local climb = options.climb or false
|
||||
local nh = {
|
||||
walkable = options.handler_walkable or walkable_default,
|
||||
blocking = options.handler_blocking or blocking_default,
|
||||
}
|
||||
local get_node
|
||||
if options.use_vmanip then
|
||||
local vmanip = minetest.get_voxel_manip(min_pos, max_pos)
|
||||
get_node = function(pos)
|
||||
return vmanip:get_node_at(pos)
|
||||
end
|
||||
else
|
||||
get_node = minetest.get_node
|
||||
end
|
||||
|
||||
-- Can't make a path if start or end node
|
||||
-- are blocking
|
||||
local target_node = get_node(pos2)
|
||||
if nh.blocking(target_node) then
|
||||
-- End position blocked
|
||||
return nil, "pos2_blocked"
|
||||
end
|
||||
|
||||
local start_node = get_node(pos1)
|
||||
if nh.blocking(start_node) then
|
||||
-- Start position blocked
|
||||
return nil, "pos1_blocked"
|
||||
end
|
||||
|
||||
local start_hash = minetest.hash_node_position(pos1)
|
||||
local final_hash = minetest.hash_node_position(pos2)
|
||||
|
||||
local open_set = {}
|
||||
local closed_set = {}
|
||||
|
||||
local open_set_size = 0
|
||||
|
||||
-- Helper functions to set and get search nodes
|
||||
local set_search_node = function(set, hash, values)
|
||||
if set == open_set then
|
||||
if not set[hash] and values ~= nil then
|
||||
open_set_size = open_set_size + 1
|
||||
elseif set[hash] and values == nil then
|
||||
open_set_size = open_set_size - 1
|
||||
end
|
||||
end
|
||||
set[hash] = values
|
||||
end
|
||||
local get_search_node = function(set, hash)
|
||||
return set[hash]
|
||||
end
|
||||
local get_next_search_node = function(set)
|
||||
return next(set)
|
||||
end
|
||||
|
||||
--[[ Syntax of a single search node for the A* search:
|
||||
{
|
||||
pos: World position of the Minetest node that this search node represents
|
||||
parent: Reference to preceding node in the search (nil for start node)
|
||||
h: Heuristic cost estimate from node to finish
|
||||
g: Total cost from start to this node
|
||||
f: Equals g+h
|
||||
}
|
||||
]]
|
||||
|
||||
-- Add the first search node to open set at the start
|
||||
|
||||
local h_first = get_distance_3d(pos1, pos2)
|
||||
set_search_node(open_set, start_hash, {
|
||||
pos = pos1,
|
||||
parent = nil,
|
||||
h = h_first,
|
||||
g = 0,
|
||||
f = h_first,
|
||||
})
|
||||
|
||||
-- Node has 4 neighbors: 4 cardinal directions
|
||||
local neighbor_dirs = neighbor_dirs_2d
|
||||
|
||||
while open_set_size > 0 do
|
||||
-- Find node with lowest f cost (f value)
|
||||
local current_hash, current_data = get_next_search_node(open_set)
|
||||
for hash, data in pairs(open_set) do
|
||||
if data.f < open_set[current_hash].f or data.f == current_data.f and data.h < current_data.h then
|
||||
current_hash = hash
|
||||
current_data = data
|
||||
end
|
||||
end
|
||||
|
||||
set_search_node(open_set, current_hash, nil)
|
||||
set_search_node(closed_set, current_hash, current_data)
|
||||
|
||||
-- Target position found: Return path
|
||||
if current_hash == final_hash then
|
||||
return build_finished_path(closed_set, start_hash, current_hash)
|
||||
end
|
||||
|
||||
local current_pos = current_data.pos
|
||||
local current_node = get_node(current_pos)
|
||||
local current_neighbor_dirs = neighbor_dirs
|
||||
local current_max_jump = max_jump
|
||||
local current_max_drop = max_drop
|
||||
if climb then
|
||||
current_neighbor_dirs = table.copy(neighbor_dirs)
|
||||
if respect_climb_restrictions then
|
||||
if climbable(current_node, 1) then
|
||||
table.insert(current_neighbor_dirs, {x=0,y=1,z=0})
|
||||
elseif climbable(current_node) then
|
||||
current_max_jump = 0
|
||||
end
|
||||
if climbable(current_node, -1) then
|
||||
table.insert(current_neighbor_dirs, {x=0,y=-1,z=0})
|
||||
elseif climbable(current_node) then
|
||||
current_max_drop = 0
|
||||
end
|
||||
else
|
||||
if climbable(current_node) then
|
||||
table.insert(current_neighbor_dirs, {x=0,y=1,z=0})
|
||||
table.insert(current_neighbor_dirs, {x=0,y=-1,z=0})
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Prevent jumping from disable_jump nodes (if enabled)
|
||||
if respect_disable_jump and max_jump > 0 then
|
||||
local current_jumpable = jumpable(current_node)
|
||||
local below_current_pos = table.copy(current_pos)
|
||||
below_current_pos.y = below_current_pos.y - 1
|
||||
local below_current_node = get_node(below_current_pos)
|
||||
local below_jumpable = jumpable(below_current_node)
|
||||
|
||||
if not current_jumpable or not below_jumpable then
|
||||
current_max_jump = 0
|
||||
end
|
||||
end
|
||||
|
||||
local neighbors = {}
|
||||
|
||||
for n=1, #current_neighbor_dirs do
|
||||
local ndir = current_neighbor_dirs[n]
|
||||
local x, y, z = ndir.x, ndir.y, ndir.z
|
||||
local neighbor_pos = {x = current_pos.x + x, y = current_pos.y + y, z = current_pos.z + z}
|
||||
|
||||
if vector.in_area(neighbor_pos, min_pos, max_pos) then
|
||||
|
||||
local neighbor = get_node(neighbor_pos)
|
||||
-- Check height clearance of raw (unmodified) neighbor
|
||||
if check_height_clearance(neighbor_pos, clear_height-1, nh, get_node) then
|
||||
-- Get floor position of neighbor. Implements jumping up or falling down.
|
||||
local neighbor_floor
|
||||
if y == 0 then
|
||||
neighbor_floor = get_neighbor_floor_pos(neighbor_pos, current_pos, clear_height,
|
||||
current_max_jump, current_max_drop, climb, nh, get_node)
|
||||
-- In case of Y change, we do a climb check
|
||||
elseif climb then
|
||||
-- No additional floor check needed
|
||||
if not nh.blocking(neighbor) then
|
||||
neighbor_floor = neighbor_pos
|
||||
end
|
||||
end
|
||||
if neighbor_floor then
|
||||
-- Check height clearance of modified neighbor
|
||||
if check_height_clearance(neighbor_floor, clear_height-1, nh, get_node) then
|
||||
local hash = minetest.hash_node_position(neighbor_floor)
|
||||
table.insert(neighbors, {
|
||||
hash = hash,
|
||||
pos = neighbor_floor,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, neighbor in pairs(neighbors) do
|
||||
local in_closed_list = get_search_node(closed_set, neighbor.hash) ~= nil
|
||||
if neighbor.hash ~= current_hash and not in_closed_list then
|
||||
local g = 0 -- cost from start
|
||||
local h -- estimated cost from search node to finish
|
||||
local f -- g+h
|
||||
local neighbor_cost = current_data.g + get_distance_3d(current_data.pos, neighbor.pos)
|
||||
local neighbor_data = get_search_node(open_set, neighbor.hash)
|
||||
local neighbor_exists
|
||||
if neighbor_data then
|
||||
g = neighbor_data.g
|
||||
neighbor_exists = true
|
||||
else
|
||||
neighbor_exists = false
|
||||
end
|
||||
if not neighbor_exists or neighbor_cost < g then
|
||||
h = get_distance_3d(neighbor.pos, pos2)
|
||||
g = neighbor_cost
|
||||
f = g + h
|
||||
set_search_node(open_set, neighbor.hash, {
|
||||
pos = neighbor.pos,
|
||||
parent = current_hash,
|
||||
f = f,
|
||||
g = g,
|
||||
h = h,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if open_set_size > MAX_OPEN then
|
||||
-- Path complexity limit reached
|
||||
return nil, "path_complexity_reached"
|
||||
end
|
||||
local end_time = minetest.get_us_time()
|
||||
if (end_time - start_time)/1000000 > timeout then
|
||||
-- Aborting due to timeout
|
||||
return nil, "timeout"
|
||||
end
|
||||
end
|
||||
|
||||
-- No path exists within searched area
|
||||
return nil, "no_path"
|
||||
end
|
3
mods/rp_pathfinder/mod.conf
Normal file
3
mods/rp_pathfinder/mod.conf
Normal file
@ -0,0 +1,3 @@
|
||||
name = rp_pathfinder
|
||||
title = Advanced Pathfinder for Repixture
|
||||
description = Provides advanced pathfinding that finds an optimal path between two positions in the world.
|
Loading…
x
Reference in New Issue
Block a user