From c0673612b1bdf0be5bdc357089b428c1e8b9de31 Mon Sep 17 00:00:00 2001 From: Matt Blunt Date: Sun, 7 Feb 2016 18:20:06 +0000 Subject: [PATCH] Add files --- aStar | 166 +++++++++++++++++++++++ location | 131 +++++++++++++++++++ maps/compactMap | 209 +++++++++++++++++++++++++++++ maps/map | 142 ++++++++++++++++++++ maps/remoteMap_client | 185 ++++++++++++++++++++++++++ maps/remoteMap_server | 163 +++++++++++++++++++++++ maps/remoteMap_viewer | 117 +++++++++++++++++ maps/tinyMap | 222 +++++++++++++++++++++++++++++++ maps/viewMap | 128 ++++++++++++++++++ netNav | 296 ++++++++++++++++++++++++++++++++++++++++++ netNav_goto | 27 ++++ pQueue | 108 +++++++++++++++ starNav | 291 +++++++++++++++++++++++++++++++++++++++++ starNav_goto | 19 +++ 14 files changed, 2204 insertions(+) create mode 100644 aStar create mode 100644 location create mode 100644 maps/compactMap create mode 100644 maps/map create mode 100644 maps/remoteMap_client create mode 100644 maps/remoteMap_server create mode 100644 maps/remoteMap_viewer create mode 100644 maps/tinyMap create mode 100644 maps/viewMap create mode 100644 netNav create mode 100644 netNav_goto create mode 100644 pQueue create mode 100644 starNav create mode 100644 starNav_goto diff --git a/aStar b/aStar new file mode 100644 index 0000000..656721e --- /dev/null +++ b/aStar @@ -0,0 +1,166 @@ +if not pQueue then + if not os.loadAPI("pQueue") then + error("could not load pQueue API") + end +end + +-- a very basic map API used to store node information +local mapMethods = { + get = function(self, tVector) + if self.map[tVector.x] and self.map[tVector.x][tVector.y] then + return self.map[tVector.x][tVector.y][tVector.z] + end + end, + set = function(self, tVector, value) + if not self.map[tVector.x] then + self.map[tVector.x] = {} + end + if not self.map[tVector.x][tVector.y] then + self.map[tVector.x][tVector.y] = {} + end + self.map[tVector.x][tVector.y][tVector.z] = value + return self.map[tVector.x][tVector.y][tVector.z] + end, + getOrSet = function(self, tVector, value) + if self.map[tVector.x] and self.map[tVector.x][tVector.y] and self.map[tVector.x][tVector.y][tVector.z] ~= nil then + return self.map[tVector.x][tVector.y][tVector.z], false + else + return self:set(tVector, value), true + end + end, +} +local mapMetatable = {__index = mapMethods} + +function newMap() + return setmetatable({map = {}}, mapMetatable) +end + +local function makePath(nodes, start, startEnd, goalStart, goal) + local current, path = startEnd, {} + while not vectorEquals(current, start) do + table.insert(path, current) + current = nodes:get(current)[1] + end + current = goalStart + while not vectorEquals(current, goal) do + table.insert(path, 1, current) + current = nodes:get(current)[1] + end + table.insert(path, 1, goal) + return path +end + +function vectorEquals(a, b) -- the comparison function used in pQueue + return a.x == b.x and a.y == b.y and a.z == b.z +end + +local posZ = vector.new(0, 0, 1) +local negX = vector.new(-1, 0, 0) +local negZ = vector.new(0, 0, -1) +local posX = vector.new(1, 0, 0) +local posY = vector.new(0, 1, 0) +local negY = vector.new(0, -1, 0) +function adjacent(u) + return { + u + posZ, + u + negX, + u + negZ, + u + posX, + u + posY, + u + negY, + } +end + +function distance(a, b) -- 1-norm/manhattan metric + return math.abs(a.x - b.x) + math.abs(a.y - b.y) + math.abs(a.z - b.z) +end + +function compute(distanceFunction, start, goal) + + if type(distanceFunction) ~= "function" then + error("aStar new: distanceFunction must be of type function", 2) + end + + local distanceFunc = distanceFunction -- is this necessary? + + -- node data structure is {parent node, true cost from startNode/goalNode, whether in closed list, search direction this node was found in, whether in open list} + local nodes = newMap() + nodes:set(start, {start + vector.new(0, 0, -1), 0, false, true, true}) + nodes:set(goal, {goal + vector.new(0, 0, -1), 0, false, false, true}) + + local openStartSet = pQueue.new() + openStartSet:insert(start, distance(start, goal)) + local openGoalSet = pQueue.new() + openGoalSet:insert(goal, distance(start, goal)) + + local yieldCount = 0 + local activeOpenSet, pendingOpenSet = openStartSet, openGoalSet + local forwardSearch, lastNode, switch = true, false, false + + local current, currNode, parent + local baseCost + local newCost + local nbrNode, newNode + local preHeuristic + + while not openStartSet:isEmpty() and not openGoalSet:isEmpty() do + + --yield every so often to avoid getting timed out + yieldCount = yieldCount + 1 + if yieldCount > 200 then + os.pullEvent(os.queueEvent("yield")) + yieldCount = 0 + end + + if switch then --switch the search direction + activeOpenSet, pendingOpenSet = pendingOpenSet, activeOpenSet + forwardSearch = not forwardSearch + lastNode = false + end + + current = activeOpenSet:pop() + currNode = nodes:get(current) + parent = current - currNode[1] + + currNode[3], currNode[5], switch = true, false, true + + for _, neighbour in ipairs(adjacent(current)) do + + baseCost = distanceFunc(current, neighbour) + if baseCost < math.huge then -- if not graph:get(neighbour) then + + newCost = currNode[2] + baseCost + + nbrNode, newNode = nodes:getOrSet(neighbour, {current, newCost, false, forwardSearch, false}) + if switch and ((not lastNode) or vectorEquals(lastNode, neighbour)) then + switch = false + end + + if not newNode then + if forwardSearch ~= nbrNode[4] then -- nbrNode has been discovered in the opposite search direction + if nbrNode[3] then -- and is in the closed list so has been expanded already + return makePath(nodes, start, (forwardSearch and current) or neighbour, (forwardSearch and neighbour) or current, goal) + end + elseif newCost < nbrNode[2] then + if nbrNode[5] then + activeOpenSet:remove(neighbour, vectorEquals) + nbrNode[5] = false + end + nbrNode[3] = false + end + end + + if (newNode or (forwardSearch ~= nbrNode[4] and not nbrNode[5] and not nbrNode[3])) and newCost < math.huge then + nbrNode[1] = current + nbrNode[2] = newCost + nbrNode[4] = currNode[4] + nbrNode[5] = true + preHeuristic = distance(neighbour, (forwardSearch and goal) or start) + activeOpenSet:insert(neighbour, newCost + preHeuristic + 0.0001*(preHeuristic + parent.length(parent:cross(neighbour - current)))) + end + end + end + lastNode = current + end + return false +end \ No newline at end of file diff --git a/location b/location new file mode 100644 index 0000000..e19a2da --- /dev/null +++ b/location @@ -0,0 +1,131 @@ +-- a slightly modified version of an API I found somewhere +-- the headings have been altered to use the Minecraft directions +local location = { + add = function(self, o) + return vector.new( + self.x + o.x, + self.y + o.y, + self.z + o.z + ) + end, + sub = function(self, o) + return vector.new( + self.x - o.x, + self.y - o.y, + self.z - o.z + ) + end, + mul = function(self, m) + return vector.new( + self.x * m, + self.y * m, + self.z * m + ) + end, + div = function(self, d) + return vector.new( + self.x / d, + self.y / d, + self.z / d + ) + end, + left = function(self) + if turtle.turnLeft() then + self.h = (self.h - 1) % 4 + return true + end + return false + end, + right = function(self) + if turtle.turnRight() then + self.h = (self.h + 1) % 4 + return true + end + return false + end, + forward = function(self) + if turtle.forward() then + self.x = self.x + (self.h - 2) * (self.h % 2) + self.z = self.z + (1 - self.h) * ((self.h + 1) % 2) + return true + end + return false + end, + back = function(self) + if turtle.back() then + self.x = self.x - (self.h - 2) * (self.h % 2) + self.z = self.z - (1 - self.h) * ((self.h + 1) % 2) + return true + end + return false + end, + up = function(self) + if turtle.up() then + self.y = self.y + 1 + return true + end + return false + end, + down = function(self) + if turtle.down() then + self.y = self.y - 1 + return true + end + return false + end, + moveDeltas = function(self) + return (self.h - 2) * (self.h % 2), (1 - self.h) * ((self.h + 1) % 2) + end, + setHeading = function(self, heading) + if not heading or heading < 0 or heading > 3 then return nil, "Heading Not in Range" end + while self.h ~= heading do + if (self.h + 1) % 4 == heading % 4 then + self:right() + else + self:left() + end + end + return true + end, + getHeading = function(self) + return self.h + end, + tovector = function(self) + if vector then + return vector.new( + self.x, + self.y, + self.z + ) + else + return nil + end + end, + tostring = function(self) + return self.x..","..self.y..","..self.z..","..self.h + end, + value = function(self) + return self.x, self.y, self.z, self.h + end, +} + +local lmetatable = { + __index = location, + __add = location.add, + __sub = location.sub, + __mul = location.mul, + __div = location.div, + __unm = function(l) return l:mul(-1) end, + __tostring = function(l) return l:tostring() end, +} + +function new( x, y, z, h ) + local l = { + x = x or 0, + y = y or 0, + z = z or 0, + h = h or 1 + } + setmetatable( l, lmetatable ) + return l +end \ No newline at end of file diff --git a/maps/compactMap b/maps/compactMap new file mode 100644 index 0000000..7d0ddf4 --- /dev/null +++ b/maps/compactMap @@ -0,0 +1,209 @@ +local BIT_MASKS = { + SET_COORD = bit.blshift(1, 7), + SET_X_COORD = bit.blshift(1, 6), + COORD = 15, + COORD_DATA = bit.blshift(7, 4), +} + +local function isValidValue(value) + return value == nil or (type(value) == "number" and value % 1 == 0 and value >= 0 and value <= 7) +end + +local function toGridCode(tVector) + return math.floor(tVector.x/16), math.floor(tVector.y/16), math.floor(tVector.z/16), tVector.x % 16, tVector.y % 16, tVector.z % 16 +end + +local function setGrid(tMap, x, y, z, grid) + if not tMap.map[x] then + tMap.map[x] = {} + end + if not tMap.map[x][y] then + tMap.map[x][y] = {} + end + tMap.map[x][y][z] = grid + return tMap.map[x][y][z] +end + +local function getGrid(tMap, x, y, z) + if not tMap.map[x] or not tMap.map[x][y] or not tMap.map[x][y][z] then + return tMap:load(x, y, z) + else + return tMap.map[x][y][z] + end +end + +local mapMethods = { + + getGrid = function(self, tVector, y, z) + local gX, gY, gZ + if y and z then + gX, gY, gZ = tVector, y, z + else + gX, gY, gZ = toGridCode(tVector) + end + return getGrid(self, gX, gY, gZ) + end, + + load = function(self, tVector, y, z) + local gX, gY, gZ + if y and z then + gX, gY, gZ = tVector, y, z + else + gX, gY, gZ = toGridCode(tVector) + end + local gridPath = fs.combine(self.mapDir, gX..","..gY..","..gZ) + if fs.exists(gridPath) then + local handle = fs.open(gridPath, "rb") + if handle then + local grid = {} + + --load grid data + local currX, currY, currZ + local dataByte = handle.read() + while dataByte do + if bit.band(dataByte, BIT_MASKS.SET_COORD) == BIT_MASKS.SET_COORD then + --we are changing our currX or currY coord + if bit.band(dataByte, BIT_MASKS.SET_X_COORD) == BIT_MASKS.SET_X_COORD then + --we are changing our currX coord + currX = bit.band(dataByte, BIT_MASKS.COORD) + else + --we are changing our currY coord + currY = bit.band(dataByte, BIT_MASKS.COORD) + end + else + --we are setting the value for a proper coord + currZ = bit.band(dataByte, BIT_MASKS.COORD) + if currX and currY and currZ then + if not grid[currX] then + grid[currX] = {} + end + if not grid[currX][currY] then + grid[currX][currY] = {} + end + grid[currX][currY][currZ] = bit.brshift(bit.band(dataByte, BIT_MASKS.COORD_DATA), 4) + end + end + dataByte = handle.read() + end + handle.close() + return setGrid(self, gX, gY, gZ, grid) + end + end + return setGrid(self, gX, gY, gZ, {}) + end, + + loadAll = function(self) + if fs.exists(self.mapDir) and fs.isDir(self.mapDir) then + for _, gridFile in ipairs(fs.list(self.mapDir)) do + local _, _, gX, gY, gZ = string.find(gridFile, "(.+)%,(.+)%,(.+)") + if gX and gY and gX then + self:load(tonumber(gX), tonumber(gY), tonumber(gZ)) + end + end + end + end, + + save = function(self, tVector, y, z) + local gX, gY, gZ + if y and z then + gX, gY, gZ = tVector, y, z + else + gX, gY, gZ = toGridCode(tVector) + end + if self.map[gX] and self.map[gX][gY] and self.map[gX][gY][gZ] then + local grid = self.map[gX][gY][gZ] + if next(grid) then + local handle = fs.open(fs.combine(self.mapDir, gX..","..gY..","..gZ), "wb") + if handle then + for x, gridYZ in pairs(grid) do + handle.write(BIT_MASKS.SET_COORD + BIT_MASKS.SET_X_COORD + x) + for y, gridZ in pairs(gridYZ) do + handle.write(BIT_MASKS.SET_COORD + y) + for z, coordValue in pairs(gridZ) do + handle.write(bit.blshift(coordValue, 4) + z) + end + end + end + handle.close() + end + else + fs.delete(fs.combine(self.mapDir, gX..","..gY..","..gZ)) + end + end + end, + + saveAll = function(self) + for gX, YZmap in pairs(self.map) do + for gY, Zmap in pairs(YZmap) do + for gZ, grid in pairs(Zmap) do + self:save(gX, gY, gZ) + end + end + end + end, + + get = function(self, tVector) + local gX, gY, gZ, pX, pY, pZ = toGridCode(tVector) + local grid = getGrid(self, gX, gY, gZ) + if grid[pX] and grid[pX][pY] then + return grid[pX][pY][pZ] + end + end, + + set = function(self, tVector, value) + if not isValidValue(value) then + --should we throw an error or use a default value? + error("compactMap set: value is not valid", 2) + end + local gX, gY, gZ, pX, pY, pZ = toGridCode(tVector) + local grid = getGrid(self, gX, gY, gZ) + if not grid[pX] then + grid[pX] = {} + end + if not grid[pX][pY] then + grid[pX][pY] = {} + end + grid[pX][pY][pZ] = value + return grid[pX][pY][pZ] + end, + + getOrSet = function(self, tVector, value) + local gX, gY, gZ, pX, pY, pZ = toGridCode(tVector) + local grid = getGrid(self, gX, gY, gZ) + if grid[pX] and grid[pX][pY] and grid[pX][pY][pZ] then + return grid[pX][pY][pZ], false + else + if not isValidValue(value) then + --should we throw an error or use a default value? + error("compactMap getOrSet: value is not valid", 2) + end + if not grid[pX] then + grid[pX] = {} + end + if not grid[pX][pY] then + grid[pX][pY] = {} + end + grid[pX][pY][pZ] = value + return grid[pX][pY][pZ], true + end + end, + +} +local mapMetatable = {__index = mapMethods} + +function new(mapDir) + local tMap = {} + if type(mapDir) == "string" then + if not fs.exists(mapDir) then + fs.makeDir(mapDir) + elseif not fs.isDir(mapDir) then + error("compactMap new: not a valid directory", 2) + end + tMap.mapDir = mapDir + else + error("compactMap new: directory must be string", 2) + end + tMap.map = {} + setmetatable(tMap, mapMetatable) + return tMap +end \ No newline at end of file diff --git a/maps/map b/maps/map new file mode 100644 index 0000000..9866739 --- /dev/null +++ b/maps/map @@ -0,0 +1,142 @@ +local maps = {} + +local function toGridCode(tVector) + return math.floor(tVector.x/16), math.floor(tVector.y/16), math.floor(tVector.z/16), tVector.x % 16, tVector.y % 16, tVector.z % 16 +end + +local function setGrid(tMap, x, y, z, grid) + if not tMap.map[x] then + tMap.map[x] = {} + end + if not tMap.map[x][y] then + tMap.map[x][y] = {} + end + tMap.map[x][y][z] = grid + return tMap.map[x][y][z] +end + +local function getGrid(tMap, x, y, z) + if not tMap.map[x] or not tMap.map[x][y] or not tMap.map[x][y][z] then + return tMap:load(x, y, z) + else + return tMap.map[x][y][z] + end +end + +local methods = { + + load = function(self, tVector, y, z) + local gX, gY, gZ + if y and z then + gX, gY, gZ = tVector, y, z + else + gX, gY, gZ = toGridCode(tVector) + end + if self.name then + if fs.exists(".maps/"..self.name.."/"..gX..","..gY..","..gZ) then + local handle = fs.open(".maps/"..self.name.."/"..gX..","..gY..","..gZ, "r") + if handle then + local grid = handle.readAll() + handle.close() + for i = 15, 0, -1 do + grid = string.gsub(grid, tostring(i).."=", "%["..tostring(i).."%]=") + end + grid = textutils.unserialize(grid) + if type(grid) == "table" then + return setGrid(self, gX, gY, gZ, grid) + end + end + end + end + return setGrid(self, gX, gY, gZ, {}) + end, + + loadAll = function(self) + if self.name and fs.exists(".maps/"..self.name) and fs.isDir(".maps/"..self.name) then + for _, gridFile in ipairs(fs.list(".maps/"..self.name)) do + local _, _, gX, gY, gZ = string.find(gridFile, "(.+)%,(.+)%,(.+)") + if gX and gY and gX then + self:load(tonumber(gX), tonumber(gY), tonumber(gZ)) + end + end + end + end, + + save = function(self) + if self.name then + local saveDir = ".maps/"..self.name + for x, YZmap in pairs(self.map) do + for y, Zmap in pairs(YZmap) do + for z, grid in pairs(Zmap) do + if next(grid) then + local handle = fs.open(fs.combine(saveDir, x..","..y..","..z), "w") + local data = textutils.serialize(grid) + data = string.gsub(data, " ", "") + for i = 0, 15 do + data, num = string.gsub(data, "%["..tostring(i).."%]=", tostring(i).."=") + end + handle.write(data) + handle.close() + end + end + end + end + end + end, + + get = function(self, tVector) + local gX, gY, gZ, pX, pY, pZ = toGridCode(tVector) + local grid = getGrid(self, gX, gY, gZ) + if grid[pX] and grid[pX][pY] then + return grid[pX][pY][pZ] + end + end, + + set = function(self, tVector, value) + local gX, gY, gZ, pX, pY, pZ = toGridCode(tVector) + local grid = getGrid(self, gX, gY, gZ) + if not grid[pX] then + grid[pX] = {} + end + if not grid[pX][pY] then + grid[pX][pY] = {} + end + grid[pX][pY][pZ] = value + return grid[pX][pY][pZ] + end, + + getOrSet = function(self, tVector, value) + local gX, gY, gZ, pX, pY, pZ = toGridCode(tVector) + local grid = getGrid(self, gX, gY, gZ) + if grid[pX] and grid[pX][pY] and grid[pX][pY][pZ] then + return grid[pX][pY][pZ], false + else + if not grid[pX] then + grid[pX] = {} + end + if not grid[pX][pY] then + grid[pX][pY] = {} + end + grid[pX][pY][pZ] = value + return grid[pX][pY][pZ], true + end + end, + +} + +function new(name) + local tMap = {} + if name and type(name) == "string" then + if maps[name] then + return maps[name] + end + tMap.name = name + if not fs.exists(".maps/"..name) then + fs.makeDir(".maps/"..name) + end + maps[name] = tMap + end + tMap.map = {} + setmetatable(tMap, {__index = methods}) + return tMap +end \ No newline at end of file diff --git a/maps/remoteMap_client b/maps/remoteMap_client new file mode 100644 index 0000000..8cbeeb9 --- /dev/null +++ b/maps/remoteMap_client @@ -0,0 +1,185 @@ +local REDNET_TIMEOUT = 1 + +local MESSAGE_TYPE = { + GET = 0, + SET = 1, +} + +local function newMessage(messageType, grid, data) + return { + type = messageType, + ID = math.random(0, 2^30), + grid = grid, + data = data, + } +end + +local function sendAndWaitForResponse(recipientID, message, protocol) + rednet.send(recipientID, message, protocol) + local attemptNumber = 1 + while true do + local senderID, reply = rednet.receive(protocol, REDNET_TIMEOUT) + if senderID == recipientID and type(reply) == "table" and reply.type == message.type and reply.ID == message.ID then + return reply.data + elseif not senderID then + if attemptNumber < 3 then + rednet.send(recipientID, message, protocol) + attemptNumber = attemptNumber + 1 + else + return false + end + end + end +end + +local function isValidValue(value) + return value == nil or value == -1 or value == 1 +end + +local function toGridCode(tVector) + return math.floor(tVector.x/16), math.floor(tVector.y/16), math.floor(tVector.z/16), tVector.x % 16, tVector.y % 16, tVector.z % 16 +end + +local function getRemoteGrid(tMap, x, y, z) + if not tMap.remoteGrids[x] or not tMap.remoteGrids[x][y] or not tMap.remoteGrids[x][y][z] then + local message = newMessage(MESSAGE_TYPE.GET, {x, y, z}, nil) + local remoteGrid = sendAndWaitForResponse(tMap.serverID, message, tMap.protocol) or {} + tMap.remoteGridsAge[x..","..y..","..z] = os.clock() + if not tMap.remoteGrids[x] then + tMap.remoteGrids[x] = {} + end + if not tMap.remoteGrids[x][y] then + tMap.remoteGrids[x][y] = {} + end + tMap.remoteGrids[x][y][z] = remoteGrid + return remoteGrid + else + return tMap.remoteGrids[x][y][z] + end +end + +local function getUpdateGrid(tMap, x, y, z) + if not tMap.updateGrids[x] or not tMap.updateGrids[x][y] or not tMap.updateGrids[x][y][z] then + local updateGrid = {} + if not tMap.updateGrids[x] then + tMap.updateGrids[x] = {} + end + if not tMap.updateGrids[x][y] then + tMap.updateGrids[x][y] = {} + end + tMap.updateGrids[x][y][z] = updateGrid + return updateGrid + else + return tMap.updateGrids[x][y][z] + end +end + +local remoteMapMethods = { + get = function(self, coord) + local gX, gY, gZ, pX, pY, pZ = toGridCode(coord) + local grid = getRemoteGrid(self, gX, gY, gZ) + if grid[pX] and grid[pX][pY] then + return grid[pX][pY][pZ] + end + end, + + set = function(self, coord, value) + if not isValidValue(value) then + --should we throw an error or use a default value? + error("remoteMap set: value is not valid", 2) + end + local gX, gY, gZ, pX, pY, pZ = toGridCode(coord) + local grid = getUpdateGrid(self, gX, gY, gZ) + if not grid[pX] then + grid[pX] = {} + end + if not grid[pX][pY] then + grid[pX][pY] = {} + end + grid[pX][pY][pZ] = value + end, + + check = function(self) + local time = os.clock() + local newRemoteGridsAge = {} + for gridCode, gridAge in pairs(self.remoteGridsAge) do + if time - gridAge >= self.timeout then + local x, y, z = string.match(gridCode, "([-]?%d+),([-]?%d+),([-]?%d+)") + x, y, z = tonumber(x), tonumber(y), tonumber(z) + if x and y and z then + if self.remoteGrids[x] and self.remoteGrids[x][y] then + self.remoteGrids[x][y][z] = nil + end + end + else + newRemoteGridsAge[gridCode] = gridAge + end + end + local newUpdateGridsAge = {} + for gridCode, gridAge in pairs(self.updateGridsAge) do + if time - gridAge >= self.timeout then + -- remove grid from updateGrids ??? + else + newUpdateGridsAge[gridCode] = gridAge + end + end + self.remoteGridsAge = newRemoteGridsAge + self.updateGridsAge = newUpdateGridsAge + end, + + pushUpdates = function(self, ignoreTimeout) + local newUpdateGrids = {} + for gX, YZmap in pairs(self.updateGrids) do + newUpdateGrids[gX] = {} + for gY, Zmap in pairs(YZmap) do + newUpdateGrids[gX][gY] = {} + for gZ, grid in pairs(Zmap) do + local gridCode = gX..","..gY..","..gZ + if next(grid) then + if ignoreTimeout == true or (not self.updateGridsAge[gridCode]) or os.clock() - self.updateGridsAge[gridCode] >= self.timeout then + local message = newMessage(MESSAGE_TYPE.SET, {gX, gY, gZ}, grid) + local response = sendAndWaitForResponse(self.serverID, message, self.protocol) + if response == true then + self.updateGridsAge[gridCode] = os.clock() + else + newUpdateGrids[gX][gY][gZ] = grid + end + else + newUpdateGrids[gX][gY][gZ] = grid + end + end + end + end + end + self.updateGrids = newUpdateGrids + end, +} +local remoteMapMetatable = {__index = remoteMapMethods} + +function new(mapName, timeout) + if type(mapName) ~= "string" then + error("mapName must be string") + end + if type(timeout) ~= "number" or timeout < 0 then + error("timeout must be positive number") + end + + local protocol = "map_share:"..mapName + + local serverID = rednet.lookup(protocol, "SERVER") + if not serverID then + error("could not find map share server") + end + + local remoteMap = { + mapName = mapName, + protocol = protocol, + serverID = serverID, + timeout = timeout, + remoteGrids = {}, + remoteGridsAge = {}, + updateGrids = {}, + updateGridsAge = {}, + } + return setmetatable(remoteMap, remoteMapMetatable) +end \ No newline at end of file diff --git a/maps/remoteMap_server b/maps/remoteMap_server new file mode 100644 index 0000000..b7494d0 --- /dev/null +++ b/maps/remoteMap_server @@ -0,0 +1,163 @@ +local args = {...} + +--===== OPEN REDNET =====-- +for _, side in ipairs(redstone.getSides()) do + if peripheral.getType(side) == "modem" then + rednet.open(side) + end +end + +if not rednet.isOpen() then + printError("could not open rednet") + return +end + +--===== LOAD MAP =====-- +if not compactMap then + if not os.loadAPI("compactMap") then + error("could not load API: compactMap") + end +end +local map = compactMap.new(args[1]) + +--===== SET REDNET PROTOCOL =====-- +local MAP_SHARE_PROTOCOL +if args[2] and type(args[2]) == "string" then + MAP_SHARE_PROTOCOL = "map_share:"..args[2] +else + MAP_SHARE_PROTOCOL = "map_share:"..fs.getName(args[1]) +end + +--===== HOST AS SERVER =====-- +do + local host = rednet.lookup(MAP_SHARE_PROTOCOL, "SERVER") + if host and host ~= os.computerID() then + printError("server already running for this map share") + return + end +end +rednet.host(MAP_SHARE_PROTOCOL, "SERVER") + +--===== UTILS =====-- +local MESSAGE_TYPE = { + GET = 0, + SET = 1, +} +local receivedMessages = {} +local receivedMessageTimeouts = {} + +local function isValidCoord(coord) + return type(coord) == "number" and coord % 1 == 0 and coord >= 0 and coord <= 15 +end + +local function updateCoord(x, y, z, value) + local coord = vector.new(x, y, z) + local currValue = map:get(coord) + if value == 1 then + if currValue then + map:set(coord, math.min(7, currValue + 1)) + else + map:set(coord, 0) + end + elseif value == -1 then + if currValue then + if currValue == 0 then + map:set(coord, nil) + else + map:set(coord, currValue - 1) + end + end + end +end + +local function updateMap(newData, gX, gY, gZ) + if type(newData) == "table" then + local currX, currY + for x, gridYZ in pairs(newData) do + if isValidCoord(x) and type(gridYZ) == "table" then + currX = gX*16 + x + for y, gridZ in pairs(gridYZ) do + if isValidCoord(y) and type(gridZ) == "table" then + currY = gY*16 + y + for z, value in pairs(gridZ) do + if isValidCoord(z) then + updateCoord(currX, currY, gZ*16 + z, value) + end + end + end + end + end + end + map:save(gX, gY, gZ) + end +end + +local function checkGridCoordFormat(gridCoord) + if type(gridCoord) == "table" and #gridCoord == 3 then + for i = 1, 3 do + local coord = gridCoord[i] + if type(coord) ~= "number" or coord % 1 ~= 0 then + return false + end + end + return true + end + return false +end + +local function newMessage(messageType, messageID, grid, data) + return { + type = messageType, + ID = messageID, + grid = grid, + data = data, + } +end + +--===== REPEATED MESSAGE HANDLING =====-- +local function clearOldMessages() + while true do + local event, timer = os.pullEvent("timer") + local messageID = receivedMessageTimeouts[timer] + if messageID then + receivedMessageTimeouts[timer] = nil + receivedMessages[messageID] = nil + end + end +end + +--===== MAIN =====-- +local function main() + while true do + local senderID, message = rednet.receive(MAP_SHARE_PROTOCOL) + if type(message) == "table" and checkGridCoordFormat(message.grid) then + if message.type == MESSAGE_TYPE.GET then + local gridData = map:getGrid(unpack(message.grid)) + local replyMessage = newMessage(MESSAGE_TYPE.GET, message.ID, message.grid, gridData) + rednet.send(senderID, replyMessage, MAP_SHARE_PROTOCOL) + elseif message.type == MESSAGE_TYPE.SET then + if not receivedMessages[message.ID] then + updateMap(message.data, unpack(message.grid)) + receivedMessages[message.ID] = true + receivedMessageTimeouts[os.startTimer(15)] = message.ID + end + local replyMessage = newMessage(MESSAGE_TYPE.SET, message.ID, message.grid, true) + rednet.send(senderID, replyMessage, MAP_SHARE_PROTOCOL) + end + end + end +end + +--===== USER INTERFACE =====-- +local function control() + while true do + local event, key = os.pullEvent("key") + if key == keys.backspace then + break + end + end +end + +parallel.waitForAny(main, clearOldMessages, control) + +rednet.unhost(MAP_SHARE_PROTOCOL) \ No newline at end of file diff --git a/maps/remoteMap_viewer b/maps/remoteMap_viewer new file mode 100644 index 0000000..5875b50 --- /dev/null +++ b/maps/remoteMap_viewer @@ -0,0 +1,117 @@ +local tArgs = {...} + +local function printUsage() + print("Usage: viewRemoteMap <(string) mapName> <(int) x-coord> <(int) y-coord> <(int) z-coord>") + print("mapName must be the complete file path") +end + +if not remoteMap then + if not os.loadAPI("remoteMap") then + error("Could not load remoteMap API") + end +end + +for _, side in ipairs(redstone.getSides()) do + if peripheral.getType(side) == "modem" then + rednet.open(side) + end +end + +if type(tArgs[1]) ~= "string" then + error("string expected for map name") +end +local map = remoteMap.new(tArgs[1], 5) + +local startX, startY, startZ +if #tArgs == 4 then + for i = 2, 4 do + local num = tArgs[i] + if not tonumber(num) or num % 1 ~= 0 then + printUsage() + return + end + end + startX = tArgs[2] + startY = tArgs[3] + startZ = tArgs[4] +end + +term.setCursorBlink(false) +term.setBackgroundColour(colours.black) + +local cont = true +local width, height = term.getSize() +local currW, currH = 1, 1 +term.setCursorBlink(true) +term.setTextColour(colours.red) +local redraw = true +while cont do + if redraw then + map:check() + term.setBackgroundColour(colours.black) + term.clear() + term.setCursorPos(1, height) + term.clearLine() + term.write(tostring(startX + currW - 1)..","..tostring(startY)..","..tostring(startZ + currH - 1)) + term.write(" -- ") + term.write(tostring(math.floor( (startX + currW - 1)/16 ))) + term.write(",") + term.write(tostring(math.floor( (startY)/16 ))) + term.write(",") + term.write(tostring(math.floor( (startZ + currH - 1)/16 ))) + for x = 1, width do + for z = 1, height - 1 do + local value = map:get(vector.new(startX + x - 1, startY, startZ + z - 1)) + if value then + term.setBackgroundColour(colours.white) + term.setCursorPos(x, z) + term.write(string.sub(value, 1, 1)) + end + end + end + term.setCursorPos(currW, currH) + redraw = false + end + local event = {os.pullEvent()} + if event[1] == "key" then + local key = event[2] + if key == keys.up then + startZ = startZ - 1 + elseif key == keys.down then + startZ = startZ + 1 + elseif key == keys.left then + startX = startX - 1 + elseif key == keys.right then + startX = startX + 1 + elseif key == keys.numPadAdd then + startY = startY + 1 + elseif key == keys.numPadSubtract then + startY = startY - 1 + elseif key == keys.backspace then + cont = false + end + redraw = true + elseif event[1] == "mouse_click" then + if event[4] < height then + currW, currH = event[3], event[4] + term.setBackgroundColour(colours.black) + term.setCursorPos(1, height) + term.clearLine() + term.write(tostring(startX + currW - 1)..","..tostring(startY)..","..tostring(startZ + currH - 1)) + term.write(" -- ") + term.write(tostring(math.floor( (startX + currW - 1)/16 ))) + term.write(",") + term.write(tostring(math.floor( (startY)/16 ))) + term.write(",") + term.write(tostring(math.floor( (startZ + currH - 1)/16 ))) + term.setCursorPos(currW, currH) + end + elseif event[1] == "term_resize" then + width, height = term.getSize() + redraw = true + end +end + +term.setBackgroundColour(colours.black) +term.setCursorPos(1, 1) +term.clear() \ No newline at end of file diff --git a/maps/tinyMap b/maps/tinyMap new file mode 100644 index 0000000..167d243 --- /dev/null +++ b/maps/tinyMap @@ -0,0 +1,222 @@ +local BIT_MASKS = { + SET_COORD = bit.blshift(1, 5), + SET_X_COORD = bit.blshift(1, 4), + COORD = 15, + COORD_DATA = bit.blshift(1, 4), +} + +local function base256_to_base64(input) + local output = {} + for i = 1, #input, 3 do + table.insert(output, bit.brshift(input[i] or 0, 2)) + table.insert(output, bit.blshift(bit.band(input[i] or 0, 3), 4) + bit.brshift(input[i+1] or 0, 4)) + table.insert(output, bit.blshift(bit.band(input[i+1] or 0, 15), 2) + bit.brshift(input[i+2] or 0, 6)) + table.insert(output, bit.band(input[i+2] or 0, 63)) + end + return output +end + +local function base64_to_base256(input) + local output = {} + for i = 1, #input, 4 do + table.insert(output, bit.blshift(input[i] or 0, 2) + bit.brshift(input[i+1] or 0, 4)) + table.insert(output, bit.blshift(bit.band(input[i+1] or 0, 15), 4) + bit.brshift(input[i+2] or 0, 2)) + table.insert(output, bit.blshift(bit.band(input[i+2] or 0, 3), 6) + (input[i+3] or 0)) + end + return output +end + +local function isValidValue(value) + return value == nil or value == 0 or value == 1 +end + +local function toGridCode(tVector) + return math.floor(tVector.x/16), math.floor(tVector.y/16), math.floor(tVector.z/16), tVector.x % 16, tVector.y % 16, tVector.z % 16 +end + +local function setGrid(tMap, x, y, z, grid) + if not tMap.map[x] then + tMap.map[x] = {} + end + if not tMap.map[x][y] then + tMap.map[x][y] = {} + end + tMap.map[x][y][z] = grid + return tMap.map[x][y][z] +end + +local function getGrid(tMap, x, y, z) + if not tMap.map[x] or not tMap.map[x][y] or not tMap.map[x][y][z] then + return tMap:load(x, y, z) + else + return tMap.map[x][y][z] + end +end + +local mapMethods = { + + load = function(self, tVector, y, z) + local gX, gY, gZ + if y and z then + gX, gY, gZ = tVector, y, z + else + gX, gY, gZ = toGridCode(tVector) + end + local gridPath = fs.combine(self.mapDir, gX..","..gY..","..gZ) + if fs.exists(gridPath) then + local handle = fs.open(gridPath, "rb") + if handle then + local grid = {} + + local rawData = {} + local rawDataByte = handle.read() + while rawDataByte do + table.insert(rawData, rawDataByte) + rawDataByte = handle.read() + end + handle.close() + + --local data = rawData + local data = base256_to_base64(rawData) + + --load grid data + local currX, currY, currZ + local dataByte + for _, dataByte in ipairs(data) do + if bit.band(dataByte, BIT_MASKS.SET_COORD) == BIT_MASKS.SET_COORD then + --we are changing our currX or currY coord + if bit.band(dataByte, BIT_MASKS.SET_X_COORD) == BIT_MASKS.SET_X_COORD then + --we are changing our currX coord + currX = bit.band(dataByte, BIT_MASKS.COORD) + else + --we are changing our currY coord + currY = bit.band(dataByte, BIT_MASKS.COORD) + end + else + --we are setting the value for a proper coord + currZ = bit.band(dataByte, BIT_MASKS.COORD) + if currX and currY and currZ then + if not grid[currX] then + grid[currX] = {} + end + if not grid[currX][currY] then + grid[currX][currY] = {} + end + grid[currX][currY][currZ] = bit.brshift(bit.band(dataByte, BIT_MASKS.COORD_DATA), 4) + end + end + end + return setGrid(self, gX, gY, gZ, grid) + end + end + return setGrid(self, gX, gY, gZ, {}) + end, + + loadAll = function(self) + if fs.exists(self.mapDir) and fs.isDir(self.mapDir) then + for _, gridFile in ipairs(fs.list(self.mapDir)) do + local _, _, gX, gY, gZ = string.find(gridFile, "(.+)%,(.+)%,(.+)") + if gX and gY and gX then + self:load(tonumber(gX), tonumber(gY), tonumber(gZ)) + end + end + end + end, + + save = function(self) + for gX, YZmap in pairs(self.map) do + for gY, Zmap in pairs(YZmap) do + for gZ, grid in pairs(Zmap) do + if next(grid) then + local rawData = {} + for x, gridYZ in pairs(grid) do + table.insert(rawData, BIT_MASKS.SET_COORD + BIT_MASKS.SET_X_COORD + x) + for y, gridZ in pairs(gridYZ) do + table.insert(rawData, BIT_MASKS.SET_COORD + y) + for z, coordValue in pairs(gridZ) do + table.insert(rawData, bit.blshift(coordValue, 4) + z) + end + end + end + --local data = rawData + local data = base64_to_base256(rawData) + local handle = fs.open(fs.combine(self.mapDir, gX..","..gY..","..gZ), "wb") + if handle then + for _, dataByte in ipairs(data) do + handle.write(dataByte) + end + handle.close() + end + else + fs.delete(fs.combine(self.mapDir, gX..","..gY..","..gZ)) + end + end + end + end + end, + + get = function(self, tVector) + local gX, gY, gZ, pX, pY, pZ = toGridCode(tVector) + local grid = getGrid(self, gX, gY, gZ) + if grid[pX] and grid[pX][pY] then + return grid[pX][pY][pZ] + end + end, + + set = function(self, tVector, value) + if not isValidValue(value) then + --should we throw an error or use a default value? + error("set: value is not valid", 2) + end + local gX, gY, gZ, pX, pY, pZ = toGridCode(tVector) + local grid = getGrid(self, gX, gY, gZ) + if not grid[pX] then + grid[pX] = {} + end + if not grid[pX][pY] then + grid[pX][pY] = {} + end + grid[pX][pY][pZ] = value + return grid[pX][pY][pZ] + end, + + getOrSet = function(self, tVector, value) + local gX, gY, gZ, pX, pY, pZ = toGridCode(tVector) + local grid = getGrid(self, gX, gY, gZ) + if grid[pX] and grid[pX][pY] and grid[pX][pY][pZ] then + return grid[pX][pY][pZ], false + else + if not isValidValue(value) then + --should we throw an error or use a default value? + error("getOrSet: value is not valid", 2) + end + if not grid[pX] then + grid[pX] = {} + end + if not grid[pX][pY] then + grid[pX][pY] = {} + end + grid[pX][pY][pZ] = value + return grid[pX][pY][pZ], true + end + end, + +} +local mapMetatable = {__index = mapMethods} + +function new(mapDir) + local tMap = {} + if type(mapDir) == "string" then + if not fs.exists(mapDir) then + fs.makeDir(mapDir) + elseif not fs.isDir(mapDir) then + error("new: not a valid directory", 2) + end + tMap.mapDir = mapDir + else + error("new: directory must be string", 2) + end + tMap.map = {} + setmetatable(tMap, mapMetatable) + return tMap +end \ No newline at end of file diff --git a/maps/viewMap b/maps/viewMap new file mode 100644 index 0000000..44886c9 --- /dev/null +++ b/maps/viewMap @@ -0,0 +1,128 @@ +local tArgs = {...} + +local function printUsage() + print("Usages:") + print("viewMap <(string) mapAPIPath> <(string) mapName>") + print("viewMap <(string) mapAPIPath> <(string) mapName> <(int) x-coord> <(int) y-coord> <(int) z-coord>") + print("mapName must be the complete file path") +end + +local mapAPIPath = tArgs[1] +if not mapAPIPath or type(mapAPIPath) ~= "string" or not fs.exists(mapAPIPath) or fs.isDir(mapAPIPath) then + error("invalid mapAPIPath: "..tostring(mapAPIPath)) +end +local mapAPI = fs.getName(mapAPIPath) +if not _G[mapAPI] then + if not os.loadAPI(mapAPIPath) then + error("could not load mapAPI: "..tostring(mapAPIPath)) + end +end +mapAPI = _G[mapAPI] + +local map +local mapName = tArgs[2] +if mapName and type(mapName) == "string" and fs.exists(mapName) and fs.isDir(mapName) then + map = mapAPI.new(mapName) + if not map then + error("could not load map at "..mapName) + end +else + printUsage() +end + +local startX, startY, startZ +if #tArgs == 5 then + for i = 3, 5 do + local num = tArgs[i] + if not tonumber(num) or num % 1 ~= 0 then + printUsage() + return + end + end + startX = tArgs[3] + startY = tArgs[4] + startZ = tArgs[5] +end + +if not startX then + map:loadAll() + --find any point on map that isn't empty +end + +term.setCursorBlink(false) +term.setBackgroundColour(colours.black) + +local cont = true +local width, height = term.getSize() +local currW, currH = 1, 1 +term.setCursorBlink(true) +term.setTextColour(colours.red) +local redraw = true +while cont do + if redraw then + term.setBackgroundColour(colours.black) + term.clear() + term.setCursorPos(1, height) + term.clearLine() + term.write(tostring(startX + currW - 1)..","..tostring(startY)..","..tostring(startZ + currH - 1)) + term.write(" -- ") + term.write(tostring(math.floor( (startX + currW - 1)/16 ))) + term.write(",") + term.write(tostring(math.floor( (startY)/16 ))) + term.write(",") + term.write(tostring(math.floor( (startZ + currH - 1)/16 ))) + for x = 1, width do + for z = 1, height - 1 do + local value = map:get(vector.new(startX + x - 1, startY, startZ + z - 1)) + if value then + term.setBackgroundColour(colours.white) + term.setCursorPos(x, z) + term.write(string.sub(value, 1, 1)) + end + end + end + term.setCursorPos(currW, currH) + redraw = false + end + local event = {os.pullEvent()} + if event[1] == "key" then + local key = event[2] + if key == keys.up then + startZ = startZ - 1 + elseif key == keys.down then + startZ = startZ + 1 + elseif key == keys.left then + startX = startX - 1 + elseif key == keys.right then + startX = startX + 1 + elseif key == keys.numPadAdd then + startY = startY + 1 + elseif key == keys.numPadSubtract then + startY = startY - 1 + elseif key == keys.backspace then + cont = false + end + redraw = true + elseif event[1] == "mouse_click" then + if event[4] < height then + currW, currH = event[3], event[4] + term.setCursorPos(1, height) + term.clearLine() + term.write(tostring(startX + currW - 1)..","..tostring(startY)..","..tostring(startZ + currH - 1)) + term.write(" -- ") + term.write(tostring(math.floor( (startX + currW - 1)/16 ))) + term.write(",") + term.write(tostring(math.floor( (startY)/16 ))) + term.write(",") + term.write(tostring(math.floor( (startZ + currH - 1)/16 ))) + term.setCursorPos(currW, currH) + end + elseif event[1] == "term_resize" then + width, height = term.getSize() + redraw = true + end +end + +term.setBackgroundColour(colours.black) +term.setCursorPos(1, 1) +term.clear() \ No newline at end of file diff --git a/netNav b/netNav new file mode 100644 index 0000000..2971576 --- /dev/null +++ b/netNav @@ -0,0 +1,296 @@ +if not remoteMap then + if not os.loadAPI("remoteMap") then + error("could not load remoteMap API") + end +end +if not aStar then + if not os.loadAPI("aStar") then + error("could not load aStar API") + end +end +if not location then + if not os.loadAPI("location") then + error("could not load location API") + end +end + +local position + +local SESSION_COORD_CLEAR = 1 +local SESSION_COORD_BLOCKED = 2 + +local UPDATE_COORD_CLEAR = -1 +local UPDATE_COORD_BLOCKED = 1 + +local sessionMap +local serverMap + +local function distanceFunc(a, b) + local sessionMapA, sessionMapB = sessionMap:get(a), sessionMap:get(b) + if sessionMapA == SESSION_COORD_BLOCKED or sessionMapB == SESSION_COORD_BLOCKED then + return math.huge -- we have found one of these coords to be blocked during this session + elseif sessionMapA == SESSION_COORD_CLEAR and sessionMapB == SESSION_COORD_CLEAR then + return aStar.distance(a, b) -- we have found both of these coords to be clear during this session + else + local serverMapA, serverMapB = serverMap:get(a), serverMap:get(b) + if serverMapA or serverMapB then + serverMapA = serverMapA and 2^(serverMapA + 1) or 1 + serverMapB = serverMapB and 2^(serverMapB + 1) or 1 + return math.max(serverMapA, serverMapB) -- the remote server map is indicating one of these coords may be blocked + end + end + return aStar.distance(a, b) -- we dont know anything useful so just calc the distance +end + +local directions = { + [vector.new(0, 0, 1)] = 0, + [vector.new(-1, 0, 0)] = 1, + [vector.new(0, 0, -1)] = 2, + [vector.new(1, 0, 0)] = 3, + [vector.new(0, 1, 0)] = 4, + [vector.new(0, -1, 0)] = 5, +} + +local function deltaToDirection(delta) + for vec, dir in pairs(directions) do + if aStar.vectorEquals(delta, vec) then + return dir + end + end +end + +local function tryMove() + for i = 1, 4 do + if turtle.forward() then + return true + end + turtle.turnRight() + end + return false +end + +local function findPosition() + local move = turtle.up + while not tryMove() do + if not move() then + if move == turtle.up then + move = turtle.down + move() + else + error("trapped in a ridiculous place") + end + end + end + + local p1 = {gps.locate()} + if #p1 == 3 then + p1 = vector.new(unpack(p1)) + else + error("no gps signal - phase 1") + end + + if not turtle.back() then + error("couldn't move to determine direction") + end + + local p2 = {gps.locate()} + if #p2 == 3 then + p2 = vector.new(unpack(p2)) + else + error("no gps signal - phase 2") + end + + local direction = deltaToDirection(p1 - p2) + if direction and direction < 4 then + return location.new(p2.x, p2.y, p2.z, direction) + else + return false + end +end + +local function detect(currPos, adjPos) + local direction = deltaToDirection(adjPos - currPos) + if direction then + position:setHeading(direction) + if direction == 4 then + return turtle.detectUp() + elseif direction == 5 then + return turtle.detectDown() + else + return turtle.detect() + end + end + return false +end + +local function inspect(currPos, adjPos) + local direction = deltaToDirection(adjPos - currPos) + if direction then + position:setHeading(direction) + if direction == 4 then + return turtle.inspectUp() + elseif direction == 5 then + return turtle.inspectDown() + else + return turtle.inspect() + end + end + return false +end + +local function updateCoord(coord, isBlocked) + if isBlocked then + sessionMap:set(coord, SESSION_COORD_BLOCKED) + serverMap:set(coord, UPDATE_COORD_BLOCKED) + else + sessionMap:set(coord, SESSION_COORD_CLEAR) + serverMap:set(coord, UPDATE_COORD_CLEAR) + end +end + +local function detectAll(currPos) + for _, pos in ipairs(aStar.adjacent(currPos)) do -- better order of checking directions + updateCoord(pos, detect(currPos, pos)) + end +end + +local function findSensor() + for _, side in ipairs(peripheral.getNames()) do + if peripheral.getType(side) == "turtlesensorenvironment" then + return side + end + end + return false +end + +local function scan(currPos) + local sensorSide = findSensor() + if sensorSide then + local rawBlockInfo = peripheral.call(sensorSide, "sonicScan") + local sortedBlockInfo = aStar.newMap() + for _, blockInfo in ipairs(rawBlockInfo) do + sortedBlockInfo:set(currPos + vector.new(blockInfo.x, blockInfo.y, blockInfo.z), blockInfo) + end + local toCheckQueue = {} + for _, pos in ipairs(aStar.adjacent(currPos)) do + if sortedBlockInfo:get(pos) then + table.insert(toCheckQueue, pos) + end + end + while toCheckQueue[1] do + local pos = table.remove(toCheckQueue, 1) + local blockInfo = sortedBlockInfo:get(pos) + if blockInfo.type == "AIR" then + for _, pos2 in ipairs(aStar.adjacent(pos)) do + local blockInfo2 = sortedBlockInfo:get(pos2) + if blockInfo2 and not blockInfo2.checked then + table.insert(toCheckQueue, pos2) + end + end + updateCoord(pos, false) + else + updateCoord(pos, true) + end + blockInfo.checked = true + end + else + detectAll(currPos) + end +end + +local function move(currPos, adjPos) + local direction = deltaToDirection(adjPos - currPos) + if direction then + position:setHeading(direction) + if direction == 4 then + return position:up() + elseif direction == 5 then + return position:down() + else + return position:forward() + end + end + return false +end + +function goto(x, y, z) + if not serverMap then + error("serverMap has not been specified") + end + if turtle.getFuelLevel() == 0 then + return false, "ran out of fuel" + end + if not position then + position = findPosition() + if not position then + return false, "couldn't determine location" + end + end + + local goal = vector.new(tonumber(x), tonumber(y), tonumber(z)) + + serverMap:check() -- remove timed out data we have received from server + sessionMap = aStar.newMap() -- reset the sessionMap + + local path = aStar.compute(distanceFunc, position, goal) + if not path then + return false, "no known path to goal" + end + + while not aStar.vectorEquals(position, goal) do + local movePos = table.remove(path) + while not move(position, movePos) do + local blockPresent, blockData = inspect(position, movePos) + local recalculate, isTurtle = false, false + if blockPresent and (blockData.name == "ComputerCraft:CC-TurtleAdvanced" or blockData.name == "ComputerCraft:CC-Turtle") then -- there is a turtle in the way + sleep(math.random(0, 3)) + local blockPresent2, blockData2 = inspect(position, movePos) + if blockPresent2 and (blockData2.name == "ComputerCraft:CC-TurtleAdvanced" or blockData2.name == "ComputerCraft:CC-Turtle") then -- the turtle is still there + recalculate, isTurtle = true, true + end + elseif blockPresent then + recalculate = true + elseif turtle.getFuelLevel() == 0 then + return false, "ran out of fuel" + else + sleep(1) + end + if recalculate then + scan(position) + serverMap:check() + serverMap:pushUpdates() + if sessionMap:get(goal) == SESSION_COORD_BLOCKED then return false, "goal is blocked" end + path = aStar.compute(distanceFunc, position, goal) + if not path then + return false, "no known path to goal" + end + if isTurtle then + sessionMap:set(movePos, nil) + end + movePos = table.remove(path) + end + end + if serverMap:get(movePos) then + serverMap:set(movePos, UPDATE_COORD_CLEAR) + end + end + + serverMap:check() + serverMap:pushUpdates(true) + + return true +end + +function setMap(mapName, mapTimeout) + if type(mapName) ~= "string" then + error("mapName must be string") + if type(mapTimeout) ~= "number" or mapTimeout < 0 then + end + error("timeout must be positive number") + end + serverMap = remoteMap.new(mapName, mapTimeout) +end + +function getMap() + return serverMap +end \ No newline at end of file diff --git a/netNav_goto b/netNav_goto new file mode 100644 index 0000000..2963a2c --- /dev/null +++ b/netNav_goto @@ -0,0 +1,27 @@ +if not netNav then + if not os.loadAPI("netNav") then + error("could not load netNav API") + end +end + +for _, side in ipairs(redstone.getSides()) do + if peripheral.getType(side) == "modem" then + rednet.open(side) + end +end + +netNav.setMap("main", 30) + +local tArgs = {...} +if #tArgs == 3 then + for i = 1, #tArgs do + if tonumber(tArgs[i]) then + tArgs[i] = tonumber(tArgs[i]) + else + error("argument "..i.." must be a valid coordinate") + end + end + print(netNav.goto(tArgs[1], tArgs[2], tArgs[3])) +else + error("arguments must be 3 or 4 numbers") +end \ No newline at end of file diff --git a/pQueue b/pQueue new file mode 100644 index 0000000..4c5776f --- /dev/null +++ b/pQueue @@ -0,0 +1,108 @@ +local function sift_up(queue, index) + local current, parent = index, (index - (index % 2))/2 + while current > 1 and queue.cmp(queue[current][2], queue[parent][2]) do + queue[current], queue[parent] = queue[parent], queue[current] + current, parent = parent, (parent - (parent % 2))/2 + end + return current +end + +local function sift_down(queue, index) + local current, child, size = index, 2*index, #queue + while child <= size do + if child < size and queue.cmp(queue[child + 1][2], queue[child][2]) then + child = child + 1 + end + if queue.cmp(queue[child][2], queue[current][2]) then + queue[current], queue[child] = queue[child], queue[current] + current, child = child, 2*child + else + break + end + end + return current +end + +local methods = { + + insert = function(self, element, value) + table.insert(self, {element, value}) + return sift_up(self, #self) + end, + + remove = function(self, element, compFunc) + local index = self:contains(element, compFunc) + if index then + local size = #self + self[index], self[size] = self[size], self[index] + local ret = table.remove(self) + if size > 1 and index < size then + sift_down(self, index) + if index > 1 then + sift_up(self, index) + end + end + return unpack(ret) + end + end, + + pop = function(self) + if self[1] then + local size = #self + self[1], self[size] = self[size], self[1] + local ret = table.remove(self) + if size > 1 then + sift_down(self, 1) + end + return unpack(ret) + end + end, + + peek = function(self) + if self[1] then + return self[1][1], self[1][2] + end + end, + + contains = function(self, element, compFunc) + for index, entry in ipairs(self) do + if (compFunc and compFunc(entry[1], element)) or entry[1] == element then + return index + end + end + return false + end, + + isEmpty = function(self) + return #self == 0 + end, + + size = function(self) + return #self + end, + + getValue = function(self, element, compFunc) + local index = self:contains(element, compFunc) + return (index and self[index][2]) or false + end, + + setValue = function(self, element, value, compFunc) + local index = self:contains(element, compFunc) + if index then + self[index][2] = value + sift_up(self, index) + sift_down(self, index) + return true + else + return false + end + end, + +} + +function new(compareFunc) + local queue = {} + queue.cmp = type(compareFunc) == "function" and compareFunc or function(a, b) return a < b end + setmetatable(queue, {__index = methods}) + return queue +end \ No newline at end of file diff --git a/starNav b/starNav new file mode 100644 index 0000000..2ea4cab --- /dev/null +++ b/starNav @@ -0,0 +1,291 @@ +if not tinyMap then + if not os.loadAPI("tinyMap") then + error("could not load tinyMap API") + end +end +if not aStar then + if not os.loadAPI("aStar") then + error("could not load aStar API") + end +end +if not location then + if not os.loadAPI("location") then + error("could not load location API") + end +end + +local position + +local SESSION_MAX_DISTANCE_DEFAULT = 32 + +local SESSION_COORD_CLEAR = 1 +local SESSION_COORD_BLOCKED = 2 +local SESSION_COORD_DISTANCE = math.huge +local MAIN_COORD_CLEAR = nil +local MAIN_COORD_BLOCKED = 1 +local MAIN_COORD_DISTANCE = 1024 + +local sessionMidPoint +local sessionMaxDistance +local sessionMap +local mainMap = tinyMap.new("starNavMaps") + +local function sup_norm(a, b) + return math.max(math.abs(a.x - b.x), math.abs(a.y - b.y), math.abs(a.z - b.z)) +end + +local function distanceFunc(a, b) + local sessionMapA, sessionMapB = sessionMap:get(a), sessionMap:get(b) + if sup_norm(a, sessionMidPoint) > sessionMaxDistance then + return SESSION_COORD_DISTANCE -- first coord is outside the search region + elseif sup_norm(b, sessionMidPoint) > sessionMaxDistance then + return SESSION_COORD_DISTANCE -- second coord is outside the search region + elseif sessionMapA == SESSION_COORD_BLOCKED or sessionMapB == SESSION_COORD_BLOCKED then + return SESSION_COORD_DISTANCE -- one of the coords has been found to be blocked this during this session + elseif sessionMapA == SESSION_COORD_CLEAR and sessionMapA == SESSION_COORD_CLEAR then + return aStar.distance(a, b) -- both coords has been found to be clear this during this session + elseif mainMap:get(a) or mainMap:get(b) then + return MAIN_COORD_DISTANCE + end + return aStar.distance(a, b) -- we are assuming both coords are clear +end + +local function getHeading() + local i = 0 + while turtle.detect() do + if i == 4 then + if turtle.up() then + i = 0 + else + error("help I'm trapped in a ridiculous place") + end + else + turtle.turnRight() + i = i + 1 + end + end + local p1 = {gps.locate()} + if #p1 == 3 then + p1 = vector.new(unpack(p1)) + else + error("no gps signal - phase 1") + end + i = 0 + while not turtle.forward() do + if i > 5 then error("couldn't move to determine direction") end + i = i + 1 + sleep(1) + end + local p2 = {gps.locate()} + turtle.back() + if #p2 == 3 then + p2 = vector.new(unpack(p2)) + else + error("no gps signal - phase 2") + end + local dir = p2 - p1 + if dir.x == 1 then + return 3 + elseif dir.x == -1 then + return 1 + elseif dir.z == 1 then + return 0 + elseif dir.z == -1 then + return 2 + else + error("could not determine direction - phase 3") + end +end + +local function detect(currPos, adjPos) + local dir = adjPos - currPos + if dir.y == 1 then + return turtle.detectUp() + elseif dir.y == -1 then + return turtle.detectDown() + elseif dir.x == 1 then + position:setHeading(3) + return turtle.detect() + elseif dir.x == -1 then + position:setHeading(1) + return turtle.detect() + elseif dir.z == 1 then + position:setHeading(0) + return turtle.detect() + elseif dir.z == -1 then + position:setHeading(2) + return turtle.detect() + else + return false + end +end + +local function inspect(currPos, adjPos) + local dir = adjPos - currPos + if dir.y == 1 then + return turtle.inspectUp() + elseif dir.y == -1 then + return turtle.inspectDown() + elseif dir.x == 1 then + position:setHeading(3) + return turtle.inspect() + elseif dir.x == -1 then + position:setHeading(1) + return turtle.inspect() + elseif dir.z == 1 then + position:setHeading(0) + return turtle.inspect() + elseif dir.z == -1 then + position:setHeading(2) + return turtle.inspect() + else + return false + end +end + +local function updateCoord(coord, isBlocked) + if isBlocked then + sessionMap:set(pos, SESSION_COORD_BLOCKED) + mainMap:set(pos, MAIN_COORD_BLOCKED) + else + sessionMap:set(pos, SESSION_COORD_CLEAR) + mainMap:set(pos, MAIN_COORD_CLEAR) + end +end + +local function detectAll(currPos) + for _, pos in ipairs(aStar.adjacent(currPos)) do -- better order of checking directions + updateCoord(pos, detect(currPos, pos)) + end +end + +local function findSensor() + for _, side in ipairs(peripheral.getNames()) do + if peripheral.getType(side) == "turtlesensorenvironment" then + return side + end + end + return false +end + +local function scan(currPos) + local sensorSide = findSensor() + if sensorSide then + local rawBlockInfo = peripheral.call(sensorSide, "sonicScan") + local sortedBlockInfo = aStar.newMap() + for _, blockInfo in ipairs(rawBlockInfo) do + sortedBlockInfo:set(currPos + vector.new(blockInfo.x, blockInfo.y, blockInfo.z), blockInfo) + end + local toCheckQueue = {} + for _, pos in ipairs(aStar.adjacent(currPos)) do + if sortedBlockInfo:get(pos) then + table.insert(toCheckQueue, pos) + end + end + while toCheckQueue[1] do + local pos = table.remove(toCheckQueue, 1) + local blockInfo = sortedBlockInfo:get(pos) + if blockInfo.type == "AIR" then + for _, pos2 in ipairs(aStar.adjacent(pos)) do + local blockInfo2 = sortedBlockInfo:get(pos2) + if blockInfo2 and not blockInfo2.checked then + table.insert(toCheckQueue, pos2) + end + end + updateCoord(pos, false) + else + updateCoord(pos, true) + end + blockInfo.checked = true + end + else + detectAll(currPos) + end + mainMap:save() +end + +local function move(currPos, adjPos) + if not detect(position, adjPos) then + local dir = adjPos - currPos + if dir.y == 1 then + return position:up() + elseif dir.y == -1 then + return position:down() + else + return position:forward() + end + else + return false + end +end + +function goto(x, y, z, maxDistance) + if turtle.getFuelLevel() == 0 then + return false, "ran out of fuel" + end + if not position then + local heading = getHeading() + local currPos = {gps.locate()} + if #currPos == 3 then + local x, y, z = unpack(currPos) + position = location.new(x, y, z, heading) + else + return false, "couldn't determine location" + end + end + + local goal = vector.new(tonumber(x), tonumber(y), tonumber(z)) + + sessionMap = aStar.newMap() + sessionMidPoint = vector.new(math.floor((goal.x + position.x)/2), math.floor((goal.y + position.y)/2), math.floor((goal.z + position.z)/2)) + sessionMaxDistance = (type(maxDistance) == "number" and maxDistance) or math.max(2*sup_norm(sessionMidPoint, goal), SESSION_MAX_DISTANCE_DEFAULT) + + local path = aStar.compute(distanceFunc, position, goal) + if not path then + return false, "no known path to goal" + end + + while not aStar.vectorEquals(position, goal) do + local movePos = table.remove(path) + while not move(position, movePos) do + local blockPresent, blockData = inspect(position, movePos) + local recalculate, isTurtle = false, false + if blockPresent and (blockData.name == "ComputerCraft:CC-TurtleAdvanced" or blockData.name == "ComputerCraft:CC-Turtle") then -- there is a turtle in the way + sleep(math.random(0, 3)) + local blockPresent2, blockData2 = inspect(position, movePos) + if blockPresent2 and (blockData2.name == "ComputerCraft:CC-TurtleAdvanced" or blockData2.name == "ComputerCraft:CC-Turtle") then -- the turtle is still there + sessionMap:set(movePos, SESSION_COORD_BLOCKED) + recalculate, isTurtle = true, true + end + elseif blockPresent then + scan(position) -- update map info + recalculate = true + elseif turtle.getFuelLevel() == 0 then + return false, "ran out of fuel" + else + sleep(1) + end + if recalculate then + if sessionMap:get(goal) == SESSION_COORD_BLOCKED then return false, "goal is blocked" end + path = aStar.compute(distanceFunc, position, goal) + if not path then + return false, "no known path to goal" + end + movePos = table.remove(path) + if isTurtle then + sessionMap:set(movePos, nil) + end + end + end + if mainMap:get(movePos) then + mainMap:set(movePos, MAIN_COORD_CLEAR) + end + end + return true +end + +function getPosition() + if position then + return position:value() + end +end \ No newline at end of file diff --git a/starNav_goto b/starNav_goto new file mode 100644 index 0000000..a088c6d --- /dev/null +++ b/starNav_goto @@ -0,0 +1,19 @@ +if not starNav then + if not os.loadAPI("starNav") then + error("could not load starNav API") + end +end + +local tArgs = {...} +if #tArgs == 3 or #tArgs == 4 then + for i = 1, #tArgs do + if tonumber(tArgs[i]) then + tArgs[i] = tonumber(tArgs[i]) + else + error("argument "..i.." must be a valid coordinate") + end + end + print(starNav.goto(tArgs[1], tArgs[2], tArgs[3], tArgs[4])) +else + error("arguments must be 3 or 4 numbers") +end \ No newline at end of file