cc-programs/mine.lua

732 lines
16 KiB
Lua

package.path = "/mod/?.lua;" .. package.path
local PriorityQueue = require "PriorityQueue"
function Set(list)
local set = {}
for _, l in ipairs(list) do set[l] = true end
return set
end
local yieldTime = 0
function yield()
if os.clock() - yieldTime > 2 then
sleep(0)
yieldTime = os.clock()
end
end
function arr(n, init)
local a = {}
for _ = 1, n do
table.insert(a, init)
end
return a
end
function arr2(x, y, init)
local a = {}
for _ = 1, y do
table.insert(a, arr(x, init))
end
return a
end
local dirMap = {
["0,-1"] = 0,
["0,1"] = 0.5,
["1,0"] = 0.25,
["-1,0"] = -0.25,
}
local invDirMap = {
[0] = vector.new(0, 0, -1),
[0.5] = vector.new(0, 0, 1),
[0.25] = vector.new(1, 0, 0),
[-0.25] = vector.new(-1, 0, 0),
[-0.5] = vector.new(0, 0, 1),
[-0.75] = vector.new(1, 0, 0),
[0.75] = vector.new(-1, 0, 0),
}
function direction(v)
local d = dirMap[v.x..","..v.z]
assert(d)
return d
end
function findDistances(points)
local dist = arr2(#points, #points, math.huge)
for i = 1, #points do
for j = 1, #points do
if i ~= j then
dist[i][j] = (points[i] - points[j]):length()
end
end
end
for i = 1, #points do
dist[i][i] = 0
end
return dist
end
function groupOres(ores, dist, offset)
if not offset then
offset = 0
end
local groups = {}
for i, o in ipairs(ores) do
table.insert(groups, {
["type"] = o.name,
["blocks"] = {
[i] = vector.new(o.x, o.y, o.z),
},
})
end
local didJoin = true
while didJoin do
didJoin = false
for i, io in pairs(groups) do
for x, a in pairs(io.blocks) do
for j, jo in pairs(groups) do
if i ~= j and jo.type == io.type then
for y, b in pairs(jo.blocks) do
-- yield()
assert(a ~= b)
if dist[offset + x][offset + y] == 1 then
for z, p in pairs(jo.blocks) do
io.blocks[z] = p
end
groups[j] = nil
didJoin = true
break
end
end
end
end
end
end
end
local retGroups = {}
for _, g in pairs(groups) do
local v = vector.new(0, 0, 0)
local retBlocks = {}
for _, p in pairs(g.blocks) do
v = v + p
table.insert(retBlocks, p)
end
g.blocks = retBlocks
g.center = v / #g.blocks
assert(g.center)
table.insert(retGroups, g)
end
return retGroups
end
-- nearest neighbour
function pathThrough(points, dist, s, e)
local unvisited = {}
local ui = nil
for i = 1, #points do
if not e or i ~= e then
table.insert(unvisited, i)
end
if i == s then
ui = #unvisited
end
end
assert(ui)
local visited = {}
local route = {}
while #unvisited ~= 1 do
local u = unvisited[ui]
table.remove(unvisited, ui)
local cDist = math.huge
local closest = nil
for ii = 1, #unvisited do
local i = unvisited[ii]
assert(i ~= u)
if dist[u][i] < cDist then
cDist = dist[u][i]
closest = ii
end
end
assert(closest)
table.insert(route, unvisited[closest])
ui = closest
end
if e then
table.insert(route, e)
end
return route
end
-- A*
function findPath(s, e, neighbors, limit)
local points = {}
function addPoint(p)
local k = p:tostring()
if not points[k] then
points[k] = p
end
return k
end
s = addPoint(s)
e = addPoint(e)
function h(i)
return (points[e] - points[i]):length()
end
local gScoreT = {}
gScoreT[s] = 0
function gScore(i)
if gScoreT[i] then
return gScoreT[i]
else
return math.huge
end
end
local fScoreT = {}
fScoreT[s] = h(s)
function fScore(i)
if fScoreT[i] then
return fScoreT[i]
else
return math.huge
end
end
local openSet = PriorityQueue(function(a, b)
return fScore(a) < fScore(b)
end)
openSet:enqueue(s, s)
local prev = {}
local found = false
local brokeLimit = false
while not openSet:empty() do
local current = openSet:dequeue()
if current == e then
found = true
break
elseif limit and (points[current] - points[s]):length() > limit then
e = current
brokeLimit = true
break
end
local ns = neighbors(points[current])
for i, n in ipairs(ns) do
ns[i] = addPoint(n)
end
for _, n in ipairs(ns) do
tentativeG = gScore(current) + 1
if tentativeG < gScore(n) then
prev[n] = current
gScoreT[n] = tentativeG
fScoreT[n] = tentativeG + h(n)
if not openSet:contains(n) then
openSet:enqueue(n, n)
end
end
end
end
assert(found or brokeLimit)
local path = {points[e]}
local current = e
while prev[current] do
current = prev[current]
table.insert(path, 1, points[current])
end
return path, brokeLimit
end
local ORES = Set{
"minecraft:coal_ore",
"minecraft:deepslate_coal_ore",
"minecraft:iron_ore",
"minecraft:deepslate_iron_ore",
"minecraft:copper_ore",
"minecraft:deepslate_copper_ore",
"minecraft:gold_ore",
"minecraft:deepslate_gold_ore",
"minecraft:redstone_ore",
"minecraft:deepslate_redstone_ore",
"minecraft:emerald_ore",
"minecraft:deepslate_emerald_ore",
"minecraft:lapis_ore",
"minecraft:deepslate_lapis_ore",
"minecraft:diamond_ore",
"minecraft:deepslate_diamond_ore",
"minecraft:glowstone",
"minecraft:nether_quartz_ore",
"minecraft:ancient_debris",
"create:zinc_ore",
"create:deepslate_zinc_ore",
"create_new_age:thorium_ore",
"create_new_age:magnetite_block",
"powah:deepslate_uraninite_ore_poor",
"powah:deepslate_uraninite_ore",
"powah:deepslate_uraninite_ore_dense",
"powah:uraninite_ore_poor",
"powah:uraninite_ore",
"powah:uraninite_ore_dense",
-- "minecraft:diamond_ore",
-- "minecraft:netherrack",
}
local BLACKLIST = Set{
"minecraft:diamond_pickaxe",
"computercraft:wireless_modem_advanced",
"advancedperipherals:geo_scanner",
"enderchests:ender_chest",
"functionalstorage:ender_drawer",
"advancedperipherals:chunk_controller",
}
local FUEL = "minecraft:coal"
local PROTOCOL = "automine"
local PROTOCOL_CTRL = "automineCtrl"
settings.define("mine.name", {
description = "Miner name",
default = "DiggyBoi",
type = "string",
})
settings.define("mine.stripWidth", {
description = "Strip width",
default = 16,
type = "number",
})
function selectItem(item, nbt)
for i = 1, 16 do
info = turtle.getItemDetail(i)
if (not item and not info) or (info and info.name == item and (not nbt or item.nbt == nbt)) then
turtle.select(i)
return i
end
end
end
Miner = {}
function Miner.new()
local self = setmetatable({}, { __index = Miner })
self.name = settings.get("mine.name")
self.stripWidth = settings.get("mine.stripWidth")
self.equipped = {
["left"] = {
["last"] = "NONE",
["lastNbt"] = nil,
["fun"] = turtle.equipLeft,
},
["right"] = {
["last"] = "NONE",
["lastNbt"] = nil,
["fun"] = turtle.equipRight,
},
}
-- Get upgrades to a known state by "equipping" nothing
self:equip(nil, "left")
self:equip(nil, "right")
self:equip("advancedperipherals:geo_scanner", "right")
self.scanner = peripheral.wrap("right")
self:equip("computercraft:wireless_modem_advanced", "right")
rednet.open("right")
self:findDimension()
self.pos = vector.new(0, 0, 0)
self:doGPS(true)
self:equip("advancedperipherals:chunk_controller", "left")
self.i = 1
return self
end
function Miner:equip(item, side, nbt)
local e = self.equipped[side]
if item == e.last and nbt == e.lastNbt then
return
end
assert(selectItem(item, nbt))
e.fun()
e.last = item
e.lastNbt = nbt
end
function Miner:sendMessage(type, msg)
self:equip("computercraft:wireless_modem_advanced", "right")
rednet.open("right")
msg["type"] = type
msg["name"] = self.name
msg["dimension"] = self.dimension
msg["pos"] = self.absolutePos
rednet.broadcast(msg, PROTOCOL)
end
function Miner:dig(dir)
self:equip("minecraft:diamond_pickaxe", "right")
if dir == "up" then
assert(turtle.digUp())
elseif dir == "forward" then
assert(turtle.dig())
elseif dir == "down" then
assert(turtle.digDown())
end
end
function Miner:xchgItems(forceUnload)
local slots = {}
local emptySlots = 0
for i = 1, 16 do
local info = turtle.getItemDetail(i)
if info then
assert(info.name)
if not BLACKLIST[info.name] then
slots[i] = info
end
else
emptySlots = emptySlots + 1
end
end
if (emptySlots >= 2 and not forceUnload) and turtle.getFuelLevel() >= 1000 then
return
end
if turtle.detectDown() then
self:dig("down")
end
if emptySlots < 2 or forceUnload then
selectItem("enderchests:ender_chest")
assert(turtle.placeDown())
for i, info in pairs(slots) do
assert(turtle.select(i))
assert(turtle.dropDown())
end
self:dig("down")
end
if turtle.getFuelLevel() < 1000 then
selectItem("functionalstorage:ender_drawer")
assert(turtle.placeDown())
assert(turtle.suckDown())
assert(turtle.refuel())
self:dig("down")
end
end
function Miner:safeNeighbors(p)
local ns = {
p + vector.new( 1, 0, 0),
p + vector.new( 0, 0, 1),
p + vector.new(-1, 0, 0),
p + vector.new( 0, 0, -1),
}
-- only allow downwards if above / below bedrock
if (self.absolutePos - self.pos + p).y - 1 > -60 then
table.insert(ns, p + vector.new(0, -1, 0))
end
if self.dimension ~= "the_nether" or (self.absolutePos - self.pos + p).y + 1 < 123 then
table.insert(ns, p + vector.new(0, 1, 0))
end
return ns
end
function Miner:safeMove(dir)
if dir == "up" then
while turtle.detectUp() do
self:dig("up")
end
assert(turtle.up())
elseif dir == "forward" then
while turtle.detect() do
self:dig("forward")
end
assert(turtle.forward())
else
if turtle.detectDown() then
self:dig("down")
end
assert(turtle.down())
end
end
function Miner:findDimension()
self:equip("computercraft:wireless_modem_advanced", "right")
local m = peripheral.wrap("right")
local replyChannel = 666 + os.computerID()
m.open(replyChannel)
m.transmit(666, replyChannel)
local event, side, channel, _, reply, distance = os.pullEvent("modem_message")
assert(distance)
assert(channel == replyChannel)
self.dimension = reply
m.close(replyChannel)
print("Looks like my dimension is " .. self.dimension)
end
function Miner:doGPS(orientation)
local x, y, z = gps.locate()
assert(x)
self.absolutePos = vector.new(x, y, z)
if orientation then
assert(turtle.forward())
x, y, z = gps.locate()
assert(x)
self.dir = vector.new(x, y, z) - self.absolutePos
assert(turtle.back())
end
end
function Miner:findOres(radius)
self:equip("advancedperipherals:geo_scanner", "right")
local info, err = self.scanner.scan(radius)
assert(info, err)
local found = {}
for _, b in ipairs(info) do
-- only ores and avoid bedrock
if ORES[b.name] and self.absolutePos.y + b.y > -60 and (self.dimension ~= "the_nether" or self.absolutePos.y + b.y < 123) then
table.insert(found, b)
end
end
return found
end
function Miner:faceDir(newDir)
local deltaDir = direction(newDir) - direction(self.dir)
if deltaDir == 0.25 or deltaDir == -0.75 then
assert(turtle.turnRight())
self.dir = newDir
elseif deltaDir == -0.25 or deltaDir == 0.75 then
assert(turtle.turnLeft())
self.dir = newDir
elseif deltaDir == 0.5 or deltaDir == -0.5 then
assert(turtle.turnRight())
assert(turtle.turnRight())
self.dir = newDir
elseif deltaDir == 0 then
-- nothing to do :)
else
assert(false, "invalid delta dir "..deltaDir)
end
end
function Miner:turnDir(delta)
local newDirNum = direction(self.dir) + delta
if newDirNum >= 1 then
newDirNum = newDirNum - 1
elseif newDirNum <= -1 then
newDirNum = newDirNum + 1
end
self:faceDir(invDirMap[newDirNum])
end
function Miner:navigateThrough(path, limit)
for _, p in ipairs(path) do
-- print("move from to", self.pos, p)
local delta = p - self.pos
assert(delta:length() ~= 0)
-- print("doing", delta)
if delta:length() ~= 1 then
-- print("path finding between points")
local path, brokeLimit = findPath(self.pos, p, function(v) return self:safeNeighbors(v) end, limit)
table.remove(path, 1)
if not brokeLimit then
table.remove(path, #path)
end
assert(#path ~= 0)
self:navigateThrough(path)
if brokeLimit then
return false
end
delta = p - self.pos
end
assert(delta:length() == 1)
local moveDir = nil
if delta.y == 1 then
moveDir = "up"
elseif delta.y == -1 then
moveDir = "down"
else
self:faceDir(delta)
moveDir = "forward"
end
self:safeMove(moveDir)
self.pos = p
self.absolutePos = self.absolutePos + delta
end
return true
end
function Miner:checkRecall()
local sender, msg = rednet.receive(PROTOCOL_CTRL, 0.5)
if sender and msg.type == "recall" then
-- HACK: Seems like we need this so the monitor can pick up the ack...
sleep(0.5)
self:sendMessage("ackRecall", msg)
self:xchgItems(true)
local to = vector.new(msg.to.x, msg.to.y, msg.to.z)
print("Recalling to " .. to:tostring())
while not self:navigateThrough({self.pos + (to - self.absolutePos)}, 64) do
self:sendMessage("recallProgress", { to = msg.to })
self:xchgItems()
end
self:xchgItems(true)
return true
end
return false
end
function Miner:mineOres(radius)
local startingDir = self.dir
self.pos = vector.new(0, 0, 0)
local ores = self:findOres(radius)
local orePoints = {}
for _, b in ipairs(ores) do
table.insert(orePoints, vector.new(b.x, b.y, b.z))
end
local veins = groupOres(ores, findDistances(orePoints))
print("Found "..#ores.." ores ("..#veins.." veins)")
local veinsPoints = {
self.pos,
self.dir * radius * 2,
}
for _, v in ipairs(veins) do
table.insert(veinsPoints, v.center)
end
local veinsPath = pathThrough(veinsPoints, findDistances(veinsPoints), 1, 2)
-- strip out end point
table.remove(veinsPath, #veinsPath)
local veinsSummary = {}
for _, i in ipairs(veinsPath) do
local v = veins[i - 2]
table.insert(veinsSummary, {
pos = self.absolutePos + v.center,
type = v.type,
count = #v.blocks,
})
end
self:sendMessage("iterationStart", {
i = self.i,
oreCount = #ores,
veins = veinsSummary,
})
if self:checkRecall() then
return false
end
for pi, i in ipairs(veinsPath) do
self:xchgItems()
local vein = veins[i - 2]
local closest = nil
local cDist = math.huge
for _, b in ipairs(vein.blocks) do
local d = (b - self.pos):length()
if d < cDist then
closest = b
cDist = d
end
end
assert(closest)
-- Find path here so we can strip out the actual ore itself
local pathToStart = findPath(self.pos, closest, function(v) return self:safeNeighbors(v) end)
table.remove(pathToStart, 1)
table.remove(pathToStart, #pathToStart)
print("Moving to vein of " .. #vein.blocks .. " " .. vein.type .. " " .. cDist .. " blocks away starting at", closest)
self:navigateThrough(pathToStart)
local veinPoints = {self.pos}
for _, b in ipairs(vein.blocks) do
table.insert(veinPoints, b)
end
local veinPathI = pathThrough(veinPoints, findDistances(veinPoints), 1)
local veinPath = {}
for _, i in ipairs(veinPathI) do
table.insert(veinPath, veinPoints[i])
end
print("Digging through vein (" .. #vein.blocks .. " blocks)")
self:sendMessage("veinStart", {
i = pi,
total = #veins,
oreType = vein.type,
oreCount = #vein.blocks,
})
if self:checkRecall() then
return false
end
self:navigateThrough(veinPath)
end
self:navigateThrough({veinsPoints[2]})
self:faceDir(startingDir)
self.i = self.i + 1
return true
end
function Miner:loop()
self:turnDir(0.25)
local shouldRun = true
while shouldRun do
local i = self.i - 1
if i > 1 and (i % self.stripWidth == 0 or i % self.stripWidth == 1) then
local delta = -0.25 -- left
if i % (self.stripWidth * 2) == 0 or i % (self.stripWidth * 2) == 1 then
delta = 0.25 -- right
end
self:turnDir(delta)
end
shouldRun = self:mineOres(16)
end
end
local miner = Miner.new()
miner:loop()