Switch to using footpath mod

master
Ciaran Gultnieks 2015-05-09 12:32:33 +01:00
parent cb29cd13d7
commit 642cc58348
7 changed files with 86 additions and 360 deletions

View File

@ -30,7 +30,9 @@ the default minetest game.
## Requirements ## Requirements
Optionally, you might want these mods: Optionally, you might want these mods:
* areas (my fork of it) - https://github.com/CiaranG/areas - for footpath * areas (my fork of it) - https://github.com/CiaranG/areas - for being able to
specify an area as a destination
* footpath - https://gitlab.com/CiaranG/moddebug - for full route finding and
navigation support navigation support
* moddebug - https://gitlab.com/CiaranG/moddebug - for debug logging * moddebug - https://gitlab.com/CiaranG/moddebug - for debug logging

View File

@ -17,61 +17,60 @@ people.actions.go = function(state)
if (not state.action.pos) and (not state.action.footpathdest) and state.action.name then if (not state.action.pos) and (not state.action.footpathdest) and state.action.name then
-- The action says to go to a named place. We're going to convert this -- The action says to go to a named place. We're going to convert this
-- into a position, ready for the next call... -- into a position (if we have a way of doing so, e.g. via the footpath
-- or areas mods), ready for the next call...
if not areas then
dbg.v1(state.ent.name.." can't go to named destination without areas mod")
return true, false
end
if type(state.action.name) ~= "string" then if type(state.action.name) ~= "string" then
dbg.v1(state.ent.name.." can't go to a named destination that isn't a string") dbg.v1(state.ent.name.." can't go to a named destination that isn't a string")
return true, false return true, false
end end
-- Look for a footpath node with the right name -- Look for a footpath node with the right name
local pathdestarea = areas:findNearestArea(state.pos, "^path_"..people.escape_pattern(state.action.name).."$") if footpath then
if pathdestarea then local pathdestpos = footpath.get_nodepos("path_"..state.action.name)
if pathdestpos then
dbg.v2(state.ent.name.." selected path destination "..pathdestarea.name) dbg.v2(state.ent.name.." selected path destination path_"..state.action.name)
-- Find the nearest footpath junction to us. We'll head to that -- Find the nearest footpath junction to us. We'll head to that
-- first (as a pos) and then follow the footpath network from -- first (as a pos) and then follow the footpath network from
-- there. -- there.
local pathstartarea = areas:findNearestArea(state.pos, "^path_.+") local pathstartnode = footpath.nearest_node(state.pos)
if pathstartarea then if pathstartnode then
dbg.v2(state.ent.name.." selected path start "..pathstartarea.name.." at "..minetest.pos_to_string(pathstartarea.pos1)) dbg.v2(state.ent.name.." selected path start "..pathstartnode.name.." at "..minetest.pos_to_string(pathstartnode.pos))
state.action.pos = vector.new(pathstartarea.pos1) state.action.pos = vector.new(pathstartnode.pos)
state.action.pos.y = state.action.pos.y + ANTP state.action.pos.y = state.action.pos.y + ANTP
state.action.footpathdest = pathdestarea state.action.footpathdest = "path_"..state.action.name
return false return false
end
end end
end end
-- No path node, so look for any area with the actual name and -- No path node, so look for any area with the actual name and
-- just go there. -- just go there.
local nearest = areas:findNearestArea(state.pos, "^"..people.escape_pattern(state.action.name).."$") if areas then
if not nearest then local nearest = areas:findNearestArea(state.pos, "^"..people.escape_pattern(state.action.name).."$")
dbg.v2(state.ent.name.." couldn't find location "..state.action.name) if nearest then
return true, false dbg.v2(state.ent.name.." set destination for centre of area "..nearest.name)
state.action.pos = vector.interpolate(nearest.pos1, nearest.pos2, 0.5)
state.action.pos.y = state.action.pos.y - 0.5
return false
end
end end
dbg.v2(state.ent.name.." set destination for centre of area "..nearest.name) dbg.v2(state.ent.name.." couldn't find location "..state.action.name)
state.action.pos = vector.interpolate(nearest.pos1, nearest.pos2, 0.5) return true, false
state.action.pos.y = state.action.pos.y - 0.5
return false
end end
if not state.action.pos and state.action.footpathdest then if not state.action.pos and state.action.footpathdest then
-- path handling - only comes here when we've arrived at a new path square -- path handling - only comes here when we've arrived at a new path square
-- (note, the range passed to findNearestArea determines our lookahead distance -- (note, the range passed to nearest_node determines our lookahead distance
dbg.v3(state.ent.name.." at new footpath node at "..minetest.pos_to_string(state.pos)) dbg.v3(state.ent.name.." at new footpath node at "..minetest.pos_to_string(state.pos))
local curpatharea, distance = areas:findNearestArea(state.pos, "^path_.+", nil, 10) local curpathnode, distance = footpath.nearest_node(state.pos, nil, 10)
if curpatharea and curpatharea.pos1.x == state.pos.x and curpatharea.pos1.z == state.pos.z then if curpathnode and curpathnode.pos.x == state.pos.x and curpathnode.pos.z == state.pos.z then
if curpatharea.name == state.action.footpathdest.name then if curpathnode.name == state.action.footpathdest then
dbg.v2(state.ent.name.." arrived at footpath destination") dbg.v2(state.ent.name.." arrived at footpath destination")
if state.action.endpos then if state.action.endpos then
state.action.footpathdest = nil state.action.footpathdest = nil
@ -87,21 +86,37 @@ people.actions.go = function(state)
end end
-- We're at a junction -- We're at a junction
dbg.v2(state.ent.name.." is at footpath junction "..curpatharea.name.." - looking for route to "..state.action.name) dbg.v2(state.ent.name.." is at footpath junction "..curpathnode.name.." - looking for route to "..state.action.name)
state.action.lastpathpos = vector.new(state.pos.x, state.pos.y - PNTP, state.pos.z) state.action.lastpathpos = vector.new(state.pos.x, state.pos.y - PNTP, state.pos.z)
local nextpathdest = people.get_footpath_route(curpatharea.name, "path_"..state.action.name) local nextpathdest = footpath.get_route(curpathnode.name, "path_"..state.action.name)
if not nextpathdest then if not nextpathdest then
dbg.v2(state.ent.name.." can't find route") dbg.v1(state.ent.name.." can't find route")
return true, false return true, false
end end
local tonodename = "^to_"..string.sub(nextpathdest[2], 6).."$" local exitoff = nil
nextpathdest = areas:findNearestArea(state.pos, tonodename) for dir, exitto in pairs(curpathnode.neighbours) do
if not nextpathdest then if exitto.name == nextpathdest[2] then
dbg.v2(state.ent.name.." can't find first route node "..tonodename) if dir == "n" then
exitoff = {x=0, z=1}
elseif dir == "s" then
exitoff = {x=0, z=-1}
elseif dir == "e" then
exitoff = {x=1, z=0}
elseif dir == "w" then
exitoff = {x=-1, z=0}
else
dbg.v1(state.ent.name.." can't go in direction "..dir)
return true, false
end
break
end
end
if not exitoff then
dbg.v1(state.ent.name.." can't get exit offset from "..curpathnode.name.." to "..nextpathdest[2])
return true, false return true, false
end end
dbg.v2(state.ent.name.." leaving junction via "..nextpathdest.name) dbg.v2(state.ent.name.." leaving junction towards "..nextpathdest[2])
state.action.pos = vector.add(nextpathdest.pos1, {x=0, y=ANTP, z=0}) state.action.pos = vector.add(curpathnode.pos, {x=exitoff.x, y=ANTP, z=exitoff.z})
return false return false
end end
@ -114,7 +129,7 @@ people.actions.go = function(state)
-- extra 0.5 allowing for dodgy step-ups, for now... -- extra 0.5 allowing for dodgy step-ups, for now...
local thispathpos = vector.round({x=state.pos.x, y=state.pos.y + 0.5, z=state.pos.z}) local thispathpos = vector.round({x=state.pos.x, y=state.pos.y + 0.5, z=state.pos.z})
thispathpos.y = thispathpos.y - 1 thispathpos.y = thispathpos.y - 1
local nextpathpos = people.footpath_findnext(thispathpos, state.action.lastpathpos, false) local nextpathpos = footpath.findnext(thispathpos, state.action.lastpathpos, false)
if nextpathpos == "unloaded" then if nextpathpos == "unloaded" then
state.wait = 1 state.wait = 1
dbg.v3(state.ent.name.." waiting for block load") dbg.v3(state.ent.name.." waiting for block load")
@ -130,7 +145,7 @@ people.actions.go = function(state)
if not distance or distance > 10 then distance = 10 end if not distance or distance > 10 then distance = 10 end
while distance > 2 do while distance > 2 do
-- Look ahead a bit while going in a straight line, to increase speed -- Look ahead a bit while going in a straight line, to increase speed
local aheadpathpos = people.footpath_findnext(nextpathpos, thispathpos, true) local aheadpathpos = footpath.findnext(nextpathpos, thispathpos, true)
if aheadpathpos == "unloaded" then if aheadpathpos == "unloaded" then
state.wait = 1 state.wait = 1
dbg.v3(state.ent.name.." waiting for block load for lookahead") dbg.v3(state.ent.name.." waiting for block load for lookahead")
@ -174,16 +189,16 @@ people.actions.go = function(state)
-- If it's a long way to the destination position, see if we can find -- If it's a long way to the destination position, see if we can find
-- a route using a footpath instead. -- a route using a footpath instead.
if distance > 30 and not state.action.footpathdest then if footpath and distance > 30 and not state.action.footpathdest then
local pathstartarea, psdist = areas:findNearestArea(state.pos, "^path_.+") local pathstartnode = footpath.nearest_node(state.pos, nil, distance)
if pathstartarea and psdist < distance then if pathstartnode then
local pathdestarea, psdist = areas:findNearestArea(state.action.pos, "^path_.+") local pathdestnode = footpath.nearest_node(state.action.pos, nil, distance)
if pathdestarea and psdist < distance then if pathdestarea then
state.action.endpos = state.action.pos state.action.endpos = state.action.pos
state.action.pos = vector.new(pathstartarea.pos1) state.action.pos = vector.new(pathstartnode.pos)
state.action.pos.y = state.action.pos.y + ANTP state.action.pos.y = state.action.pos.y + ANTP
state.action.footpathdest = pathdestarea state.action.footpathdest = pathdestnode.name
dbg.v2(state.ent.name.." using footpath from "..pathstartarea.name.." to "..pathdestarea.name.." for intermediate route") dbg.v2(state.ent.name.." using footpath from "..pathstartnode.name.." to "..pathdestnode.name.." for intermediate route")
return false return false
end end
end end

View File

@ -64,7 +64,7 @@ subcmd.footpath_update = {
return false, "Only admins can update the footpath node graph" return false, "Only admins can update the footpath node graph"
end end
response = people.footpath_make_pathnodes() response = footpath.make_pathnodes()
if response then if response then
return false, response return false, response
end end

View File

@ -1,2 +1,3 @@
areas? areas?
footpath?
mod_debug? mod_debug?

View File

@ -1,301 +0,0 @@
local dbg
if moddebug then dbg=moddebug.dbg("people") else dbg={v1=function() end,v2=function() end,v3=function() end} end
local pathnodes = nil
local routecache = {}
--- Save the current footpath network status.
people.footpath_save = function()
if not pathnodes then return end
-- 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 _, ne in pairs(nn.neighbours) do
table.insert(nei, ne.name)
end
lnodes[n] = {name=n, pos=nn.pos, neighbours=nei}
end
local f = io.open(minetest.get_worldpath().."/people_footpaths.json", "w+")
if f then
f:write(minetest.write_json(lnodes))
f:close()
end
end
--- Load the footpath network status.
people.footpath_load = function()
local f = io.open(minetest.get_worldpath().."/people_footpaths.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 _, ne in pairs(nn.neighbours) do
table.insert(nei, pathnodes[ne])
end
end
nn.neighbours = nei
end
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 areas. This graph is then
-- used when finding paths.
-- @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).
people.footpath_make_pathnodes = function()
newpathnodes = {}
local allnodes = areas:getAreas({x=0,y=0,z=0}, "^path_.*", nil)
for _, pn in pairs(allnodes) do
if not vector.equals(pn.pos1, pn.pos2) then
return "Path node "..pn.name.." is the wrong size"
end
newpathnodes[pn.name] = {name=pn.name, pos=pn.pos1, neighbours={}}
end
for _, pn in ipairs(allnodes) do
local exits = areas:getAreas(pn.pos1, "^to_.*", 1.5)
for _, tn in pairs(exits) do
if tn.name ~= "to_*" then
exname = tn.name:sub(4)
if not newpathnodes["path_"..exname] then
return "Exit "..exname.." at "..minetest.pos_to_string(tn.pos1).." has no destination"
end
table.insert(newpathnodes[pn.name].neighbours, newpathnodes["path_"..exname])
end
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 = {}
people.footpath_save()
return nil
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 ipairs(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.
people.get_footpath_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
path = astar(pathnodes[start], pathnodes[goal])
if not path then
dbg.v3("Route from "..start.." to "..goal.." not found")
return nil
end
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.
people.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 == '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

View File

@ -9,7 +9,6 @@ local people_modpath = minetest.get_modpath("people")
dofile(people_modpath .. "/commands.lua") dofile(people_modpath .. "/commands.lua")
dofile(people_modpath .. "/tracking.lua") dofile(people_modpath .. "/tracking.lua")
dofile(people_modpath .. "/form.lua") dofile(people_modpath .. "/form.lua")
dofile(people_modpath .. "/footpath.lua")
-- Contains all action handlers. Each action handler takes a 'state' table as -- Contains all action handlers. Each action handler takes a 'state' table as
@ -33,8 +32,6 @@ dofile(people_modpath .. "/actions/pickup.lua")
dofile(people_modpath .. "/actions/rightclicknode.lua") dofile(people_modpath .. "/actions/rightclicknode.lua")
dofile(people_modpath .. "/actions/harvest.lua") dofile(people_modpath .. "/actions/harvest.lua")
people.footpath_load()
people.presets = {} people.presets = {}
for p, priv in pairs({FollowOwner=false, for p, priv in pairs({FollowOwner=false,
SimpleCommands=false, SimpleCommands=false,

View File

@ -21,8 +21,11 @@ if event.type == "program" then
"farming_plus:orange_seed 20", "farming_plus:orange_seed 20",
"farming_plus:potato_seed 20", "farming_plus:potato_seed 20",
"farming_plus:rhubarb_seed 20", "farming_plus:rhubarb_seed 20",
"farming:pumpkin_seed 20",
"farming_plus:strawberry_seed 20"}}, "farming_plus:strawberry_seed 20"}},
{"go", name="Ciaran's Farm"}, {"go", name="Ciaran's Farm"},
-- Inside gate
{"go", pos={x=174, y=11.5, z=320}},
-- Row 1 -- Row 1
@ -85,8 +88,16 @@ if event.type == "program" then
{"wait", time=5}, {"wait", time=5},
{"gather"}, {"gather"},
-- Pumpkin patch
{"go", pos={x=194, y=11.5, z=322}},
{"go", pos={x=202, y=11.5, z=322}},
{"gather", nodes={"farming:weed", "farming:pumpkin"}, plant="farming:pumpkin_seed"},
{"wait", time=5},
{"gather"},
-- Finished. Save seed stock... -- Finished. Save seed stock...
{"go", pos={x=174, y=11.5, z=320}},
{"go", name="Ciaran's Farm"}, {"go", name="Ciaran's Farm"},
{"wait", time=10}, {"wait", time=10},
@ -99,12 +110,13 @@ if event.type == "program" then
"farming_plus:potato_seed 20", "farming_plus:potato_seed 20",
"farming_plus:orange_seed 20", "farming_plus:orange_seed 20",
"farming_plus:rhubarb_seed 20", "farming_plus:rhubarb_seed 20",
"farming:pumpkin_seed 20",
"farming_plus:strawberry_seed 20"}}, "farming_plus:strawberry_seed 20"}},
-- Load up items... -- Load up items...
{"go", name="Ciaran's Farm"}, {"go", name="Ciaran's Farm"},
{"go", pos={x=174, y=11.5, z=319}}, {"go", pos={x=174, y=11.5, z=320}},
{"go", pos={x=174, y=11.5, z=341}}, {"go", pos={x=174, y=11.5, z=341}},
{"stash", dest="default:chest", items={"*farming:weed", "*farming:cotton", {"stash", dest="default:chest", items={"*farming:weed", "*farming:cotton",
@ -117,7 +129,7 @@ if event.type == "program" then
"*farming_plus:orange_item", "*farming_plus:orange_item",
"*farming_plus:strawberry_item", "*farming_plus:strawberry_seed"}}, "*farming_plus:strawberry_item", "*farming_plus:strawberry_seed"}},
{"go", pos={x=174, y=11.5, z=319}}, {"go", pos={x=174, y=11.5, z=320}},
-- ***************** -- *****************
-- Tree Farm -- Tree Farm