forked from VoxeLibre/VoxeLibre
Compare commits
4 Commits
master
...
pathfindin
Author | SHA1 | Date |
---|---|---|
|
04f97db993 | |
|
5b50154fc2 | |
|
9fd09dce98 | |
|
9f5c948deb |
|
@ -0,0 +1,79 @@
|
||||||
|
local modname = minetest.get_current_modname()
|
||||||
|
local modpath = minetest.get_modpath(modname)
|
||||||
|
local storage = minetest.get_mod_storage()
|
||||||
|
local mod = mcl2_pathfinding
|
||||||
|
|
||||||
|
dofile(modpath.."/functions.lua")
|
||||||
|
minetest.register_async_dofile(modpath.."/functions.lua")
|
||||||
|
|
||||||
|
local EXPAND_SIZE = mod.EXPAND_SIZE
|
||||||
|
local FALL_DAMAGE_DISTANCE = mod.FALL_DAMAGE_DISTANCE
|
||||||
|
local MOB_HEIGHT_EXPAND_SIZE = mod.MOB_HEIGHT_EXPAND_SIZE
|
||||||
|
|
||||||
|
mod.load_pathfinding_data = function(pos, callback)
|
||||||
|
local state = {
|
||||||
|
content_id_cache = {},
|
||||||
|
navigation_rules = {},
|
||||||
|
inverted_navigation_rules = {},
|
||||||
|
navigation_lut = {
|
||||||
|
compression_mapping = {
|
||||||
|
count = 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
node_base = vector.new(
|
||||||
|
math.floor(pos.x / 16) * 16,
|
||||||
|
math.floor(pos.y / 16) * 16,
|
||||||
|
math.floor(pos.z / 16) * 16
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Get voxel data
|
||||||
|
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 data = vm:get_data(); state.data = data
|
||||||
|
|
||||||
|
-- Hash the map data to compare with the existing calculated data
|
||||||
|
local current_hash = mod.hash_voxel_data(data, emin, emax)
|
||||||
|
|
||||||
|
-- Check the current voxel data against existing data to see if we can reuse a previously computed LUT
|
||||||
|
local storage_key = tostring(node_base.x)..","..tostring(node_base.y)..","..tostring(node_base.z)
|
||||||
|
local old_data = storage:get_string(storage_key)
|
||||||
|
local need_refresh = true
|
||||||
|
if old_data then
|
||||||
|
local ds = minetest.deserialize(old_data)
|
||||||
|
if ds and ds.hash == current_hash then
|
||||||
|
callback(ds.lut)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Note: because this function can run inside an asynchronous worker thread, this function can't
|
||||||
|
-- access anything besides globals
|
||||||
|
local function process(state)
|
||||||
|
local mod = mcl2_pathfinding
|
||||||
|
return mod.process_recalculate_pathfinding_data(state)
|
||||||
|
end
|
||||||
|
local function finish(node_base, lut)
|
||||||
|
local storage_key = tostring(node_base.x)..","..tostring(node_base.y)..","..tostring(node_base.z)
|
||||||
|
local new_data = minetest.serialize({ hash = current_hash, lut = lut })
|
||||||
|
storage:set_string(storage_key,new_data)
|
||||||
|
callback(data)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- use async environemt if supported
|
||||||
|
if minetest.handle_async then
|
||||||
|
minetest.handle_async(process, finish, state)
|
||||||
|
|
||||||
|
-- and use synchronous processing if not supported
|
||||||
|
else
|
||||||
|
finish(process(state))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -0,0 +1,465 @@
|
||||||
|
mcl2_pathfinding = mcl2_pathfinding or {}
|
||||||
|
local mod = mcl2_pathfinding
|
||||||
|
local modname = "mcl2_pathfinding"
|
||||||
|
|
||||||
|
local EXPAND_SIZE = 1
|
||||||
|
mod.EXPAND_SIZE = EXPAND_SIZE
|
||||||
|
|
||||||
|
local FALL_DAMAGE_DISTANCE = 3
|
||||||
|
mod.FALL_DAMAGE_DISTANCE = FALL_DAMAGE_DISTANCE
|
||||||
|
|
||||||
|
local MOB_HEIGHT_EXPAND_SIZE = 2
|
||||||
|
mod.MOB_HEIGHT_EXPAND_SIZE = MOB_HEIGHT_EXPAND_SIZE
|
||||||
|
|
||||||
|
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["_"..modname.."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 ACTION_WALK = nil
|
||||||
|
local ACTION_JUMP = {}
|
||||||
|
local ACTION_OPEN = {}
|
||||||
|
local ACTION_CLIMB = {}
|
||||||
|
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,
|
||||||
|
size = 1, -- TODO: calculate a clearance value for mobs larger than 1 block wide
|
||||||
|
pos = node_pos,
|
||||||
|
neighbors = {},
|
||||||
|
actions = {},
|
||||||
|
}
|
||||||
|
navigation_rules[idx] = rule
|
||||||
|
|
||||||
|
-- Update compressed rules map. This is used to store lookup tables in a smaller amount of memory
|
||||||
|
-- for better performance
|
||||||
|
local compression_mapping = state.navigation_lut.compression_mapping
|
||||||
|
compression_mapping.count = compression_mapping.count + 1
|
||||||
|
compression_mapping[minetest.hash_node_position(node_pos)] = {
|
||||||
|
idx = compression_mapping.count,
|
||||||
|
pos = node_pos
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Now check neighbors to see if we can move there
|
||||||
|
local neighbors = rule.neighbors
|
||||||
|
local actions = rule.actions
|
||||||
|
local function check_neighbors(state, node_pos, dir, neighbors, actions, i)
|
||||||
|
-- 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
|
||||||
|
neighbors[#neighbors + 1] = dir
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check jumps
|
||||||
|
neighbor_rule = get_navigation_rules_for_node(state, vector.offset(neighbor_pos,0,1,0))
|
||||||
|
if not neighbor_rule.no_nav then
|
||||||
|
local idx = #neighbors + 1
|
||||||
|
neighbors[idx] = NODE_NEIGHBOR_JUMPS[i]
|
||||||
|
actions[idx] = ACTION_JUMP
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
neighbors[#neighbors + 1] = NODE_NEIGHBOR_DROP_ONE[i]
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
neighbors[#neighbors + 1] = NODE_NEIGHBOR_DROP_TWO[i]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for i,dir in ipairs(NODE_NEIGHBORS) do
|
||||||
|
check_neighbors(state, node_pos, dir, neighbors, actions, i)
|
||||||
|
end
|
||||||
|
|
||||||
|
--print("node_pos="..tostring(node_pos)..",name="..tostring(name)..",rule="..dump(rule))
|
||||||
|
|
||||||
|
return rule
|
||||||
|
end
|
||||||
|
|
||||||
|
local function gen_navigation_rules(state)
|
||||||
|
local miny = state.bound.min.y
|
||||||
|
local maxy = state.bound.max.y
|
||||||
|
local minx = state.bound.min.x
|
||||||
|
local maxx = state.bound.max.x
|
||||||
|
|
||||||
|
-- Generate raw navigation rules for every node in the mapblock
|
||||||
|
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
|
||||||
|
|
||||||
|
-- Invert navigation rules. Currently the data is all connections that you can go
|
||||||
|
-- to from a given node. We need all the connections that will get you to a given
|
||||||
|
-- node so that we can generate a gradient lookup table to get to any node from
|
||||||
|
-- any other node in this mapblock.
|
||||||
|
--
|
||||||
|
-- While we are doing this, we will also create the rule index for the lookup table
|
||||||
|
local area = state.area
|
||||||
|
local inverted_navigation_rules = state.inverted_navigation_rules
|
||||||
|
local rule_table = { index={} }
|
||||||
|
state.rule_table = rule_table
|
||||||
|
for vm_idx,rule in pairs(state.navigation_rules) do
|
||||||
|
if rule.neighbors then
|
||||||
|
for _,neighbor_dir in pairs(rule.neighbors) do
|
||||||
|
local neighbor_pos = vector.add(neighbor_dir, rule.pos)
|
||||||
|
local neighbor_idx = area:index(neighbor_pos.x, neighbor_pos.y, neighbor_pos.z)
|
||||||
|
if neighbor_idx then
|
||||||
|
local neighbor_inverted_rules = inverted_navigation_rules[neighbor_idx]
|
||||||
|
if not neighbor_inverted_rules then
|
||||||
|
neighbor_inverted_rules = { count=0 }
|
||||||
|
inverted_navigation_rules[neighbor_idx] = neighbor_inverted_rules
|
||||||
|
end
|
||||||
|
local new_count = neighbor_inverted_rules.count + 1
|
||||||
|
neighbor_inverted_rules.count = new_count
|
||||||
|
neighbor_inverted_rules[new_count] = vector.multiply(neighbor_dir, -1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local dir_hash = minetest.hash_node_position(neighbor_dir)
|
||||||
|
if not rule_table.index[dir_hash] then
|
||||||
|
local idx = #rule_table + 1
|
||||||
|
rule_table[idx] = neighbor_dir
|
||||||
|
rule_table.index[dir_hash] = idx
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rule_table.present = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local HEX = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F" }
|
||||||
|
local function gen_navigation_lut(state, target_node_pos)
|
||||||
|
-- Based on Dijkstra's Algorithm for shortest path calculation
|
||||||
|
-- We want the minimum distance from every navigable node to
|
||||||
|
-- `node_pos'
|
||||||
|
local comp_map = state.navigation_lut.compression_mapping
|
||||||
|
local hash_pos = minetest.hash_node_position
|
||||||
|
local area = state.area
|
||||||
|
local distances = {}
|
||||||
|
local inv_nav_rules = state.inverted_navigation_rules
|
||||||
|
|
||||||
|
-- Start with `node_pops' as the only open node with a distance of 0
|
||||||
|
local open = { target_node_pos }
|
||||||
|
distances[comp_map[hash_pos(target_node_pos)].idx] = 0
|
||||||
|
|
||||||
|
-- While we have nodes left to compute a distance for
|
||||||
|
while #open > 0 do
|
||||||
|
-- Select next node to process. This will be the node
|
||||||
|
-- with the shortest dinstance of all open nodes
|
||||||
|
local next_node_idx = 1
|
||||||
|
local min_distance = nil
|
||||||
|
for i,node in ipairs(open) do
|
||||||
|
local distance = distances[comp_map[hash_pos(node)].idx]
|
||||||
|
if not min_distance or distance < min_distance then
|
||||||
|
next_node_idx = i
|
||||||
|
min_distance = distance
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Remove the next node from the open list
|
||||||
|
local node_pos = open[next_node_idx]
|
||||||
|
open[next_node_idx] = open[#open]
|
||||||
|
open[#open] = nil
|
||||||
|
|
||||||
|
-- Process the node
|
||||||
|
local node_hash = hash_pos(node_pos)
|
||||||
|
local node_idx = area:index(node_pos.x, node_pos.y, node_pos.z)
|
||||||
|
local inv_nav_rule = inv_nav_rules[node_idx]
|
||||||
|
local distance = min_distance
|
||||||
|
|
||||||
|
-- Update neighbor distances
|
||||||
|
if inv_nav_rule then
|
||||||
|
for i=1,inv_nav_rule.count do
|
||||||
|
local neighbor_pos = vector.add( inv_nav_rule[i], node_pos )
|
||||||
|
local neighbor_idx = comp_map[hash_pos(neighbor_pos)].idx
|
||||||
|
local neighbor_dist = distances[neighbor_idx]
|
||||||
|
if not neighbor_dist then
|
||||||
|
-- Neighbor hasn't been seen before, add to open list
|
||||||
|
open[#open + 1] = neighbor_pos
|
||||||
|
end
|
||||||
|
if not neighbor_dist or distance + 1 < neighbor_dist then
|
||||||
|
distances[neighbor_idx] = distance + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Now that we have the shortest distance from every navigable node to the target
|
||||||
|
-- node, we can calculate a lookup table that gives us the next neighbor connection
|
||||||
|
-- to take that is along the shortest path to the target node
|
||||||
|
local best_directions = {}
|
||||||
|
local navigation_rules = state.navigation_rules
|
||||||
|
local rule_table_index = state.rule_table.index
|
||||||
|
for _,v in pairs(comp_map) do
|
||||||
|
local pos = v.pos
|
||||||
|
local vm_idx = area:index(pos.x, pos.y, pos.z)
|
||||||
|
local rule = navigation_rules[vm_idx]
|
||||||
|
|
||||||
|
-- Pick the neighbor rule that has the lowest distance to the target node
|
||||||
|
-- This neighbor is the next step along the shortest past to the target
|
||||||
|
-- node from this position
|
||||||
|
local best_rule = nil
|
||||||
|
local best_distance = nil
|
||||||
|
for i,dir in ipairs(rule.neighbors) do
|
||||||
|
local neighbor_pos = vector.add(dir, rule.pos)
|
||||||
|
local neighbor_idx = comp_map[hash_pos(neighbor_pos)].idx
|
||||||
|
local neighbor_distance = distances[neighbor_idx]
|
||||||
|
if not best_distance or ( neighbor_distance and neighbor_distance < best_distance ) then
|
||||||
|
best_distance = distances[neighbor_idx]
|
||||||
|
best_rule = dir
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Store the best direction as an index into the rule table for this block
|
||||||
|
local value = 0
|
||||||
|
if best_rule then
|
||||||
|
local best_dir_hash = hash_pos(best_rule)
|
||||||
|
value = rule_table_index[best_dir_hash]
|
||||||
|
end
|
||||||
|
best_directions[comp_map[hash_pos(rule.pos)].idx] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create the lookup table
|
||||||
|
local join_list = {}
|
||||||
|
for i,best_dir_idx in ipairs(best_directions) do
|
||||||
|
join_list[i] = string.char(best_dir_idx) --HEX[best_dir_idx+1]
|
||||||
|
end
|
||||||
|
local lut = table.concat(join_list,"")
|
||||||
|
local target_node_hash = hash_pos(target_node_pos)
|
||||||
|
local target_node_idx = comp_map[target_node_hash].idx
|
||||||
|
state.navigation_lut[target_node_idx] = lut
|
||||||
|
print(tostring(target_node_idx).." => "..lut)
|
||||||
|
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_all_navigation_luts(state)
|
||||||
|
local navigation_lut = state.navigation_lut
|
||||||
|
local count = navigation_lut.compression_mapping.count
|
||||||
|
navigation_lut.compression_mapping.count = nil
|
||||||
|
|
||||||
|
-- Compute the navigation data for each node that is navigable
|
||||||
|
for _,v in pairs(navigation_lut.compression_mapping) do
|
||||||
|
local pos = v.pos
|
||||||
|
gen_navigation_lut(state, pos)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Calculate a hash value given voxel data
|
||||||
|
local function hash_voxel_data(data, min, max)
|
||||||
|
local area = VoxelArea:new{ MinEdge = min, MaxEdge = max }
|
||||||
|
local content_id_cache = {}
|
||||||
|
|
||||||
|
local node_list = {}
|
||||||
|
local minx = min.x
|
||||||
|
local maxx = max.x
|
||||||
|
local miny = min.x
|
||||||
|
local maxy = max.x
|
||||||
|
for z = min.z,max.z do
|
||||||
|
for y = miny,maxy do
|
||||||
|
for x = minx,maxx do
|
||||||
|
local idx = area:index(x,y,z)
|
||||||
|
local cid = data[idx]
|
||||||
|
local node_name = "unloaded"
|
||||||
|
if cid then
|
||||||
|
node_name = content_id_cache[cid]
|
||||||
|
if not node_name then
|
||||||
|
node_name = minetest.get_name_from_content_id(cid)
|
||||||
|
content_id_cache[cid] = node_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
node_list[#node_list + 1] = node_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return minetest.sha1(table.concat(node_list, ""))
|
||||||
|
end
|
||||||
|
mod.hash_voxel_data = hash_voxel_data
|
||||||
|
|
||||||
|
-- This packages the lookup table data into a more compact representation for disk storage
|
||||||
|
local function pack_navigation_lut(navigation_lut)
|
||||||
|
return minetest.encode_base64(minetest.compress(minetest.serialize(navigation_lut), "zstd" ))
|
||||||
|
end
|
||||||
|
mod.pack_navigation_lut = pack_navigation_lut
|
||||||
|
|
||||||
|
-- The inverse of pack_navigation_lut
|
||||||
|
local function unpack_navigation_lut(packed)
|
||||||
|
local binary = minetest.decode_base64(packed)
|
||||||
|
local uncompressed = minetest.decompress(binary, "zstd")
|
||||||
|
return minetest.deserialize(uncompressed)
|
||||||
|
end
|
||||||
|
mod.unpack_navigation_lut = unpack_navigation_lut
|
||||||
|
|
||||||
|
-- Calculate the navigation lookup table for a given region
|
||||||
|
local function process_recalculate_pathfinding_data(state)
|
||||||
|
state.area = VoxelArea:new{ MinEdge = state.min, MaxEdge = state.max }
|
||||||
|
|
||||||
|
gen_navigation_rules(state)
|
||||||
|
gen_all_navigation_luts(state)
|
||||||
|
|
||||||
|
return state.node_base,pack_navigation_lut(state.navigation_lut)
|
||||||
|
end
|
||||||
|
mod.process_recalculate_pathfinding_data = process_recalculate_pathfinding_data
|
|
@ -0,0 +1,38 @@
|
||||||
|
local modname = minetest.get_current_modname()
|
||||||
|
local modpath = minetest.get_modpath(modname)
|
||||||
|
local S = minetest.get_translator(modname)
|
||||||
|
|
||||||
|
local mod = {}
|
||||||
|
mcl2_pathfinding = mod
|
||||||
|
|
||||||
|
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.load_pathfinding_data(player:get_pos(),function(lut)
|
||||||
|
return
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
if minetest.get_modpath("worldedit_commands") then
|
||||||
|
minetest.register_chatcommand("test_path",{
|
||||||
|
description = S("Test path planning"),
|
||||||
|
params = "",
|
||||||
|
privs = { worldedit=true },
|
||||||
|
func = function(name)
|
||||||
|
local start = worldedit.pos1[name]
|
||||||
|
local stop = worldedit.pos2[name]
|
||||||
|
|
||||||
|
mcl2_pathfinding.clear_path_debug()
|
||||||
|
local path = mcl2_pathfinding.find_path(start, stop)
|
||||||
|
mcl2_pathfinding.debug_path(path)
|
||||||
|
end
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
name = mcl2_pathfinding
|
||||||
|
author = teknomunk
|
||||||
|
description = Adds a pathfinding API
|
||||||
|
optional_depends = worldedit_commands
|
Loading…
Reference in New Issue