Initial commit
commit
e00f176567
|
@ -0,0 +1,11 @@
|
|||
# Footpath Mod
|
||||
|
||||
Minetest mod allowing the building and management of a network of footpath
|
||||
nodes which can be used for route finding and navigation.
|
||||
|
||||
Author: Ciaran Gultnieks
|
||||
License: LGPL
|
||||
|
||||
Textures are modified versions of the 'marker stone' from Sokomine's
|
||||
markers mod.
|
||||
|
|
@ -0,0 +1 @@
|
|||
moddebug?
|
|
@ -0,0 +1,530 @@
|
|||
|
||||
local dbg
|
||||
if moddebug then dbg=moddebug.dbg("footpath") else dbg={v1=function() end,v2=function() end,v3=function() end} end
|
||||
|
||||
footpath = {}
|
||||
|
||||
-- This is the current node graph, which in actually in use. It only changes
|
||||
-- when a footpath.make_pathnodes() produces a new and error-free graph.
|
||||
local pathnodes = {}
|
||||
|
||||
-- This is the definition of all the junctions in the map. It's updated in
|
||||
-- real time as junctions are added/removed/edited in the map. Changes to
|
||||
-- this do not affect node or route finding until footpath.make_pathnodes()
|
||||
-- is called (and detects no errors).
|
||||
--
|
||||
-- It's mostly a duplicate of information in pathnodes - the difference being
|
||||
-- this is a live view of the actual map, which is not in use and may contain
|
||||
-- routing errors or incompleteness.
|
||||
local junctions = {}
|
||||
|
||||
-- This is just a cache of recently calculated routes, to avoid recalculating
|
||||
-- the same thing.
|
||||
local routecache = {}
|
||||
|
||||
|
||||
--- Save the current footpath network status.
|
||||
-- 'what' is a table with either pathnodes=true, junctions=true, or
|
||||
-- both.
|
||||
footpath.save = function(what)
|
||||
|
||||
if what.pathnodes then
|
||||
-- For the purposes of serialisation, we want a copy of the nodes with
|
||||
-- neighbours referenced by node name, not a reference to the actual
|
||||
-- node, otherwise it will serialise horribly!
|
||||
local lnodes = {}
|
||||
for n, nn in pairs(pathnodes) do
|
||||
local nei = {}
|
||||
for dir, ne in pairs(nn.neighbours) do
|
||||
nei[dir] = ne.name
|
||||
end
|
||||
lnodes[n] = {name=n, pos=nn.pos, neighbours=nei}
|
||||
end
|
||||
|
||||
local f = io.open(minetest.get_worldpath().."/footpath_pathnodes.json", "w+")
|
||||
if f then
|
||||
f:write(minetest.write_json(lnodes))
|
||||
f:close()
|
||||
end
|
||||
end
|
||||
|
||||
if what.junctions then
|
||||
local f = io.open(minetest.get_worldpath().."/footpath_junctions.json", "w+")
|
||||
if f then
|
||||
f:write(minetest.write_json(junctions))
|
||||
f:close()
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
--- Load the footpath network status.
|
||||
footpath.load = function()
|
||||
local f = io.open(minetest.get_worldpath().."/footpath_pathnodes.json", "r")
|
||||
if f then
|
||||
pathnodes = minetest.parse_json(f:read("*all"))
|
||||
f:close()
|
||||
|
||||
-- Now we need to undo the dereferencing we did when we saved it...
|
||||
for _, nn in pairs(pathnodes) do
|
||||
local nei = {}
|
||||
if nn.neighbours then
|
||||
for dir, ne in pairs(nn.neighbours) do
|
||||
nei[dir] = pathnodes[ne]
|
||||
end
|
||||
end
|
||||
nn.neighbours = nei
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
f = io.open(minetest.get_worldpath().."/footpath_junctions.json", "r")
|
||||
if f then
|
||||
junctions = minetest.parse_json(f:read("*all"))
|
||||
f:close()
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
--- Build the path nodes graph.
|
||||
-- This is intended to be called via a chat command by a server administrator.
|
||||
-- It builds a graph by interrogating the defined junctions. This graph is then
|
||||
-- used when finding nodes and routes.
|
||||
-- @return A string describing an error in the setup (in which case no changes
|
||||
-- are made), or nil on success (in which case, the new graph will
|
||||
-- take effect).
|
||||
footpath.make_pathnodes = function()
|
||||
|
||||
newpathnodes = {}
|
||||
for _, jct in pairs(junctions) do
|
||||
newpathnodes[jct.name] = {name=jct.name, pos=jct.pos, neighbours={}}
|
||||
end
|
||||
for _, jct in pairs(junctions) do
|
||||
if not jct.exits then
|
||||
return "Junction "..jct.name.." has no exits"
|
||||
end
|
||||
for dir, exit in pairs(jct.exits) do
|
||||
if not junctions[exit] then
|
||||
return "Exit "..exit.." from "..jct.name.." has no destination"
|
||||
end
|
||||
newpathnodes[jct.name].neighbours[dir] = newpathnodes[exit]
|
||||
end
|
||||
end
|
||||
|
||||
for _, pn in pairs(newpathnodes) do
|
||||
for _, nn in pairs(pn.neighbours) do
|
||||
local ok = false
|
||||
for _, nnn in pairs(nn.neighbours) do
|
||||
if nnn == pn then
|
||||
ok = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not ok then
|
||||
return "Route from "..pn.name.." at "..minetest.pos_to_string(pn.pos)..
|
||||
" to "..nn.name.." at "..minetest.pos_to_string(nn.pos)..
|
||||
" does not return"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
pathnodes = newpathnodes
|
||||
routecache = {}
|
||||
footpath.save{pathnodes=true}
|
||||
|
||||
|
||||
|
||||
return nil
|
||||
|
||||
end
|
||||
|
||||
|
||||
-- Get the position of the path node with the given name. Note that they're
|
||||
-- all currently called path_<something>.
|
||||
footpath.get_nodepos = function(name)
|
||||
if not pathnodes[name] then return nil end
|
||||
return pathnodes[name].pos
|
||||
end
|
||||
|
||||
|
||||
-- Returns the nearest footpath node to the given position, optionally
|
||||
-- checking only those matching a given pattern (which is a lua regex).
|
||||
-- The pattern will be amended to make it case-insensitive.
|
||||
-- Returns nil if nothing could be found, otherwise the footpath node
|
||||
-- and the distance to it.
|
||||
-- maxdist is the maximum distance at which to search.
|
||||
footpath.nearest_node = function(pos, pattern, maxdist)
|
||||
|
||||
local nearest, nearestdist
|
||||
if pattern then
|
||||
-- Make the pattern case-insensitive...
|
||||
pattern = pattern:gsub("(%%?)(.)", function(percent, letter)
|
||||
if percent ~= "" or not letter:match("%a") then
|
||||
return percent .. letter
|
||||
else
|
||||
return string.format("[%s%s]", letter:lower(), letter:upper())
|
||||
end
|
||||
end)
|
||||
end
|
||||
for _, pn in pairs(pathnodes) do
|
||||
if (not pattern) or string.find(pn.name, pattern) then
|
||||
local dist = vector.distance(pos, pn.pos)
|
||||
if ((not nearestdist) or dist < nearestdist) and ((not maxdist) or dist <= maxdist) then
|
||||
nearest = pn
|
||||
nearestdist = dist
|
||||
end
|
||||
end
|
||||
end
|
||||
return nearest, nearestdist
|
||||
end
|
||||
|
||||
|
||||
--- Fast removal of a value from a table
|
||||
-- Avoids shuffling by swapping the last item with the one to be removed.
|
||||
-- @param t The table
|
||||
-- @param v The value to remove
|
||||
local function table_fast_remove(t, v)
|
||||
for i, tv in ipairs(t) do
|
||||
if tv == v then
|
||||
t[i] = t[#t]
|
||||
t[#t] = nil
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Check if a table contains the given value
|
||||
-- @param t The table
|
||||
-- @param v The value to check for
|
||||
-- @return True if the table contains the value
|
||||
local function table_contains(t, v)
|
||||
|
||||
for _, tv in ipairs(t) do
|
||||
if tv == v then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- Choose the best node from the list of open ones
|
||||
-- @param open List of nodes
|
||||
-- @param f Corresponding list of f scores for those node
|
||||
-- @return The node with the lowest f score
|
||||
local function choose_best(open, f)
|
||||
|
||||
local lowest, best = 1/0, nil
|
||||
for _, node in ipairs(open) do
|
||||
local ts = f[node]
|
||||
if ts < lowest then
|
||||
lowest, best = ts, node
|
||||
end
|
||||
end
|
||||
return best
|
||||
end
|
||||
|
||||
--- Reverse-iterate along the found route to construct the final path.
|
||||
local function get_path(path, prev, current)
|
||||
|
||||
if not prev[current] then return path end
|
||||
table.insert(path, 1, prev[current])
|
||||
return get_path(path, prev, prev[current])
|
||||
end
|
||||
|
||||
--- Use A* to calculate the route from start to goal. Paths are cached.
|
||||
local function astar(start, goal)
|
||||
|
||||
if not routecache[start] then
|
||||
routecache[start] = {}
|
||||
elseif routecache[start][goal] then
|
||||
dbg.v3("astar used cached path")
|
||||
return routecache[start][goal]
|
||||
end
|
||||
|
||||
local g = {[start]=0}
|
||||
local f = {[start]=vector.distance(start.pos, goal.pos)}
|
||||
local open = {start}
|
||||
local closed = {}
|
||||
local prev = {}
|
||||
|
||||
while #open > 0 do
|
||||
|
||||
local current = choose_best(open, f)
|
||||
if current == goal then
|
||||
local path = get_path({}, prev, goal)
|
||||
table.insert(path, goal)
|
||||
routecache[start][goal] = path
|
||||
return path
|
||||
end
|
||||
|
||||
table_fast_remove(open, current)
|
||||
table.insert(closed, current)
|
||||
|
||||
for _, neighbour in pairs(current.neighbours) do
|
||||
if not table_contains(closed, neighbour) then
|
||||
|
||||
local newg = g[current] + vector.distance(current.pos, neighbour.pos)
|
||||
|
||||
if not table_contains(open, neighbour) or newg < g[neighbour] then
|
||||
prev[neighbour] = current
|
||||
g[neighbour] = newg
|
||||
f[neighbour] = g[neighbour] + vector.distance(neighbour.pos, goal.pos)
|
||||
if not table_contains(open, neighbour) then
|
||||
table.insert(open, neighbour)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
--- Get a footpath route
|
||||
-- @param start Starting node name, e.g. "path_My House"
|
||||
-- @param goal Goal node name, e.g. "path_Your House"
|
||||
-- @return A list of path node names defining the route, including the start
|
||||
-- and the goal, or nil if no route was found.
|
||||
footpath.get_route = function(start, goal)
|
||||
|
||||
if not pathnodes then return nil end
|
||||
if not pathnodes[start] then
|
||||
dbg.v3("Route start "..start.." doesn't exist")
|
||||
return nil
|
||||
end
|
||||
if not pathnodes[goal] then
|
||||
dbg.v3("Route goal "..goal.." doesn't exist")
|
||||
return nil
|
||||
end
|
||||
|
||||
local path = astar(pathnodes[start], pathnodes[goal])
|
||||
if not path then
|
||||
dbg.v3("Route from "..start.." to "..goal.." not found")
|
||||
return nil
|
||||
end
|
||||
|
||||
local rpath = {}
|
||||
for _, n in ipairs(path) do
|
||||
table.insert(rpath, n.name)
|
||||
end
|
||||
dbg.v3("Route from "..start.." to "..goal.." is "..dump(rpath))
|
||||
return rpath
|
||||
|
||||
end
|
||||
|
||||
--- Find the next node along a footpath
|
||||
-- All positions are exact (rounded) node positions.
|
||||
-- @param curpos The position of a current footpath node
|
||||
-- @param lastpos The position of the previous footpath node
|
||||
-- @param samedironly True to only look in the current direction of travel
|
||||
-- @return The position of the next footpath node, or nil if there isn't
|
||||
-- one, or "unloaded" if there may or may not be one, but we can't
|
||||
-- tell because of unloaded blocks.
|
||||
footpath.findnext = function(curpos, lastpos, samedironly)
|
||||
--dbg.v3("footpath_findnext, cur="..minetest.pos_to_string(curpos)..", last="..minetest.pos_to_string(lastpos)..", same="..dump(samedironly))
|
||||
local xz
|
||||
if samedironly then
|
||||
xz = {}
|
||||
else
|
||||
xz = {{x=1,z=0}, {x=0,z=1}, {x=-1,z=0}, {x=0,z=-1}}
|
||||
end
|
||||
-- Favour the direction we're already going
|
||||
-- TODO - that means we could check it twice (but then again, only
|
||||
-- if we reach an invalid bit of footpath!
|
||||
table.insert(xz, 1, {x=curpos.x-lastpos.x, z=curpos.z-lastpos.z})
|
||||
for _, cxz in ipairs(xz) do
|
||||
for y = 1, -1, -1 do
|
||||
local x = cxz.x
|
||||
local z = cxz.z
|
||||
local npos = vector.add(curpos, vector.new(x, y, z))
|
||||
if not vector.equals(npos, lastpos) then
|
||||
local n = minetest.get_node(npos)
|
||||
if n.name == "ignore" then return "unloaded" end
|
||||
if n.name == 'default:cobble' or
|
||||
n.name == 'footpath:junction' or
|
||||
n.name == 'default:mossycobble' or
|
||||
n.name == 'stairs:stair_cobble' or
|
||||
n.name == 'default:wood' or
|
||||
n.name == 'stairs:stair_wood' then
|
||||
local n = minetest.get_node({x=npos.x,y=npos.y+1,z=npos.z})
|
||||
if n.name == "default:snow" then
|
||||
-- Snow is walkable, but we will either walk over it
|
||||
-- or dig it out of the way, so that's fine...
|
||||
--dbg.v3("footpath_findnext found (snow-covered) "..minetest.pos_to_string(npos))
|
||||
return npos
|
||||
end
|
||||
|
||||
-- Otherwise, so long as there's nothing walkable above the
|
||||
-- node it should be a valid footpath node...
|
||||
-- TODO - allowing doors/gates here, as we do elsewhere, but
|
||||
-- really we need to know if the entity can use doors!
|
||||
local nd = minetest.registered_nodes[n.name]
|
||||
local iswalkable = nd.walkable
|
||||
if iswalkable and (
|
||||
(nd.groups.door and nd.groups.door ~= 0) or
|
||||
(nd.groups.gate and nd.groups.gate ~= 0)) then
|
||||
iswalkable = false
|
||||
end
|
||||
|
||||
if not iswalkable then
|
||||
--dbg.v3("footpath_findnext found "..minetest.pos_to_string(npos))
|
||||
return npos
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
footpath.cur_edit_pos = {}
|
||||
|
||||
footpath.show_junction_formspec = function(player, pos)
|
||||
|
||||
local playername = player:get_player_name()
|
||||
if not minetest.check_player_privs(playername, {server=true}) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Our current one and only junction block defines a footpath node one
|
||||
-- above itself. Probably I'll later add ones that can be buried, in
|
||||
-- which case the offset below would be different...
|
||||
local jctpos = vector.add(pos, {x=0, y=1, z=0})
|
||||
|
||||
local editjct
|
||||
for _, jj in pairs(junctions) do
|
||||
if vector.equals(jj.pos, jctpos) then
|
||||
editjct = jj
|
||||
if not editjct.exits then editjct.exits = {} end
|
||||
break
|
||||
end
|
||||
end
|
||||
if not editjct then
|
||||
editjct = {name=nil, exits={}}
|
||||
end
|
||||
|
||||
local formspec = "size[8,9.5]"..
|
||||
"field[1,1;7,1;name;Name:;"..(editjct.name or "").."]"..
|
||||
"field[1,2;7,1;n;North:;"..(editjct.exits["n"] or "").."]"..
|
||||
"field[1,3;7,1;s;South:;"..(editjct.exits["s"] or "").."]"..
|
||||
"field[1,4;7,1;e;East:;"..(editjct.exits["e"] or "").."]"..
|
||||
"field[1,5;7,1;w;West:;"..(editjct.exits["w"] or "").."]"..
|
||||
"button_exit[4,6;2,1;update;Update]"
|
||||
|
||||
footpath.cur_edit_pos[playername] = pos
|
||||
minetest.show_formspec(playername, "footpath:junction_formspec",
|
||||
formspec)
|
||||
|
||||
end
|
||||
|
||||
minetest.register_on_player_receive_fields(function(sender, formname, fields)
|
||||
if formname == "footpath:junction_formspec" then
|
||||
if fields.update then
|
||||
local playername = sender:get_player_name()
|
||||
local pos = footpath.cur_edit_pos[playername]
|
||||
if not pos then return end
|
||||
|
||||
if fields.name == "" then return end
|
||||
|
||||
-- Our current one and only junction block defines a footpath node one
|
||||
-- above itself. Probably I'll later add ones that can be buried, in
|
||||
-- which case the offset below would be different...
|
||||
local jctpos = vector.add(pos, {x=0, y=1, z=0})
|
||||
|
||||
local editjct
|
||||
local new = true
|
||||
for _, jj in pairs(junctions) do
|
||||
if vector.equals(jj.pos, jctpos) then
|
||||
editjct = jj
|
||||
new = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if not editjct then
|
||||
editjct = {name=nil, pos=jctpos, exits={}}
|
||||
end
|
||||
|
||||
-- Check for duplicate name
|
||||
if new or editjct.name ~= fields.name then
|
||||
for _, jj in pairs(junctions) do
|
||||
if jj.name == fields.name then
|
||||
-- TODO - this is quite rude, we should really represent
|
||||
-- the filled in form, with the error message on
|
||||
-- it!
|
||||
minetest.chat_send_player(name, "A junction called "..fields.name.." already exists")
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local renamefrom = nil
|
||||
if not new and editjct.name ~= fields.name then
|
||||
renamefrom = editjct.name
|
||||
end
|
||||
editjct.name = fields.name
|
||||
local dirs = {"n", "s", "e", "w"}
|
||||
for _, dir in pairs(dirs) do
|
||||
if fields[dir] == "" then
|
||||
editjct.exits[dir] = nil
|
||||
else
|
||||
editjct.exits[dir] = fields[dir]
|
||||
end
|
||||
end
|
||||
|
||||
if renamefrom then
|
||||
for _, jct in pairs(junctions) do
|
||||
for _, dir in pairs(dirs) do
|
||||
if jct.exits[dir] == renamefrom then
|
||||
jct.exits[dir] = editjct.name
|
||||
end
|
||||
end
|
||||
end
|
||||
junctions[editjct.name] = editjct
|
||||
junctions[renamefrom] = nil
|
||||
elseif new then
|
||||
junctions[editjct.name] = editjct
|
||||
end
|
||||
|
||||
local meta = minetest.get_meta(pos)
|
||||
meta:set_string("infotext", editjct.name)
|
||||
|
||||
footpath.save{junctions=true}
|
||||
dbg.v1(playername.." updated junction "..editjct.name.." at "..minetest.pos_to_string(pos))
|
||||
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
footpath.load()
|
||||
|
||||
minetest.register_node("footpath:junction", {
|
||||
description = "Footpath junction marker",
|
||||
tiles = {"footpath_top.png", "footpath_side.png", "footpath_side.png",
|
||||
"footpath_side.png", "footpath_side.png", "footpath_side.png"},
|
||||
groups = {cracky=2},
|
||||
legacy_facedir_simple = true,
|
||||
is_ground_content = false,
|
||||
on_rightclick = function(pos, node, clicker)
|
||||
footpath.show_junction_formspec(clicker, pos);
|
||||
end,
|
||||
on_destruct = function(pos)
|
||||
-- Our current one and only junction block defines a footpath node one
|
||||
-- above itself. Probably I'll later add ones that can be buried, in
|
||||
-- which case the offset below would be different...
|
||||
local jctpos = vector.add(pos, {x=0, y=1, z=0})
|
||||
|
||||
for _, jj in pairs(junctions) do
|
||||
if vector.equals(jj.pos, jctpos) then
|
||||
junctions[jj.name] = nil
|
||||
break
|
||||
end
|
||||
end
|
||||
footpath.save{junctions=true}
|
||||
|
||||
end,
|
||||
can_dig = function(pos, player)
|
||||
return minetest.check_player_privs(player:get_player_name(), {server=true})
|
||||
end
|
||||
})
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 755 B |
Binary file not shown.
After Width: | Height: | Size: 745 B |
Loading…
Reference in New Issue