Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --[[
- StiGMaToPiA MailTurtle v1.4.3 (MC 1.12.2 • ComputerCraft classic 1.8pr1)
- PURPOSE
- A data-driven “mailman” turtle that follows a colored-wool track, visits stations,
- picks up parcels for destinations, drops off by owner, refuels at route depots, and
- persists state so it can resume after server restarts. Multiple turtles can share
- the same network.
- TRACK ACTIONS (data-driven; edit /mailman/trackmap.json to change)
- Semantics (NORMAL mode vs REVERSED mode):
- forward : FORWARD in both modes
- left : NORMAL=turn LEFT then FORWARD; REVERSED=FORWARD
- right : NORMAL=turn RIGHT then FORWARD; REVERSED=FORWARD
- climb : NORMAL=UP 1 then FORWARD; REVERSED=FORWARD
- descend : NORMAL=FORWARD 1 then DOWN 1; REVERSED=FORWARD
- uturn_left : NORMAL=FORWARD; REVERSED=turn LEFT then FORWARD
- uturn_right : NORMAL=FORWARD; REVERSED=turn RIGHT then FORWARD
- uturn_climb : NORMAL=FORWARD; REVERSED=UP 1 then FORWARD
- uturn_descend : NORMAL=FORWARD; REVERSED=DOWN 1 then FORWARD
- station : same in both, but mailbox/fuel SIDES invert in REVERSED mode
- routeRefuel : same in both, SIDES invert in REVERSED mode
- optRefuel : same in both, SIDES invert in REVERSED mode
- intersection : SAME rule in both modes → Try Right → Straight → Left
- uturn : toggle reversed-mode (persist), 180° turn, then FORWARD
- • GUARDED FORWARD (unchanged intent, kept):
- After ANY forward step, if the landing tile has no recognized marker, we LOG and SHUTDOWN:
- "TurtleMail, Shutdown: Movement marker undetected"
- • SHUTDOWN LOGGING (kept):
- On shutdown/stop we log current + starting position (GPS if available; else relative odometry).
- • STATION MONITOR MIRROR (kept):
- If a (Advanced) Monitor block is directly ABOVE on station/routeRefuel/optRefuel, logs mirror to it.
- • CHUNKLOADER HANDSHAKE (kept):
- - If `state.config.chunkloader.required` is true, we must successfully start the Chunk Loader before running.
- - A password can be provided in `state.config.chunkloader.password`; if absent, we prompt at startup.
- - We try to run a local program named "chunky" or "chunkloader". If missing, we attempt:
- shell.run("pastebin","get","5rNAyGcU","/mailman/chunky.lua")
- then run it with the provided password. If any step fails → log and shutdown.
- - (Reference the user-provided pastebin ID: 5rNAyGcU.)
- DEFAULT TRACK MAP (auto-written to /mailman/trackmap.json if missing)
- - Unused wool colors:
- -- Black wool (15)
- -- Purple wool (10)
- {
- "forward": [ {"name":"minecraft:cobblestone"}, {"name":"minecraft:wool","meta":5} ], -- Cobblestone or Lime wool
- "left": [ {"name":"minecraft:wool","meta":11} ], -- Blue wool
- "right": [ {"name":"minecraft:wool","meta":14} ], -- Red wool
- "intersection": [ {"name":"minecraft:wool","meta":9} ], -- Cyan wool
- "station": [ {"name":"minecraft:wool","meta":13} ], -- Green wool
- "climb": [ {"name":"minecraft:wool","meta":4} ], -- Yellow wool
- "descend": [ {"name":"minecraft:wool","meta":8} ], -- Light Gray wool
- "routeRefuel": [ {"name":"minecraft:wool","meta":7} ], -- Gray wool
- "optRefuel": [ {"name":"minecraft:wool","meta":1} ], -- Orange wool
- "uturn": [ {"name":"minecraft:wool","meta":12} ], -- Brown wool
- "uturn_left": [ {"name":"minecraft:wool","meta":2} ], -- Magenta wool
- "uturn_right": [ {"name":"minecraft:wool","meta":3} ], -- Light blue wool
- "uturn_climb": [ {"name":"minecraft:wool","meta":6} ], -- Pink wool
- "uturn_descend": [ {"name":"minecraft:wool","meta":10} ] -- Purple wool
- }
- FLOPPY JSON (unchanged)
- • Drop-off:
- {
- "stationType":"dropoff",
- "stationId":"Hannah-Inbox",
- "owner":"Hannah"
- }
- • Pickup: (OPTIONAL "owner" lets logs show sender nicely)
- {
- "stationType":"pickup",
- "stationId":"Hannah->StiGMaT",
- "destination":"StiGMaT",
- "owner":"Hannah"
- }
- • Optional Refuel (gray):
- {
- "stationType":"optRefuel",
- "stationId":"Hannah-Refuel",
- "take":2000, "waitForQuota":false,
- "accept":["minecraft:coal","minecraft:coal_block"]
- }
- STATE FILE (/mailman/state.json), version=2:
- {
- "version": 2,
- "turtleId": "...",
- "slots": { "1": {"dest":"StiGMaT","count":12,"from":"Hannah"}, ... },
- "stats": { "delivered":0, "picked":0 },
- "config": {
- "maxFuelSlots": 2,
- "collisionJitter": [0.15,0.25],
- "log": { "enabled": true, "toFile": false, "maxFileKB": 128 },
- "chunkloader": { "required": true, "password": null, "programHint": "chunky|chunkloader|/mailman/chunky.lua" }
- },
- "pose": {
- "hasGPS": false,
- "startAbs": {"x":null,"y":null,"z":null},
- "startRel": {"x":0,"y":0,"z":0},
- "abs": {"x":null,"y":null,"z":null},
- "rel": {"x":0,"y":0,"z":0},
- "heading": 0,
- "reversed": false
- }
- }
- --]]
- --========================
- -- Tiny JSON shim
- --========================
- local function hasJSON()
- return type(textutils)=="table" and type(textutils.serializeJSON)=="function" and type(textutils.unserializeJSON)=="function"
- end
- local function escape_str(s)
- s = s:gsub('\\','\\\\'):gsub('"','\\"'):gsub('\n','\\n'):gsub('\r','\\r'):gsub('\t','\\t')
- return '"'..s..'"'
- end
- local function encode_json(v)
- local t = type(v)
- if t=="nil" then return "null"
- elseif t=="boolean" then return v and "true" or "false"
- elseif t=="number" then return tostring(v)
- elseif t=="string" then return escape_str(v)
- elseif t=="table" then
- local isArray, n = true, 0
- for k,_ in pairs(v) do if type(k)~="number" then isArray=false break end if k>n then n=k end end
- if isArray then
- local parts = {}
- for i=1,n do parts[#parts+1] = encode_json(v[i]) end
- return "["..table.concat(parts,",").."]"
- else
- local parts = {}
- for k,val in pairs(v) do parts[#parts+1] = escape_str(tostring(k))..":"..encode_json(val) end
- return "{"
- ..table.concat(parts,",")
- .."}"
- end
- else return "null" end
- end
- local function decode_json(s)
- if hasJSON() then return textutils.unserializeJSON(s) end
- error("No JSON parser available in this CC build. Please update to 1.8+ or enable JSON.")
- end
- --========================
- -- Paths & initial state
- --========================
- local STATE_PATH = "/mailman/state.json"
- local TRACKMAP_PATH = "/mailman/trackmap.json"
- local LOG_PATH = "/mailman/log.txt"
- local CHUNKY_PATH = "/mailman/chunky.lua"
- local state = {
- version = 2,
- turtleId = tostring(os.getComputerID() or os.getComputerLabel() or "unknown"),
- slots = {},
- stats = { delivered=0, picked=0 },
- config = {
- maxFuelSlots = 2,
- collisionJitter = {0.15,0.25},
- log = { enabled = true, toFile = false, maxFileKB = 128 },
- chunkloader = { required = true, password = nil, programHint = "chunky|chunkloader|/mailman/chunky.lua" }
- },
- pose = {
- hasGPS = false,
- startAbs = {x=nil,y=nil,z=nil}, startRel={x=0,y=0,z=0},
- abs = {x=nil,y=nil,z=nil}, rel ={x=0,y=0,z=0},
- heading=0,
- reversed=false
- }
- }
- local DEFAULT_TRACKMAP = {
- forward = { {name="minecraft:cobblestone"}, {name="minecraft:wool", meta=5} },
- left = { {name="minecraft:wool", meta=11} },
- right = { {name="minecraft:wool", meta=14} },
- intersection = { {name="minecraft:wool", meta=9} },
- station = { {name="minecraft:wool", meta=13} },
- climb = { {name="minecraft:wool", meta=4} },
- descend = { {name="minecraft:wool", meta=8} },
- routeRefuel = { {name="minecraft:wool", meta=7} },
- optRefuel = { {name="minecraft:wool", meta=1} },
- uturn = { {name="minecraft:wool", meta=12} },
- uturn_left = { {name="minecraft:wool", meta=2} },
- uturn_right = { {name="minecraft:wool", meta=3} },
- uturn_climb = { {name="minecraft:wool", meta=6} },
- uturn_descend= { {name="minecraft:wool", meta=10} }
- }
- local trackmap = nil
- local shuttingDown = false
- local movedThisTick = false
- --========================
- -- FS helpers
- --========================
- local function ensureDir(path)
- local dir = fs.getDir(path)
- if dir ~= "" and not fs.exists(dir) then fs.makeDir(dir) end
- end
- local function loadFileJSON(path)
- if not fs.exists(path) then return nil end
- local f=fs.open(path,"r"); if not f then return nil end
- local txt=f.readAll(); f.close()
- local ok, data = pcall(decode_json, txt)
- if ok then return data end
- return nil
- end
- local function saveFileJSON(path, tbl)
- ensureDir(path)
- local f=fs.open(path,"w"); if not f then return end
- local body = hasJSON() and textutils.serializeJSON(tbl,true) or encode_json(tbl)
- f.write(body); f.close()
- end
- local function loadState()
- local s = loadFileJSON(STATE_PATH)
- if type(s)=="table" then
- for k,v in pairs(s) do state[k]=v end
- if not state.pose then
- state.pose = { hasGPS=false, startAbs={x=nil,y=nil,z=nil}, startRel={x=0,y=0,z=0},
- abs={x=nil,y=nil,z=nil}, rel={x=0,y=0,z=0}, heading=0, reversed=false }
- end
- if not state.version or state.version<2 then state.version=2 end
- end
- end
- local function saveState() saveFileJSON(STATE_PATH, state) end
- -- Load/augment trackmap: preserve existing, add any missing new keys
- local function deepCopy(t) local r={} for k,v in pairs(t) do if type(v)=="table" then r[k]=deepCopy(v) else r[k]=v end end return r end
- local function mergeMissing(dst, defaults)
- for k,v in pairs(defaults) do
- if dst[k]==nil then dst[k]=deepCopy(v)
- elseif type(dst[k])=="table" and type(v)=="table" then
- -- leave existing arrays as-is (don't auto-merge signature lists)
- end
- end
- end
- local function tryLoadTrackmap()
- for _,mount in ipairs({"/disk","/disk2","/disk3"}) do
- local p = mount.."/trackmap.json"
- if fs.exists(p) then
- local t = loadFileJSON(p)
- if type(t)=="table" then mergeMissing(t, DEFAULT_TRACKMAP); return t end
- end
- end
- local t = loadFileJSON(TRACKMAP_PATH)
- if type(t)=="table" then mergeMissing(t, DEFAULT_TRACKMAP); return t end
- saveFileJSON(TRACKMAP_PATH, DEFAULT_TRACKMAP)
- return deepCopy(DEFAULT_TRACKMAP)
- end
- --========================
- -- Logging + monitor mirror
- --========================
- local logCtx = { mon=nil }
- local function logEnabled() return state.config and state.config.log and state.config.log.enabled end
- local function ts()
- local day = (os.day and os.day()) or 0
- local timeStr = (textutils and textutils.formatTime and textutils.formatTime(os.time(), true)) or tostring(os.time())
- return ("[Day %d %s]"):format(day, timeStr)
- end
- local function appendFileLine(path, line)
- local f = fs.open(path, fs.exists(path) and "a" or "w")
- if not f then return end
- f.writeLine(line); f.close()
- local kb = math.floor((fs.getSize(path) or 0)/1024)
- local lim = (state.config.log.maxFileKB or 128)
- if kb > lim then local rf=fs.open(path,"w"); if rf then rf.writeLine("<<LOG ROTATED>>"); rf.close() end end
- end
- local function getTopMonitor()
- if not peripheral or not peripheral.getType then return nil end
- local t = peripheral.getType("top")
- if t=="monitor" then return peripheral.wrap("top") end
- return nil
- end
- local function monWriteLine(mon, line)
- if not mon then return end
- local w,h = mon.getSize()
- local x,y = mon.getCursorPos()
- if y >= h then mon.scroll(1); y=h; mon.setCursorPos(1,y) end
- mon.write(line)
- if y < h then mon.setCursorPos(1, y+1) else mon.setCursorPos(1, h) end
- end
- local function logLine(msg)
- if not logEnabled() then return end
- local line = ts().." "..msg
- print(line)
- if state.config.log.toFile then ensureDir(LOG_PATH); appendFileLine(LOG_PATH, line) end
- if logCtx.mon then monWriteLine(logCtx.mon, line) end
- end
- --========================
- -- GPS / Odometry + Shutdown
- --========================
- local function tryGPS()
- if not gps or not gps.locate then return end
- local x,y,z = gps.locate(1)
- if x and y and z then
- if not state.pose.hasGPS then
- state.pose.hasGPS = true
- state.pose.startAbs = {x=x,y=y,z=z}
- end
- state.pose.abs = {x=x,y=y,z=z}
- end
- end
- local function posStr()
- local rel = state.pose.rel
- local srel = ("rel=(%d,%d,%d)"):format(rel.x,rel.y,rel.z)
- if state.pose.hasGPS and state.pose.abs and state.pose.startAbs then
- local a = state.pose.abs
- local sa = ("abs=(%d,%d,%d)"):format(a.x,a.y,a.z)
- local sa0= ("startAbs=(%d,%d,%d)"):format(state.pose.startAbs.x,state.pose.startAbs.y,state.pose.startAbs.z)
- return sa.." "..srel.." "..sa0
- else
- local s0 = ("startRel=(%d,%d,%d)"):format(state.pose.startRel.x,state.pose.startRel.y,state.pose.startRel.z)
- return srel.." "..s0
- end
- end
- local function shutdownWithReason(reason)
- if shuttingDown then return end
- shuttingDown = true
- logLine("TurtleMail, Shutdown: "..tostring(reason).." | "..posStr())
- saveState()
- sleep(0.2)
- os.shutdown()
- end
- local function withTerminateTrap(fn)
- local ok, err = xpcall(fn, function(e) return e end)
- if not ok then
- if tostring(err or ""):lower():find("terminated") then
- shutdownWithReason("User Terminated")
- else
- logLine("Runtime error: "..tostring(err))
- shutdownWithReason("Unhandled Error")
- end
- end
- end
- --========================
- -- Pose wrappers for movement
- --========================
- local function _advanceRelForward()
- local h = state.pose.heading % 4
- if h==0 then state.pose.rel.z = state.pose.rel.z + 1
- elseif h==1 then state.pose.rel.x = state.pose.rel.x + 1
- elseif h==2 then state.pose.rel.z = state.pose.rel.z - 1
- elseif h==3 then state.pose.rel.x = state.pose.rel.x - 1
- end
- end
- local function _turnLeftRel() state.pose.heading = (state.pose.heading + 3) % 4 end
- local function _turnRightRel() state.pose.heading = (state.pose.heading + 1) % 4 end
- local function _upRel() state.pose.rel.y = state.pose.rel.y + 1 end
- local function _downRel() state.pose.rel.y = state.pose.rel.y - 1 end
- local function turnLeft() turtle.turnLeft(); _turnLeftRel() end
- local function turnRight() turtle.turnRight(); _turnRightRel() end
- local function moveForward()
- local ok = turtle.forward()
- if ok then _advanceRelForward(); movedThisTick=true; tryGPS() end
- return ok
- end
- local function moveUp() local ok=turtle.up(); if ok then _upRel(); tryGPS() end return ok end
- local function moveDown() local ok=turtle.down(); if ok then _downRel(); tryGPS() end return ok end
- --========================
- -- Movement + collision
- --========================
- local function jitterSleep()
- local j = state.config.collisionJitter
- sleep(math.random()*(j[2]-j[1]) + j[1])
- end
- local function safeForward()
- local tries=0
- while not moveForward() do
- if turtle.detect() then
- tries=tries+1; if tries>10 then return false,"blocked" end
- sleep(0.3)
- else
- jitterSleep()
- end
- end
- return true
- end
- --========================
- -- Block probing / action resolve
- --========================
- local function getUnderInfo()
- local ok, info = turtle.inspectDown()
- if not ok then return nil end
- info.metadata = info.metadata
- if info.state and info.state.color and type(info.state.color)=="string" then
- info._color = tostring(info.state.color)
- end
- return info
- end
- local function matchesSignature(info, sig)
- if not info or not sig or info.name ~= sig.name then return false end
- if sig.meta ~= nil then return (info.metadata == sig.meta) end
- if sig.color ~= nil then return (info._color == sig.color) end
- return true
- end
- -- deterministic order; "uturn_*" included
- local ACTION_ORDER = {
- "station","routeRefuel","optRefuel",
- "climb","descend",
- "uturn_climb","uturn_descend",
- "intersection","left","right",
- "uturn_left","uturn_right",
- "uturn",
- "forward"
- }
- local function resolveActionUnder()
- local info = getUnderInfo()
- if not info then return nil end
- for _,action in ipairs(ACTION_ORDER) do
- local sigs = trackmap[action]
- if type(sigs)=="table" then
- for _,sig in ipairs(sigs) do
- if matchesSignature(info, sig) then return action end
- end
- end
- end
- return nil
- end
- --========================
- -- Guard rail (after forward step)
- --========================
- local function guardRailCheckOrShutdown()
- if not movedThisTick then return end
- movedThisTick=false
- local a = resolveActionUnder()
- if not a then shutdownWithReason("Movement marker undetected") end
- end
- --========================
- -- Side helpers respecting REVERSED mode
- --========================
- local function withMailboxSide(fn)
- if not state.pose.reversed then
- turnLeft(); local ok,err=pcall(fn); turnRight(); return ok,err
- else
- turnRight(); local ok,err=pcall(fn); turnLeft(); return ok,err
- end
- end
- local function withFuelChestSide(fn)
- if not state.pose.reversed then
- turnRight(); local ok,err=pcall(fn); turnLeft(); return ok,err
- else
- turnLeft(); local ok,err=pcall(fn); turnRight(); return ok,err
- end
- end
- --========================
- -- Inventory helpers
- --========================
- local function refuelAllInSelected()
- local before=turtle.getFuelLevel(); turtle.refuel(64); return turtle.getFuelLevel()-before
- end
- local function countFreeCargoSlots()
- local fuelSlots=state.config.maxFuelSlots or 0
- local lastFuelStart=16-fuelSlots+1
- local free=0
- for s=1,16 do
- local isFuel=(fuelSlots>0 and s>=lastFuelStart)
- if turtle.getItemCount(s)==0 and not isFuel then free=free+1 end
- end
- return free
- end
- local function nextFuelSlot()
- local fuelSlots=state.config.maxFuelSlots or 0
- if fuelSlots<=0 then return nil end
- local start=16-fuelSlots+1
- for s=start,16 do if turtle.getItemSpace(s)>0 then return s end end
- return nil
- end
- local function nextCargoSlotFor(dest)
- local fuelSlots=state.config.maxFuelSlots or 0
- local startFuel=16-fuelSlots+1
- for s=1,16 do
- if not (fuelSlots>0 and s>=startFuel) then
- local cnt=turtle.getItemCount(s)
- if cnt>0 and state.slots[tostring(s)] and state.slots[tostring(s)].dest==dest and turtle.getItemSpace(s)>0 then
- return s
- end
- end
- end
- for s=1,16 do
- if not (fuelSlots>0 and s>=startFuel) and turtle.getItemCount(s)==0 then return s end
- end
- return nil
- end
- local function tagNewlyFilledSlots(dest, sender)
- for s=1,16 do
- local key=tostring(s); local cnt=turtle.getItemCount(s)
- if cnt>0 and (not state.slots[key] or not state.slots[key].dest) then
- state.slots[key]={dest=dest,count=cnt,from=sender}
- elseif cnt>0 and state.slots[key] and state.slots[key].dest==dest then
- state.slots[key].count=cnt; if sender and not state.slots[key].from then state.slots[key].from=sender end
- end
- end
- end
- local function refreshSlotCounts()
- for s=1,16 do
- local key=tostring(s); local cnt=turtle.getItemCount(s)
- if cnt>0 then
- if not state.slots[key] then state.slots[key]={dest=nil,count=cnt}
- else state.slots[key].count=cnt end
- else state.slots[key]=nil end
- end
- end
- --========================
- -- Chest I/O
- --========================
- local function dumpAllForOwner(ownerName)
- local moved, freed = 0,0
- local senders = {}
- withMailboxSide(function()
- for s=1,16 do
- local tag=state.slots[tostring(s)]; local cnt=turtle.getItemCount(s)
- if tag and tag.dest==ownerName and cnt>0 then
- turtle.select(s); turtle.drop()
- local after=turtle.getItemCount(s)
- local delta=cnt-after
- if delta>0 then
- moved=moved+delta; if after==0 then freed=freed+1 end
- local who=(tag.from or "unknown"); senders[who]=(senders[who] or 0)+delta
- state.slots[tostring(s)] = (after>0) and {dest=tag.dest,count=after,from=tag.from} or nil
- end
- end
- end
- end)
- state.stats.delivered=(state.stats.delivered or 0)+moved
- return moved, freed, senders
- end
- local function pickupAllForDestination(dest, senderName)
- if countFreeCargoSlots()==0 then return 0,0,"full" end
- local beforeCounts={} for s=1,16 do beforeCounts[s]=turtle.getItemCount(s) end
- withMailboxSide(function()
- while countFreeCargoSlots()>0 do
- local slot=nextCargoSlotFor(dest); if not slot then break end
- turtle.select(slot); if not turtle.suck() then break end
- tagNewlyFilledSlots(dest, senderName)
- state.stats.picked=(state.stats.picked or 0)+1
- end
- end)
- local picked=0
- for s=1,16 do local after=turtle.getItemCount(s); local before=beforeCounts[s] or 0
- if after>before then picked=picked+(after-before) end
- end
- local reserved=0
- for s=1,16 do local tag=state.slots[tostring(s)]
- if tag and tag.dest==dest and turtle.getItemCount(s)>0 then reserved=reserved+1 end
- end
- return picked, reserved
- end
- local function refuelFromSideBestEffort(targetLevel, mustWait, acceptNames)
- acceptNames = acceptNames or {["minecraft:coal"]=true,["minecraft:coal_block"]=true}
- local function isAccepted(detail) return detail and acceptNames[detail.name] end
- local filled=0
- withFuelChestSide(function()
- local function getFuel() return turtle.getFuelLevel()=="unlimited" and math.huge or turtle.getFuelLevel() end
- local function getLimit() return turtle.getFuelLimit()=="unlimited" and math.huge or turtle.getFuelLimit() end
- local limit=getLimit(); targetLevel=math.min(targetLevel or limit, limit)
- local function tryOnce()
- if getFuel()>=targetLevel then return true end
- local fslt=nextFuelSlot(); if not fslt then return false end
- turtle.select(fslt); if not turtle.suck() then return false end
- local det=turtle.getItemDetail(fslt)
- if not isAccepted(det) then turtle.drop(); return false end
- local gained=refuelAllInSelected(); filled=filled+gained
- return getFuel()>=targetLevel
- end
- while getFuel()<targetLevel do
- if not tryOnce() then if mustWait then sleep(0.8) else break end end
- saveState()
- end
- end)
- return filled
- end
- --========================
- -- Disk reading (mounted /disk*, side-agnostic)
- --========================
- local function readAnyDiskJSON(possibleFiles)
- for _,mount in ipairs({"/disk","/disk2","/disk3"}) do
- if fs.exists(mount) and fs.isDir(mount) then
- for __,fp in ipairs(possibleFiles) do
- local p=mount.."/"..fp
- if fs.exists(p) then
- local d=loadFileJSON(p); if type(d)=="table" then return d end
- end
- end
- end
- end
- return nil
- end
- local function readStationConfigBlack() return readAnyDiskJSON({"station.json"}) end
- local function readStationConfigGray() return readAnyDiskJSON({"station.optrefuel.json"}) end
- --========================
- -- Fuel scan + arrivals logging
- --========================
- local function scanFuelInventory()
- local coal,blocks,lava,pot=0,0,0,0
- for s=1,16 do local det=turtle.getItemDetail(s)
- if det then
- if det.name=="minecraft:coal_block" then blocks=blocks+(det.count or 0); pot=pot+(det.count or 0)*800
- elseif det.name=="minecraft:coal" then coal=coal+(det.count or 0); pot=pot+(det.count or 0)*80
- elseif det.name=="minecraft:lava_bucket" then lava=lava+(det.count or 0); pot=pot+(det.count or 0)*1000 end
- end
- end
- return coal,blocks,lava,pot
- end
- local function getTopMonitor() if not peripheral or not peripheral.getType then return nil end local t=peripheral.getType("top"); if t=="monitor" then return peripheral.wrap("top") end return nil end
- local function monWriteLine(mon,line) if not mon then return end local w,h=mon.getSize(); local x,y=mon.getCursorPos(); if y>=h then mon.scroll(1); y=h; mon.setCursorPos(1,y) end mon.write(line); if y<h then mon.setCursorPos(1,y+1) else mon.setCursorPos(1,h) end end
- local function logArrival(nodeType)
- local fuel=turtle.getFuelLevel(); local limit=turtle.getFuelLimit()
- local coal,blocks,lava,pot=scanFuelInventory()
- local carried=("carried fuel: coal=%d, blocks=%d%s, potential=%d"):format(coal,blocks,(lava>0 and (", lava="..lava) or ""),pot)
- logLine(("Arrived at %s | fuel=%s/%s | %s | rev=%s"):format(nodeType, tostring(fuel), tostring(limit), carried, tostring(state.pose.reversed)))
- end
- --========================
- -- Station/refuel logic (with monitor mirroring)
- --========================
- local function doBlackStation()
- logCtx.mon = getTopMonitor()
- logArrival("station")
- local cfg = readStationConfigBlack()
- if not cfg or not cfg.stationType then logLine("Station: no config; skipping."); logCtx.mon=nil; return end
- if cfg.stationType=="dropoff" then
- if not cfg.owner then logLine("Drop-off: missing 'owner' in floppy."); logCtx.mon=nil; return end
- local moved,freed,senders = dumpAllForOwner(cfg.owner)
- local parts={} for who,cnt in pairs(senders) do parts[#parts+1]=(who..":"..cnt) end table.sort(parts)
- logLine(("Drop-off @ %s | owner=%s | items=%d | freedSlots=%d | from={%s}"):format(cfg.stationId or "unknown", cfg.owner, moved, freed, table.concat(parts,", ")))
- refreshSlotCounts(); saveState()
- elseif cfg.stationType=="pickup" then
- if not cfg.destination then logLine("Pickup: missing 'destination' in floppy."); logCtx.mon=nil; return end
- if countFreeCargoSlots()==0 then logLine("Pickup skipped: inventory full."); logCtx.mon=nil; return end
- local sender = cfg.owner or (cfg.stationId or "unknown")
- local picked,reserved = pickupAllForDestination(cfg.destination, sender)
- logLine(("Pickup @ %s | sender=%s | dest=%s | items=%d | reservedSlots=%d"):format(cfg.stationId or "unknown", sender, cfg.destination, picked, reserved))
- refreshSlotCounts(); saveState()
- else
- logLine("Station: unknown type '"..tostring(cfg.stationType).."'; skipping.")
- end
- logCtx.mon=nil
- end
- local function doOrangeRefuel()
- logCtx.mon = getTopMonitor()
- logArrival("routeRefuel")
- local lim=turtle.getFuelLimit(); if lim=="unlimited" then logLine("Refuel: unlimited fuel mode; skipping."); logCtx.mon=nil; return end
- local before=turtle.getFuelLevel()
- local gained=refuelFromSideBestEffort(lim, true, {["minecraft:coal"]=true,["minecraft:coal_block"]=true})
- local after=turtle.getFuelLevel()
- logLine(("Refuel complete | +%d fuel | %d → %d"):format(gained,before,after)); saveState()
- logCtx.mon=nil
- end
- local function doGrayRefuel()
- logCtx.mon = getTopMonitor()
- logArrival("optRefuel")
- local cfg=readStationConfigGray() or {}
- local lim=turtle.getFuelLimit(); if lim=="unlimited" then logLine("OptRefuel: unlimited fuel mode; skipping."); logCtx.mon=nil; return end
- local target=tonumber(cfg.take) or lim
- local must = (cfg.waitForQuota==true)
- local acceptSet = {}
- if type(cfg.accept)=="table" then for _,n in ipairs(cfg.accept) do acceptSet[n]=true end
- else acceptSet={["minecraft:coal"]=true,["minecraft:coal_block"]=true} end
- local before=turtle.getFuelLevel()
- local gained=refuelFromSideBestEffort(target, must, acceptSet)
- local after=turtle.getFuelLevel()
- logLine(("OptRefuel @ %s | target=%d | wait=%s | +%d | %d → %d"):format(cfg.stationId or "unknown", target, tostring(must), gained, before, after)); saveState()
- logCtx.mon=nil
- end
- --========================
- -- Chunkloader bootstrap
- --========================
- local function promptPassword() term.write("ChunkLoader password: "); return read("*") end
- local function startChunkLoader(password)
- local candidates={"chunky","chunkloader",CHUNKY_PATH}
- local prog=nil
- for _,name in ipairs(candidates) do
- local p=shell.resolveProgram(name) or (fs.exists(name) and name or nil)
- if p then prog=p; break end
- end
- if not prog then
- logLine("ChunkLoader helper missing; downloading pastebin 5rNAyGcU ...")
- pcall(function() shell.run("pastebin","get","5rNAyGcU",CHUNKY_PATH) end)
- if fs.exists(CHUNKY_PATH) then prog=CHUNKY_PATH end
- end
- if not prog then logLine("ChunkLoader bootstrap not found and download failed."); return false end
- logLine("Starting ChunkLoader via: "..prog)
- local ok, err = pcall(function() shell.run(prog, password) end)
- if not ok then logLine("ChunkLoader failed: "..tostring(err)); return false end
- logLine("ChunkLoader started.")
- return true
- end
- local function ensureChunkLoaderOrDie()
- local cfg=state.config.chunkloader or {required=true}
- if not cfg.required then return end
- local pw=cfg.password; if not pw or pw=="" then pw=promptPassword() end
- if not pw or pw=="" then shutdownWithReason("ChunkLoader password missing"); return end
- if not startChunkLoader(pw) then shutdownWithReason("ChunkLoader start failed") end
- end
- --========================
- -- Action executors (exact semantics)
- --========================
- local function act_forward() safeForward(); guardRailCheckOrShutdown() end
- local function act_left() turnLeft(); safeForward(); guardRailCheckOrShutdown() end
- local function act_right() turnRight(); safeForward(); guardRailCheckOrShutdown() end
- local function act_climb() moveUp(); safeForward(); guardRailCheckOrShutdown() end
- local function act_descend() safeForward(); guardRailCheckOrShutdown(); moveDown() end
- local function act_station_then_forward() doBlackStation(); safeForward(); guardRailCheckOrShutdown() end
- local function act_routeRefuel_then_forward() doOrangeRefuel(); safeForward(); guardRailCheckOrShutdown() end
- local function act_optRefuel_then_forward() doGrayRefuel(); safeForward(); guardRailCheckOrShutdown() end
- local function act_uturn_toggle_and_forward()
- state.pose.reversed = not state.pose.reversed; saveState()
- logLine("UTURN encountered → reversed="..tostring(state.pose.reversed))
- turnLeft(); turnLeft(); safeForward(); guardRailCheckOrShutdown()
- end
- --========================
- -- Main handler (mapping actions with reversed rules + uturn_*)
- --========================
- local function handleAction(action)
- local rev = state.pose.reversed
- if action=="forward" then
- act_forward()
- elseif action=="left" then
- if rev then act_forward() else act_left() end
- elseif action=="right" then
- if rev then act_forward() else act_right() end
- elseif action=="climb" then
- if rev then act_forward() else act_climb() end
- elseif action=="descend" then
- if rev then act_forward() else act_descend() end
- elseif action=="uturn_left" then
- if rev then act_left() else act_forward() end
- elseif action=="uturn_right" then
- if rev then act_right() else act_forward() end
- elseif action=="uturn_climb" then
- if rev then act_climb() else act_forward() end
- elseif action=="uturn_descend" then
- if rev then act_descend() else act_forward() end
- elseif action=="station" then
- act_station_then_forward()
- elseif action=="routeRefuel" then
- act_routeRefuel_then_forward()
- elseif action=="optRefuel" then
- act_optRefuel_then_forward()
- elseif action=="intersection" then
- -- Same rule in both modes: try Right → Straight → Left
- -- (Note: "right"/"left" here are literal turns from current facing)
- local function tryRight() turnRight(); local ok=safeForward(); if not ok then turnLeft() end return ok end
- local function tryStraight() return safeForward() end
- local function tryLeft() turnLeft(); local ok=safeForward(); if not ok then turnRight() end return ok end
- if not tryRight() then if not tryStraight() then tryLeft() end end
- guardRailCheckOrShutdown()
- elseif action=="uturn" then
- act_uturn_toggle_and_forward()
- else
- -- Unknown tile at start: attempt forward (guard-rail will catch if truly unknown)
- act_forward()
- end
- end
- --========================
- -- Boot + main loop
- --========================
- local function main()
- loadState()
- trackmap = tryLoadTrackmap()
- pcall(tryGPS) -- best-effort initial GPS snapshot
- ensureChunkLoaderOrDie()
- if shuttingDown then return end
- local tick=0
- while true do
- local action = resolveActionUnder()
- handleAction(action)
- tick = tick + 1
- if tick%10==0 then refreshSlotCounts(); saveState() end
- end
- end
- withTerminateTrap(main)
Add Comment
Please, Sign In to add comment