-- miner_startup.lua — Flicker-free UI + debug + “mostly-stupid” miner -- Axis-only mine vectors. No area/volume planning. Controller drives vectors. -- Requires: CC:Tweaked turtle with wireless/Ender modem. -------------------- CONFIG -------------------- local PROTOCOL = "minesys:v1" -- Home / logistics (edit to your base) local HOME = { x=-924, y=99, z=-1093 } local SHAFT_XZ = { x=-924, z=-1092 } local CHEST_FUEL = { x=-924, y=98, z=-1091 } local CHEST_DUMP = { x=-924, y=99, z=-1091 } -- Fuel policy local FUEL_MIN_BUFFER = 200 local FUEL_PER_STEP = 1 local REFUEL_TARGET = 3000 -- Timing local HEARTBEAT_SEC = 2.0 local UI_REFRESH = 0.5 ------------------------------------------------ -- Peripherals local modem = peripheral.find("modem", function(_,p) return p.isWireless() end) assert(modem, "No wireless modem") rednet.open(peripheral.getName(modem)) -- UI window (double-buffered, no flicker) local root = term.current() local W,H = root.getSize() local ui = window.create(root, 1, 1, W, H, true) ui.setCursorBlink(false) local function uiBegin() ui.setVisible(false) ui.setBackgroundColor(colors.black) ui.setTextColor(colors.white) ui.clear() ui.setCursorPos(1,1) end local function uiEnd() ui.setVisible(true) end -- State local BOOT = os.epoch("utc") local function now() return os.epoch("utc") end local function uptime() local s = math.floor((now()-BOOT)/1000) local h = math.floor(s/3600); s=s-3600*h local m = math.floor(s/60); s=s-60*m return string.format("%02d:%02d:%02d", h,m,s) end local S = { status="idle", extra=nil, facing="N", pos=nil, lastFix=nil, fuel=turtle.getFuelLevel(), abort=false, stop=false, uiOn=true, lastAck=nil } -- GPS helpers local function gpsPos() local x,y,z = gps.locate(5) if x then S.pos = { x = math.floor(x+0.5), y = math.floor(y+0.5), z = math.floor(z+0.5) } S.lastFix = now() end return x,y,z end local function fixAge() if not S.lastFix then return nil end return math.floor((now()-S.lastFix)/1000) end -- Inventory stats local function invStats() local empty, fuelAvail = 0, 0 for i=1,16 do local cnt = turtle.getItemCount(i) if cnt==0 then empty = empty + 1 else turtle.select(i) if turtle.refuel(0) then fuelAvail = fuelAvail + cnt end end end turtle.select(1) return { freeSlots=empty, freePct=(empty/16)*100, fuelAvail=fuelAvail } end -- Heartbeat local function hb() S.fuel = turtle.getFuelLevel() if not S.pos then gpsPos() end rednet.broadcast({ t=now(), op="status", id=os.getComputerID(), label=os.getComputerLabel(), pos=S.pos, fuel=S.fuel, facing=S.facing, status=S.status, extra={ kind=S.extra and S.extra.kind or nil, data=S.extra, gps={fixAge=fixAge()} }, inv=invStats() }, PROTOCOL) end local function report(status, extra) S.status=status or S.status; S.extra=extra; hb() end -- UI local spinner = {"|","/","-","\\"}; local sp=1 local function drawUI() if not S.uiOn then return end uiBegin() local lbl = os.getComputerLabel() or ("id"..os.getComputerID()) ui.write(("Miner %s [%s] %s"):format(lbl, spinner[sp], uptime())) sp = (sp % #spinner) + 1 local pos = S.pos and ("("..S.pos.x..","..S.pos.y..","..S.pos.z..")") or "(?, ?, ?)" local fage = fixAge() and (fixAge().."s") or "no-fix" ui.setCursorPos(1,3); ui.write(("Pos: %s GPS: %s"):format(pos, fage)) ui.setCursorPos(1,4); ui.write(("Fuel: %s"):format(S.fuel or "?")) local inv = invStats() ui.setCursorPos(1,5); ui.write(("Inv: free %d/16 filled %d%% fuelItems %d") :format(inv.freeSlots, 100-math.floor(inv.freePct+0.5), inv.fuelAvail)) ui.setCursorPos(1,6); ui.write(("Facing: %s Status: %s"):format(S.facing, S.status or "?")) if S.extra and S.extra.kind=="work" and S.extra.total and S.extra.step then local pct = math.floor(S.extra.step*100/S.extra.total+0.5) ui.setCursorPos(1,8); ui.write(("Work: %d/%d (%d%%)"):format(S.extra.step, S.extra.total, pct)) elseif S.extra and S.extra.kind then ui.setCursorPos(1,8); ui.write(("Extra: %s"):format(S.extra.kind)) end ui.setCursorPos(1,10); ui.write(("Abort:%s Stop:%s"):format(tostring(S.abort), tostring(S.stop))) local mname = peripheral.getName(modem) ui.setCursorPos(1,12); ui.write(("Modem: %s Rednet: open"):format(mname)) if S.lastAck then ui.setCursorPos(1,13); ui.write(("Last ACK: %s"):format(S.lastAck)) end uiEnd() end -- Motion primitives local DIRS={N=0,E=1,S=2,W=3} local function setFacing(dir) if not DIRS[dir] then return false,"bad dir" end local cur=DIRS[S.facing] or 0; local tgt=DIRS[dir] local diff=(tgt-cur)%4 if diff==1 then turtle.turnRight() elseif diff==2 then turtle.turnRight(); turtle.turnRight() elseif diff==3 then turtle.turnLeft() end S.facing=dir; report("idle",{kind="face",dir=dir}); return true end local function digForward() while turtle.detect() do turtle.dig(); sleep(0.05) end end local function stepForward() local tries=0 digForward() while not turtle.forward() do digForward(); tries=tries+1 if tries>60 then return false,"blocked" end sleep(0.05) end return true end local function stepUp() while turtle.detectUp() do turtle.digUp(); sleep(0.05) end local tries=0 while not turtle.up() do while turtle.detectUp() do turtle.digUp(); sleep(0.05) end tries=tries+1; if tries>60 then return false,"blocked up" end sleep(0.05) end return true end local function stepDown() while turtle.detectDown() do turtle.digDown(); sleep(0.05) end local tries=0 while not turtle.down() do while turtle.detectDown() do turtle.digDown(); sleep(0.05) end tries=tries+1; if tries>60 then return false,"blocked down" end sleep(0.05) end return true end local function moveAxis(axis, n) if n==0 then return true end local dir, steps = nil, math.abs(n) if axis=="x" then dir = n>0 and "E" or "W" elseif axis=="z" then dir = n>0 and "S" or "N" elseif axis=="y" then for i=1,steps do if S.abort then return false,"abort" end report("moving",{kind="move",axis="y",step=i,total=steps}) local ok,err = (n>0) and stepUp() or stepDown() if not ok then return false,err end hb() end return true else return false,"bad axis" end assert(setFacing(dir)) for i=1,steps do if S.abort then return false,"abort" end report("moving",{kind="move",axis=axis,step=i,total=steps}) local ok,err = stepForward() if not ok then return false,err end hb() end return true end -- Distance/fuel estimates to get home local function distToHome() if not S.pos then return 999999 end local x,y,z = S.pos.x,S.pos.y,S.pos.z local d1 = math.abs(x-SHAFT_XZ.x) + math.abs(z-SHAFT_XZ.z) local d2 = math.abs(HOME.y - y) local d3 = math.abs(HOME.z - (-1092)) return d1 + d2 + d3 end local function fuelNeededToHome() return distToHome()*FUEL_PER_STEP + FUEL_MIN_BUFFER end -- Routing + base ops local function gotoPos(tx,ty,tz, order) order = order or {"y","x","z"} if not S.pos then gpsPos() end if not S.pos then return false,"no gps" end local dx,dy,dz = tx - S.pos.x, ty - S.pos.y, tz - S.pos.z local map={x=dx,y=dy,z=dz} for _,ax in ipairs(order) do local ok,err = moveAxis(ax, map[ax]); if not ok then return false,err end end return true end local function faceTo(target) if not S.pos then return false,"no gps" end local dx, dz = target.x - S.pos.x, target.z - S.pos.z if math.abs(dx)>math.abs(dz) then return setFacing(dx>0 and "E" or "W") else return setFacing(dz>0 and "S" or "N") end end local function returnToBase() report("returning",{kind="service",stage="to_shaft"}); assert(gotoPos(SHAFT_XZ.x, S.pos.y, SHAFT_XZ.z, {"x","z"})) report("returning",{kind="service",stage="shaft"}); assert(gotoPos(SHAFT_XZ.x, HOME.y, SHAFT_XZ.z, {"y"})) report("returning",{kind="service",stage="home"}); assert(gotoPos(HOME.x, HOME.y, HOME.z, {"z"})) return true end local function dumpAll() assert(gotoPos(CHEST_DUMP.x, CHEST_DUMP.y, CHEST_DUMP.z, {"x","y","z"})) faceTo({x=CHEST_DUMP.x+1, z=CHEST_DUMP.z}) for i=1,16 do local d=turtle.getItemDetail(i) if d then turtle.select(i) if turtle.refuel(0) then else turtle.drop() end end end turtle.select(1); return true end local function refuelFromChest() assert(gotoPos(CHEST_FUEL.x, CHEST_FUEL.y, CHEST_FUEL.z, {"x","y","z"})) faceTo({x=CHEST_FUEL.x+1, z=CHEST_FUEL.z}) local pulls=0 while turtle.getFuelLevel() < REFUEL_TARGET and pulls<64 do if turtle.suck() then for i=1,16 do turtle.select(i); if turtle.refuel(0) then turtle.refuel() end end turtle.select(1) else break end pulls=pulls+1 end return true end local function serviceIfNeeded() local need = fuelNeededToHome() if S.fuel < need then report("service",{kind="low_fuel", need=need, have=S.fuel}) assert(returnToBase()); assert(dumpAll()); assert(refuelFromChest()); assert(gotoPos(HOME.x, HOME.y, HOME.z)) report("idle",{kind="serviced"}); return true end local inv = invStats() if inv.freeSlots <= 1 then report("service",{kind="dump"}) assert(returnToBase()); assert(dumpAll()); assert(refuelFromChest()); assert(gotoPos(HOME.x, HOME.y, HOME.z)) report("idle",{kind="serviced"}); return true end return false end -- Mining (single axis only) local function mineVector(axis, len, stopOnAir, stopOnDirt) len = tonumber(len) or 0 if len==0 then return true end serviceIfNeeded() if axis=="y" then local steps=math.abs(len) for i=1,steps do if S.abort then return false,"abort" end report("ACTIVE",{kind="work",step=i,total=steps}) local ok,err = (len>0) and stepUp() or stepDown() if not ok then return false,err end hb(); serviceIfNeeded() end report("idle"); return true end local dir = (axis=="x") and ((len>0) and "E" or "W") or (axis=="z") and ((len>0) and "S" or "N") or nil if not dir then return false,"bad axis" end assert(setFacing(dir)) local steps = math.abs(len) for i=1,steps do if S.abort then return false,"abort" end if stopOnAir or stopOnDirt then local ok, data = turtle.inspect() if ok and stopOnDirt and data and data.name and data.name:find("dirt") then report("STOPPED",{kind="halt_dirt"}); return true end if (not ok) and stopOnAir then report("STOPPED",{kind="halt_air"}); return true end end report("ACTIVE",{kind="work",step=i,total=steps}) digForward() local ok,err = stepForward() if not ok then return false,err end hb(); serviceIfNeeded() end report("idle"); return true end -- Debug commands local function dbg_echo(msg) return true, ("echo: "..tostring(msg)) end local function dbg_uptime() return true, uptime() end local function dbg_toggle_ui(on) S.uiOn = on~=false; return true, "ui:"..tostring(S.uiOn) end local function dbg_where() return true, S.pos end local function dbg_gps() local x,y,z = gps.locate(3) local ids = {rednet.lookup("gps")} local n=0 for _ in pairs(ids) do n=n+1 end return x~=nil, {x=x,y=y,z=z, fixAge=fixAge(), beacons=n} end local function dbg_net() local list={} for _,n in ipairs(peripheral.getNames()) do table.insert(list, n..":"..peripheral.getType(n)) end return true, {periph=list} end -- Threads local function uiThread() while true do drawUI(); sleep(UI_REFRESH) end end local function hbThread() while true do sleep(HEARTBEAT_SEC); hb() end end local function rxThread() while true do local sid,msg,proto = rednet.receive(PROTOCOL) if proto==PROTOCOL and type(msg)=="table" and msg.op=="cmd" then local ok,err=false,nil if msg.act=="face" then ok,err=setFacing(msg.dir) elseif msg.act=="up" then ok,err=moveAxis("y", math.abs(msg.n or 1)) elseif msg.act=="down" then ok,err=moveAxis("y", -math.abs(msg.n or 1)) elseif msg.act=="mine_vector" then ok,err=mineVector(msg.axis, msg.len, msg.stopOnAir, msg.stopOnDirt) elseif msg.act=="return_service" then ok,err=returnToBase() elseif msg.act=="dump" then ok,err=dumpAll() elseif msg.act=="refuel" then ok,err=refuelFromChest() elseif msg.act=="set_facing_tag" then S.facing=msg.dir or S.facing; ok=true elseif msg.act=="set_pos" then if msg.x and msg.y and msg.z then S.pos={x=msg.x,y=msg.y,z=msg.z}; ok=true else err="bad pos" end elseif msg.act=="abort" then S.abort=true; ok=true elseif msg.act=="resume" then S.abort=false; ok=true elseif msg.act=="stop" then S.stop=true; ok=true -- DEBUG: elseif msg.act=="dbg_echo" then ok,err=dbg_echo(msg.msg) elseif msg.act=="dbg_net" then ok,err=dbg_net() elseif msg.act=="dbg_gps" then ok,err=dbg_gps() elseif msg.act=="dbg_uptime" then ok,err=dbg_uptime() elseif msg.act=="dbg_toggle_ui" then ok,err=dbg_toggle_ui(msg.on) elseif msg.act=="dbg_where" then ok,err=dbg_where() else err="unknown act" end S.lastAck = (ok and "ok") or ("err:"..tostring(err)) rednet.send(sid,{op="ack", ok=ok, err=err, ref=msg.ref}, PROTOCOL) hb() end end end report("idle",{kind="boot"}) parallel.waitForAny(uiThread, hbThread, rxThread)