Start writing new pathfinding algorithm

This commit is contained in:
teknomunk 2024-03-26 20:51:44 +00:00
parent e50ad7d6de
commit 9f5c948deb
3 changed files with 296 additions and 0 deletions

View File

@ -0,0 +1,275 @@
local storage = minetest.get_mod_storage()
local EXPAND_SIZE = 1
local FALL_DAMAGE_DISTANCE = 3
local MOB_HEIGHT_EXPAND_SIZE = 2
local function get_node_at(state, pos, idx)
if not idx then idx = state.area:index(pos.x, pos.y, pos.z) end
local cid = state.data[idx]
if not cid then return "unloaded" end
if state.content_id_cache[cid] then return state.content_id_cache[cid] end
local name = minetest.get_name_from_content_id(cid)
state.content_id_cache[cid] = name
return name
end
local air_height_topside_cache = {}
local function get_node_air_height_topside(state, pos, name)
if not name then name = get_node_at(state, pos) end
if name == "air" then return 1 end
local nodedef = minetest.registered_nodes[name]
if not nodedef then
print("Missing node definition for "..tostring(name)..", treating as solid")
return 0
end
if nodedef.drawtype == "nodebox" then
local max = -0.5
if nodedef.node_box.type == "fixed" then
for _,part in ipairs(nodedef.node_box.fixed) do
if part[5] > max then
max = part[5]
end
end
local air_height_topside = 0.5 - max
air_height_topside_cache[name] = air_height_topside
return air_height_topside
end
else
-- TODO: finish implement partial block heights
end
return 0
end
local air_height_underside_cache = {}
local function get_node_air_height_underside(state, pos, name)
if not name then name = get_node_at(state, pos) end
if name == "air" then return 1 end
local nodedef = minetest.registered_nodes[name]
if not nodedef then
print("Missing node definition for "..tostring(name)..", treating as solid")
return 0
end
local node_air_height_underside = nodedef._mcl2_pathfinding_air_height_underside or air_height_underside_cache[name]
if node_air_height_underside then return node_air_height_underside end
if nodedef.drawtype == "nodebox" then
local min = 0.5
if nodedef.node_box.type == "fixed" then
for _,part in ipairs(nodedef.node_box.fixed) do
if part[5] < min then
min = part[5]
end
end
-- Cache for future use
local air_height_underside = 0.5 + min
air_height_underside_cache[name] = air_height_underside
return air_height_underside
end
end
-- TODO: implement partial block heights
return 0
end
local RULE_NO_NAV = { no_nav = true }
local NODE_NEIGHBORS = {
vector.new( 1,0, 0),
vector.new(-1,0, 0),
vector.new( 0,0, 1),
vector.new( 0,0,-1),
}
local NODE_NEIGHBOR_JUMPS = {
vector.new( 1,1, 0),
vector.new(-1,1, 0),
vector.new( 0,1, 1),
vector.new( 0,1,-1),
}
local NODE_NEIGHBOR_DROP_ONE = {
vector.new( 1,-1, 0),
vector.new(-1,-1, 0),
vector.new( 0,-1, 1),
vector.new( 0,-1,-1),
}
local NODE_NEIGHBOR_DROP_TWO = {
vector.new( 1,-1, 0),
vector.new(-1,-1, 0),
vector.new( 0,-1, 1),
vector.new( 0,-1,-1),
}
local function get_navigation_rules_for_node(state, node_pos)
local area = state.area
local idx = area:index(node_pos.x,node_pos.y,node_pos.z)
if not idx then return RULE_NO_NAV end
-- Check if we already have generated rules for this node
local navigation_rules = state.navigation_rules
if navigation_rules[idx] then return navigation_rules[idx] end
-- Bounds check
if node_pos.x < state.bound.min.x then return RULE_NO_NAV end
if node_pos.y < state.bound.min.y then return RULE_NO_NAV end
if node_pos.z < state.bound.min.z then return RULE_NO_NAV end
if node_pos.x > state.bound.max.x then return RULE_NO_NAV end
if node_pos.y > state.bound.max.y then return RULE_NO_NAV end
if node_pos.z > state.bound.max.z then return RULE_NO_NAV end
-- Lookup node information
local name = get_node_at(state, node_pos, idx)
local nodedef = minetest.registered_nodes[name]
-- Check if navigable: Need to have at least 2 height for horizontal travel thru this node
local height = 0
if name == "air" then
-- Make sure we have a solid block below us
if get_node_air_height_topside(state, vector.offset(node_pos,0,-1,0)) == 1 then
-- Would fall thru and navigate thru node below here
navigation_rules[idx] = RULE_NO_NAV
return RULE_NO_NAV
end
height = 1
end
-- Handle solid and semi-solid block
height = get_node_air_height_topside(state, node_pos, name)
if height == 1 then
-- Solid
navigation_rules[idx] = RULE_NO_NAV
return RULE_NO_NAV
end
local node_underside_height = 0
local above_block = node_pos
repeat
above_block = vector.offset(above_block,0,1,0)
node_underside_height = get_node_air_height_underside(state, above_block)
height = height + node_underside_height
until node_underside_height ~= 1 or height >= 3
-- Handle too small spaces
if height < 2 then
navigation_rules[idx] = RULE_NO_NAV
return RULE_NO_NAV
end
-- We can navigate thru this node
local rule = {
height = height,
walk = {},
drop = {},
jump = {},
}
navigation_rules[idx] = rule
-- Now check neighbors to see if we can move there
for i,dir in ipairs(NODE_NEIGHBORS) do
-- Check direct neighbor
local neighbor_pos = vector.add(node_pos, dir)
local neighbor_rule = get_navigation_rules_for_node(state, neighbor_pos)
if not neighbor_rule.no_nav then
rule.walk[#rule.walk + 1] = dir
else
-- Check jumps
neighbor_rule = get_navigation_rules_for_node(state, vector.offset(neighbor_pos,0,1,0))
if not neighbor_rule.no_nav then
rule.jump[#rule.jump + 1] = NODE_NEIGHBOR_JUMPS[i]
else
-- Check drop one block
neighbor_rule = get_navigation_rules_for_node(state, vector.offset(neighbor_pos,0,-1,0))
if not neighbor_rule.no_nav then
rule.drop[#rule.drop + 1] = NODE_NEIGHBOR_DROP_ONE[i]
else
-- Check drop two blocks
neighbor_rule = get_navigation_rules_for_node(state, vector.offset(neighbor_pos,0,-1,0))
if not neighbor_rule.no_nav then
rule.drop[#rule.drop + 1] = NODE_NEIGHBOR_DROP_TWO[i]
end
end
end
end
end
print("node_pos="..tostring(node_pos)..",name="..tostring(name)..",rule="..dump(rule))
return rule
end
local function gen_navigation_rules(state)
local node_base = state.node_base
local area = state.area
local navigation_rules = state.navigation_rules
local miny = state.bound.min.y
local maxy = state.bound.max.y
local minx = state.bound.min.x
local maxx = state.bound.max.x
for z = (state.bound.min.z),(state.bound.max.z) do
for y = miny,maxy do
for x = minx,maxx do
get_navigation_rules_for_node(state, vector.new(x,y,z))
end
end
end
end
-- Precomputes a lookup table for each node to get the next direction to take to get to any other node
-- in the map block. This will take some time, but shouldn't have to be done very often (only when a change
-- in the map data makes it impossible to get to the target node)
local function gen_navigation_lut(state)
-- TODO: implement
end
mcl2_pathfinding.recalculate_pathfinding_data = function(pos, async)
local state = {
content_id_cache = {},
navigation_rules = {},
node_base = vector.new(
math.floor(pos.x / 16) * 16,
math.floor(pos.y / 16) * 16,
math.floor(pos.z / 16) * 16
)
}
local node_base = state.node_base
local vm = minetest.get_voxel_manip(); state.vm = vm
state.bound = {
min = vector.offset(node_base, 0 - EXPAND_SIZE, 0 - FALL_DAMAGE_DISTANCE, 0 - EXPAND_SIZE),
max = vector.offset(node_base, 16 + EXPAND_SIZE, 16 + MOB_HEIGHT_EXPAND_SIZE, 16 + EXPAND_SIZE)
}
local emin,emax = vm:read_from_map( state.bound.min, state.bound.max )
state.min = emin
state.max = emax
local area = VoxelArea:new{ MinEdge = emin, MaxEdge = emax }; state.area = area
local data = vm:get_data(); state.data = data
local function process(state)
gen_navigation_rules(state)
gen_navigation_lut(state)
return state.node_base,state.navigation_lut
end
local function finish(node_pos,navigation_lut)
local data = minetest.serialize(navigation_lut)
local hash = minetest.hash_node_position(node_pos)
storage:set_string(tostring(hash),data)
end
-- use async environemt if request and supported
if async and minetest.handle_async then
minetest.handle_async(process,finish,state)
else
finish(process(state))
end
end

View File

@ -0,0 +1,18 @@
local modname = minetest.get_current_modname()
local modpath = minetest.get_modpath(modname)
local S = minetest.get_translator(modname)
mcl2_pathfinding = {}
dofile(modpath.."/api.lua")
minetest.register_chatcommand("test_pathfinding",{
description = S("Test pathfinding"),
params = "",
privs = {},
func = function(name)
local player = minetest.get_player_by_name(name)
mcl2_pathfinding.recalculate_pathfinding_data(player:get_pos(),false)
end
})

View File

@ -0,0 +1,3 @@
name = mcl2_pathfinding
author = teknomunk
description = Adds a pathfinding API