554fde4643
Pathfinding: Adds functions that allows to map the Minetest 3D map to a 2D array to use by the pathfinding algorithm. Actions: Use new code for find_path function. Improves door opening while walking on paths, and also now close them. Cottages fence gates and doors are also now supported in addition to the default doors and gates. Changes to the Readme and the License.
247 lines
8.9 KiB
Lua
247 lines
8.9 KiB
Lua
-- Pathfinding code by Zorman2000
|
|
---------------------------------------------------------------------------------------
|
|
-- Pathfinding functionality
|
|
---------------------------------------------------------------------------------------
|
|
-- This class contains functions that allows to map the 3D map of Minetest into
|
|
-- a 2D array (basically by ignoring the y coordinate for the moment being) in order
|
|
-- to use the A* pathfinding algorithm to find the shortest path from one node to
|
|
-- another. The A* algorithm implementation is in the external Jumper LUA library,
|
|
-- by Roland Yonaba (https://github.com/Yonaba/Jumper).
|
|
-- Mapping algorithm: transforms a Minetest map surface to a 2d grid.
|
|
|
|
local path = minetest.get_modpath("advanced_npc")
|
|
|
|
-- Below code for require is taken and slightly modified
|
|
-- from irc mod by Diego Martinez (kaeza)
|
|
-- https://github.com/minetest-mods/irc
|
|
-- Handle mod security if needed
|
|
local ie, req_ie = _G, minetest.request_insecure_environment
|
|
if req_ie then ie = req_ie() end
|
|
if not ie then
|
|
error("The Advances NPC mod requires access to insecure functions in "..
|
|
"order to work. Please add the Advanced NPC mod to the "..
|
|
"secure.trusted_mods setting or disable the mod.")
|
|
end
|
|
|
|
-- Modify package path so that it can find the Jumper library files
|
|
ie.package.path =
|
|
path .. "/Jumper/?.lua;"..
|
|
ie.package.path
|
|
|
|
-- Require the main files from Jumper
|
|
local Grid = ie.require("jumper.grid")
|
|
local Pathfinder = ie.require("jumper.pathfinder")
|
|
|
|
pathfinder = {}
|
|
|
|
pathfinder.node_types = {
|
|
start = 0,
|
|
goal = 1,
|
|
walkable = 2,
|
|
openable = 3,
|
|
non_walkable = 4
|
|
}
|
|
|
|
pathfinder.nodes = {
|
|
openable_prefix = {
|
|
"doors:",
|
|
"cottages:gate",
|
|
"cottages:half_door"
|
|
}
|
|
}
|
|
|
|
|
|
-- This function uses the mapping functions and the A* algorithm implementation
|
|
-- of the Jumper library to find a path from start_pos to end_pos. The range is
|
|
-- an extra amount of nodes to search in both the x and z coordinates.
|
|
function pathfinder.find_path(start_pos, end_pos, range)
|
|
-- Map the Minetest area to a 2D array
|
|
local map = pathfinder.create_map(start_pos, end_pos, range, {})
|
|
-- Find start and end positions
|
|
local pos = pathfinder.find_start_and_end_pos(map)
|
|
-- Normalize the map
|
|
local normalized_map = pathfinder.normalize_map(map)
|
|
-- Create pathfinder object
|
|
local grid_object = Grid(normalized_map)
|
|
-- Define what is a walkable node
|
|
local walkable = 0
|
|
|
|
-- Pathfinder object using A* algorithm
|
|
local finder = Pathfinder(grid_object, 'ASTAR', walkable)
|
|
-- Set orthogonal mode meaning it will not move in diagonal directions
|
|
finder:setMode("ORTHOGONAL")
|
|
|
|
-- Calculates the path, and its length
|
|
local path = finder:getPath(pos.start_pos.x, pos.start_pos.z, pos.end_pos.x, pos.end_pos.z)
|
|
|
|
--minetest.log("Found path: "..dump(path))
|
|
-- Pretty-printing the results
|
|
if path then
|
|
return pathfinder.get_path(map, path:nodes())
|
|
end
|
|
end
|
|
|
|
-- This function is used to determine if a node is walkable
|
|
-- or openable, in which case is good to use when finding a path
|
|
local function is_good_node(node, exceptions)
|
|
-- Is openable is to support doors, fence gates and other
|
|
-- doors from other mods. Currently, default doors, gates
|
|
-- and cottages doors are supported.
|
|
local is_openable = false
|
|
for _,node_prefix in pairs(pathfinder.nodes.openable_prefix) do
|
|
local start_i,end_i = string.find(node.name, node_prefix)
|
|
if start_i ~= nil then
|
|
is_openable = true
|
|
break
|
|
end
|
|
end
|
|
if not minetest.registered_nodes[node.name].walkable then
|
|
return pathfinder.node_types.walkable
|
|
elseif is_openable then
|
|
return pathfinder.node_types.openable
|
|
else
|
|
for i = 1, #exceptions do
|
|
if node.name == exceptions[i] then
|
|
return pathfinder.node_types.walkable
|
|
end
|
|
end
|
|
return pathfinder.node_types.non_walkable
|
|
end
|
|
end
|
|
|
|
function pathfinder.create_map(start_pos, end_pos, extra_range, walkables)
|
|
|
|
-- Unused, will not use voxel areas for now
|
|
--local c_air = minetest.get_content_id("air")
|
|
minetest.log("Start pos: "..dump(start_pos))
|
|
minetest.log("End pos: "..dump(end_pos))
|
|
|
|
-- Calculate all signs to ensure:
|
|
-- 1. Correct area calculation
|
|
-- 2. Iterate in the correct direction
|
|
local start_x_sign = (start_pos.x - end_pos.x) / math.abs(start_pos.x - end_pos.x)
|
|
local start_z_sign = (start_pos.z - end_pos.z) / math.abs(start_pos.z - end_pos.z)
|
|
local end_x_sign = (end_pos.x - start_pos.x) / math.abs(end_pos.x - start_pos.x)
|
|
local end_z_sign = (end_pos.z - start_pos.z) / math.abs(end_pos.z - start_pos.z)
|
|
--minetest.log("Start x sign: "..dump(start_x_sign)..", end x sign: "..dump(end_x_sign))
|
|
--minetest.log("End z sign: "..dump(start_z_sign)..", end z sign: "..dump(end_z_sign))
|
|
|
|
-- Get starting and ending positions, adding the extra nodes to the area
|
|
local pos1 = {x=start_pos.x + (extra_range * start_x_sign), y = start_pos.y - 1, z=start_pos.z + (extra_range * start_z_sign)}
|
|
local pos2 = {x=end_pos.x + (extra_range * end_x_sign), y = end_pos.y, z=end_pos.z + (extra_range * end_z_sign)}
|
|
--minetest.log("Pos 1: "..dump(pos1))
|
|
--minetest.log("Pos 2: "..dump(pos2))
|
|
|
|
-- Get Voxel Area - Not used for the moment
|
|
-- local vm = minetest.get_voxel_manip()
|
|
-- local emin, emax = vm:read_from_map(pos1, pos2)
|
|
-- local area = VoxelArea:new({MinEdge=emin, MaxEdge=emax})
|
|
-- local data = vm:get_data()
|
|
|
|
local grid = {}
|
|
|
|
-- Loop through the area and classify nodes
|
|
for z = 1, math.abs(pos1.z - pos2.z) do
|
|
local current_row = {}
|
|
for x = 1, math.abs(pos1.x - pos2.x) do
|
|
-- Calculate current position
|
|
local current_pos = {x=pos1.x + (x*end_x_sign), y=pos1.y, z=pos1.z + (z*end_z_sign)}
|
|
-- Check if this is the starting position
|
|
if current_pos.x == start_pos.x and current_pos.z == start_pos.z then
|
|
-- Is start position
|
|
table.insert(current_row, {pos=current_pos, type=pathfinder.node_types.start})
|
|
elseif current_pos.x == end_pos.x and current_pos.z == end_pos.z then
|
|
-- Is ending position or goal position
|
|
table.insert(current_row, {pos=current_pos, type=pathfinder.node_types.goal})
|
|
else
|
|
-- Check if node is walkable
|
|
local node = minetest.get_node(current_pos)
|
|
if node.name == "default:air" then
|
|
-- If air do no more checks
|
|
table.insert(current_row, {pos=current_pos, type=pathfinder.node_types.walkable})
|
|
else
|
|
-- Check if it is of a walkable or openable type
|
|
table.insert(current_row, {pos=current_pos, type=is_good_node(node, walkables)})
|
|
end
|
|
end
|
|
end
|
|
-- Insert the converted row into the grid
|
|
table.insert(grid, current_row)
|
|
end
|
|
|
|
return grid
|
|
end
|
|
|
|
-- Utility function to print the created map to the console.
|
|
-- Used for debug.
|
|
local function print_map(map)
|
|
for z,row in pairs(map) do
|
|
local row_string = "["
|
|
for x,node in pairs(row) do
|
|
if node.type == 2 then
|
|
row_string = row_string.."- "
|
|
else
|
|
row_string = row_string..node.type.." "
|
|
end
|
|
-- Use the following if the coordinates are also needed
|
|
--row_string = row_string..node.type..": {"..node.pos.x..", "..node.pos.y..", "..node.pos.z.."}, "
|
|
end
|
|
row_string = row_string.."]"
|
|
print(row_string)
|
|
end
|
|
end
|
|
|
|
|
|
-- This function find the starting and ending points in the
|
|
-- map representation, and returns the coordinates in the map
|
|
-- for the pathfinding algorithm to use
|
|
function pathfinder.find_start_and_end_pos(map)
|
|
-- This is for debug
|
|
--print_map(map)
|
|
local result = {}
|
|
for z,row in pairs(map) do
|
|
for x,node in pairs(row) do
|
|
if node.type == pathfinder.node_types.start then
|
|
--minetest.log("Start node: "..dump(node))
|
|
result["start_pos"] = {x=x, z=z}
|
|
elseif node.type == pathfinder.node_types.goal then
|
|
--minetest.log("End node: "..dump(node))
|
|
result["end_pos"] = {x=x, z=z}
|
|
end
|
|
end
|
|
end
|
|
minetest.log("Found start and end positions: "..dump(result))
|
|
return result
|
|
end
|
|
|
|
-- This function transforms the grid into binary values
|
|
-- (0 walkable, 1 non-walkable) for the pathfinding algorithm.
|
|
function pathfinder.normalize_map(map)
|
|
local result = {}
|
|
for _,row in pairs(map) do
|
|
local result_row = {}
|
|
for _,node in pairs(row) do
|
|
if node.type ~= pathfinder.node_types.non_walkable then
|
|
table.insert(result_row, 0)
|
|
else
|
|
table.insert(result_row, 1)
|
|
end
|
|
end
|
|
table.insert(result, result_row)
|
|
end
|
|
return result
|
|
end
|
|
|
|
-- This function returns an array of tables with to parameters: type and pos.
|
|
-- The position parameter is the actual coordinate on the Minetest map. The
|
|
-- type is the type of the node at the coordinate defined as pathfinder.node_types.
|
|
function pathfinder.get_path(map, path_nodes)
|
|
local result = {}
|
|
for node, count in path_nodes do
|
|
table.insert(result, map[node:getY()][node:getX()])
|
|
-- For debug
|
|
--minetest.log("Node: "..dump(map[node:getY()][node:getX()]))
|
|
--print(('Step: %d - x: %d - y: %d'):format(count, node:getX(), node:getY()))
|
|
end
|
|
return result
|
|
end |