diff --git a/mods/ENTITIES/mcl2_pathfinding/api.lua b/mods/ENTITIES/mcl2_pathfinding/api.lua new file mode 100644 index 000000000..4435f6a78 --- /dev/null +++ b/mods/ENTITIES/mcl2_pathfinding/api.lua @@ -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 + diff --git a/mods/ENTITIES/mcl2_pathfinding/init.lua b/mods/ENTITIES/mcl2_pathfinding/init.lua new file mode 100644 index 000000000..01bb3db87 --- /dev/null +++ b/mods/ENTITIES/mcl2_pathfinding/init.lua @@ -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 +}) + diff --git a/mods/ENTITIES/mcl2_pathfinding/mod.conf b/mods/ENTITIES/mcl2_pathfinding/mod.conf new file mode 100644 index 000000000..9a3509d88 --- /dev/null +++ b/mods/ENTITIES/mcl2_pathfinding/mod.conf @@ -0,0 +1,3 @@ +name = mcl2_pathfinding +author = teknomunk +description = Adds a pathfinding API