StiGMaT14

StiGMaToPiA MailTurtle v1.4.4 - [ALPHA]

Sep 10th, 2025 (edited)
63
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 32.24 KB | None | 0 0
  1. --[[
  2. StiGMaToPiA MailTurtle v1.4.3  (MC 1.12.2 • ComputerCraft classic 1.8pr1)
  3.  
  4. PURPOSE
  5.   A data-driven “mailman” turtle that follows a colored-wool track, visits stations,
  6.   picks up parcels for destinations, drops off by owner, refuels at route depots, and
  7.   persists state so it can resume after server restarts. Multiple turtles can share
  8.   the same network.
  9.  
  10. TRACK ACTIONS (data-driven; edit /mailman/trackmap.json to change)
  11.   Semantics (NORMAL mode vs REVERSED mode):
  12.     forward           : FORWARD in both modes
  13.     left              : NORMAL=turn LEFT then FORWARD;      REVERSED=FORWARD
  14.     right             : NORMAL=turn RIGHT then FORWARD;     REVERSED=FORWARD
  15.     climb             : NORMAL=UP 1 then FORWARD;           REVERSED=FORWARD
  16.     descend           : NORMAL=FORWARD 1 then DOWN 1;       REVERSED=FORWARD
  17.     uturn_left        : NORMAL=FORWARD;                     REVERSED=turn LEFT then FORWARD
  18.     uturn_right       : NORMAL=FORWARD;                     REVERSED=turn RIGHT then FORWARD
  19.     uturn_climb       : NORMAL=FORWARD;                     REVERSED=UP 1 then FORWARD
  20.     uturn_descend     : NORMAL=FORWARD;                     REVERSED=DOWN 1 then FORWARD
  21.     station           : same in both, but mailbox/fuel SIDES invert in REVERSED mode
  22.     routeRefuel       : same in both, SIDES invert in REVERSED mode
  23.     optRefuel         : same in both, SIDES invert in REVERSED mode
  24.     intersection      : SAME rule in both modes → Try Right → Straight → Left
  25.     uturn             : toggle reversed-mode (persist), 180° turn, then FORWARD
  26.  
  27.   • GUARDED FORWARD (unchanged intent, kept):
  28.       After ANY forward step, if the landing tile has no recognized marker, we LOG and SHUTDOWN:
  29.         "TurtleMail, Shutdown: Movement marker undetected"
  30.  
  31.   • SHUTDOWN LOGGING (kept):
  32.       On shutdown/stop we log current + starting position (GPS if available; else relative odometry).
  33.  
  34.   • STATION MONITOR MIRROR (kept):
  35.       If a (Advanced) Monitor block is directly ABOVE on station/routeRefuel/optRefuel, logs mirror to it.
  36.  
  37.   • CHUNKLOADER HANDSHAKE (kept):
  38.      - If `state.config.chunkloader.required` is true, we must successfully start the Chunk Loader before running.
  39.      - A password can be provided in `state.config.chunkloader.password`; if absent, we prompt at startup.
  40.      - We try to run a local program named "chunky" or "chunkloader". If missing, we attempt:
  41.           shell.run("pastebin","get","5rNAyGcU","/mailman/chunky.lua")
  42.        then run it with the provided password. If any step fails → log and shutdown.
  43.      - (Reference the user-provided pastebin ID: 5rNAyGcU.)
  44.  
  45. DEFAULT TRACK MAP (auto-written to /mailman/trackmap.json if missing)
  46.   - Unused wool colors:
  47.   -- Black wool (15)
  48.   -- Purple wool (10)
  49.   {
  50.     "forward":       [ {"name":"minecraft:cobblestone"}, {"name":"minecraft:wool","meta":5} ], -- Cobblestone or Lime wool
  51.     "left":          [ {"name":"minecraft:wool","meta":11} ], -- Blue wool
  52.     "right":         [ {"name":"minecraft:wool","meta":14} ], -- Red wool
  53.     "intersection":  [ {"name":"minecraft:wool","meta":9} ],  -- Cyan wool
  54.     "station":       [ {"name":"minecraft:wool","meta":13} ], -- Green wool
  55.     "climb":         [ {"name":"minecraft:wool","meta":4} ],  -- Yellow wool
  56.     "descend":       [ {"name":"minecraft:wool","meta":8} ],  -- Light Gray wool
  57.     "routeRefuel":   [ {"name":"minecraft:wool","meta":7} ],  -- Gray wool
  58.     "optRefuel":     [ {"name":"minecraft:wool","meta":1} ],  -- Orange wool
  59.     "uturn":         [ {"name":"minecraft:wool","meta":12} ], -- Brown wool
  60.     "uturn_left":    [ {"name":"minecraft:wool","meta":2} ],  -- Magenta wool
  61.     "uturn_right":   [ {"name":"minecraft:wool","meta":3} ],  -- Light blue wool
  62.     "uturn_climb":   [ {"name":"minecraft:wool","meta":6} ],  -- Pink wool
  63.     "uturn_descend": [ {"name":"minecraft:wool","meta":10} ]   -- Purple wool
  64.   }
  65.  
  66. FLOPPY JSON (unchanged)
  67.   • Drop-off:
  68.       {
  69.         "stationType":"dropoff",
  70.         "stationId":"Hannah-Inbox",
  71.         "owner":"Hannah"
  72.       }
  73.   • Pickup:   (OPTIONAL "owner" lets logs show sender nicely)
  74.       {
  75.         "stationType":"pickup",
  76.         "stationId":"Hannah->StiGMaT",
  77.         "destination":"StiGMaT",
  78.         "owner":"Hannah"
  79.       }
  80.   • Optional Refuel (gray):
  81.       {
  82.         "stationType":"optRefuel",
  83.         "stationId":"Hannah-Refuel",
  84.         "take":2000, "waitForQuota":false,
  85.         "accept":["minecraft:coal","minecraft:coal_block"]
  86.       }
  87.  
  88. STATE FILE (/mailman/state.json), version=2:
  89.   {
  90.     "version": 2,
  91.     "turtleId": "...",
  92.     "slots": { "1": {"dest":"StiGMaT","count":12,"from":"Hannah"}, ... },
  93.     "stats": { "delivered":0, "picked":0 },
  94.     "config": {
  95.       "maxFuelSlots": 2,
  96.       "collisionJitter": [0.15,0.25],
  97.       "log": { "enabled": true, "toFile": false, "maxFileKB": 128 },
  98.       "chunkloader": { "required": true, "password": null, "programHint": "chunky|chunkloader|/mailman/chunky.lua" }
  99.     },
  100.     "pose": {
  101.       "hasGPS": false,
  102.       "startAbs": {"x":null,"y":null,"z":null},
  103.       "startRel": {"x":0,"y":0,"z":0},
  104.       "abs": {"x":null,"y":null,"z":null},
  105.       "rel": {"x":0,"y":0,"z":0},
  106.       "heading": 0,
  107.       "reversed": false
  108.     }
  109.   }
  110. --]]
  111.  
  112. --========================
  113. -- Tiny JSON shim
  114. --========================
  115. local function hasJSON()
  116.   return type(textutils)=="table" and type(textutils.serializeJSON)=="function" and type(textutils.unserializeJSON)=="function"
  117. end
  118.  
  119. local function escape_str(s)
  120.   s = s:gsub('\\','\\\\'):gsub('"','\\"'):gsub('\n','\\n'):gsub('\r','\\r'):gsub('\t','\\t')
  121.   return '"'..s..'"'
  122. end
  123.  
  124. local function encode_json(v)
  125.   local t = type(v)
  126.   if t=="nil" then return "null"
  127.   elseif t=="boolean" then return v and "true" or "false"
  128.   elseif t=="number" then return tostring(v)
  129.   elseif t=="string" then return escape_str(v)
  130.   elseif t=="table" then
  131.     local isArray, n = true, 0
  132.     for k,_ in pairs(v) do if type(k)~="number" then isArray=false break end if k>n then n=k end end
  133.     if isArray then
  134.       local parts = {}
  135.       for i=1,n do parts[#parts+1] = encode_json(v[i]) end
  136.       return "["..table.concat(parts,",").."]"
  137.     else
  138.       local parts = {}
  139.       for k,val in pairs(v) do parts[#parts+1] = escape_str(tostring(k))..":"..encode_json(val) end
  140.       return "{"
  141.         ..table.concat(parts,",")
  142.         .."}"
  143.     end
  144.   else return "null" end
  145. end
  146.  
  147. local function decode_json(s)
  148.   if hasJSON() then return textutils.unserializeJSON(s) end
  149.   error("No JSON parser available in this CC build. Please update to 1.8+ or enable JSON.")
  150. end
  151.  
  152. --========================
  153. -- Paths & initial state
  154. --========================
  155. local STATE_PATH     = "/mailman/state.json"
  156. local TRACKMAP_PATH  = "/mailman/trackmap.json"
  157. local LOG_PATH       = "/mailman/log.txt"
  158. local CHUNKY_PATH    = "/mailman/chunky.lua"
  159.  
  160. local state = {
  161.   version = 2,
  162.   turtleId = tostring(os.getComputerID() or os.getComputerLabel() or "unknown"),
  163.   slots = {},
  164.   stats = { delivered=0, picked=0 },
  165.   config = {
  166.     maxFuelSlots = 2,
  167.     collisionJitter = {0.15,0.25},
  168.     log = { enabled = true, toFile = false, maxFileKB = 128 },
  169.     chunkloader = { required = true, password = nil, programHint = "chunky|chunkloader|/mailman/chunky.lua" }
  170.   },
  171.   pose = {
  172.     hasGPS = false,
  173.     startAbs = {x=nil,y=nil,z=nil}, startRel={x=0,y=0,z=0},
  174.     abs      = {x=nil,y=nil,z=nil}, rel     ={x=0,y=0,z=0},
  175.     heading=0,
  176.     reversed=false
  177.   }
  178. }
  179.  
  180. local DEFAULT_TRACKMAP = {
  181.   forward      = { {name="minecraft:cobblestone"}, {name="minecraft:wool", meta=5} },
  182.   left         = { {name="minecraft:wool", meta=11} },
  183.   right        = { {name="minecraft:wool", meta=14} },
  184.   intersection = { {name="minecraft:wool", meta=9} },
  185.   station      = { {name="minecraft:wool", meta=13} },
  186.   climb        = { {name="minecraft:wool", meta=4} },
  187.   descend      = { {name="minecraft:wool", meta=8} },
  188.   routeRefuel  = { {name="minecraft:wool", meta=7} },
  189.   optRefuel    = { {name="minecraft:wool", meta=1} },
  190.   uturn        = { {name="minecraft:wool", meta=12} },
  191.   uturn_left   = { {name="minecraft:wool", meta=2} },
  192.   uturn_right  = { {name="minecraft:wool", meta=3} },
  193.   uturn_climb  = { {name="minecraft:wool", meta=6} },
  194.   uturn_descend= { {name="minecraft:wool", meta=10} }
  195. }
  196.  
  197. local trackmap = nil
  198. local shuttingDown = false
  199. local movedThisTick = false
  200.  
  201. --========================
  202. -- FS helpers
  203. --========================
  204. local function ensureDir(path)
  205.   local dir = fs.getDir(path)
  206.   if dir ~= "" and not fs.exists(dir) then fs.makeDir(dir) end
  207. end
  208.  
  209. local function loadFileJSON(path)
  210.   if not fs.exists(path) then return nil end
  211.   local f=fs.open(path,"r"); if not f then return nil end
  212.   local txt=f.readAll(); f.close()
  213.   local ok, data = pcall(decode_json, txt)
  214.   if ok then return data end
  215.   return nil
  216. end
  217.  
  218. local function saveFileJSON(path, tbl)
  219.   ensureDir(path)
  220.   local f=fs.open(path,"w"); if not f then return end
  221.   local body = hasJSON() and textutils.serializeJSON(tbl,true) or encode_json(tbl)
  222.   f.write(body); f.close()
  223. end
  224.  
  225. local function loadState()
  226.   local s = loadFileJSON(STATE_PATH)
  227.   if type(s)=="table" then
  228.     for k,v in pairs(s) do state[k]=v end
  229.     if not state.pose then
  230.       state.pose = { hasGPS=false, startAbs={x=nil,y=nil,z=nil}, startRel={x=0,y=0,z=0},
  231.                      abs={x=nil,y=nil,z=nil}, rel={x=0,y=0,z=0}, heading=0, reversed=false }
  232.     end
  233.     if not state.version or state.version<2 then state.version=2 end
  234.   end
  235. end
  236. local function saveState() saveFileJSON(STATE_PATH, state) end
  237.  
  238. -- Load/augment trackmap: preserve existing, add any missing new keys
  239. 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
  240. local function mergeMissing(dst, defaults)
  241.   for k,v in pairs(defaults) do
  242.     if dst[k]==nil then dst[k]=deepCopy(v)
  243.     elseif type(dst[k])=="table" and type(v)=="table" then
  244.       -- leave existing arrays as-is (don't auto-merge signature lists)
  245.     end
  246.   end
  247. end
  248.  
  249. local function tryLoadTrackmap()
  250.   for _,mount in ipairs({"/disk","/disk2","/disk3"}) do
  251.     local p = mount.."/trackmap.json"
  252.     if fs.exists(p) then
  253.       local t = loadFileJSON(p)
  254.       if type(t)=="table" then mergeMissing(t, DEFAULT_TRACKMAP); return t end
  255.     end
  256.   end
  257.   local t = loadFileJSON(TRACKMAP_PATH)
  258.   if type(t)=="table" then mergeMissing(t, DEFAULT_TRACKMAP); return t end
  259.   saveFileJSON(TRACKMAP_PATH, DEFAULT_TRACKMAP)
  260.   return deepCopy(DEFAULT_TRACKMAP)
  261. end
  262.  
  263. --========================
  264. -- Logging + monitor mirror
  265. --========================
  266. local logCtx = { mon=nil }
  267.  
  268. local function logEnabled() return state.config and state.config.log and state.config.log.enabled end
  269. local function ts()
  270.   local day = (os.day and os.day()) or 0
  271.   local timeStr = (textutils and textutils.formatTime and textutils.formatTime(os.time(), true)) or tostring(os.time())
  272.   return ("[Day %d %s]"):format(day, timeStr)
  273. end
  274.  
  275. local function appendFileLine(path, line)
  276.   local f = fs.open(path, fs.exists(path) and "a" or "w")
  277.   if not f then return end
  278.   f.writeLine(line); f.close()
  279.   local kb = math.floor((fs.getSize(path) or 0)/1024)
  280.   local lim = (state.config.log.maxFileKB or 128)
  281.   if kb > lim then local rf=fs.open(path,"w"); if rf then rf.writeLine("<<LOG ROTATED>>"); rf.close() end end
  282. end
  283.  
  284. local function getTopMonitor()
  285.   if not peripheral or not peripheral.getType then return nil end
  286.   local t = peripheral.getType("top")
  287.   if t=="monitor" then return peripheral.wrap("top") end
  288.   return nil
  289. end
  290.  
  291. local function monWriteLine(mon, line)
  292.   if not mon then return end
  293.   local w,h = mon.getSize()
  294.   local x,y = mon.getCursorPos()
  295.   if y >= h then mon.scroll(1); y=h; mon.setCursorPos(1,y) end
  296.   mon.write(line)
  297.   if y < h then mon.setCursorPos(1, y+1) else mon.setCursorPos(1, h) end
  298. end
  299.  
  300. local function logLine(msg)
  301.   if not logEnabled() then return end
  302.   local line = ts().." "..msg
  303.   print(line)
  304.   if state.config.log.toFile then ensureDir(LOG_PATH); appendFileLine(LOG_PATH, line) end
  305.   if logCtx.mon then monWriteLine(logCtx.mon, line) end
  306. end
  307.  
  308. --========================
  309. -- GPS / Odometry + Shutdown
  310. --========================
  311. local function tryGPS()
  312.   if not gps or not gps.locate then return end
  313.   local x,y,z = gps.locate(1)
  314.   if x and y and z then
  315.     if not state.pose.hasGPS then
  316.       state.pose.hasGPS = true
  317.       state.pose.startAbs = {x=x,y=y,z=z}
  318.     end
  319.     state.pose.abs = {x=x,y=y,z=z}
  320.   end
  321. end
  322.  
  323. local function posStr()
  324.   local rel = state.pose.rel
  325.   local srel = ("rel=(%d,%d,%d)"):format(rel.x,rel.y,rel.z)
  326.   if state.pose.hasGPS and state.pose.abs and state.pose.startAbs then
  327.     local a = state.pose.abs
  328.     local sa = ("abs=(%d,%d,%d)"):format(a.x,a.y,a.z)
  329.     local sa0= ("startAbs=(%d,%d,%d)"):format(state.pose.startAbs.x,state.pose.startAbs.y,state.pose.startAbs.z)
  330.     return sa.." "..srel.." "..sa0
  331.   else
  332.     local s0 = ("startRel=(%d,%d,%d)"):format(state.pose.startRel.x,state.pose.startRel.y,state.pose.startRel.z)
  333.     return srel.." "..s0
  334.   end
  335. end
  336.  
  337. local function shutdownWithReason(reason)
  338.   if shuttingDown then return end
  339.   shuttingDown = true
  340.   logLine("TurtleMail, Shutdown: "..tostring(reason).." | "..posStr())
  341.   saveState()
  342.   sleep(0.2)
  343.   os.shutdown()
  344. end
  345.  
  346. local function withTerminateTrap(fn)
  347.   local ok, err = xpcall(fn, function(e) return e end)
  348.   if not ok then
  349.     if tostring(err or ""):lower():find("terminated") then
  350.       shutdownWithReason("User Terminated")
  351.     else
  352.       logLine("Runtime error: "..tostring(err))
  353.       shutdownWithReason("Unhandled Error")
  354.     end
  355.   end
  356. end
  357.  
  358. --========================
  359. -- Pose wrappers for movement
  360. --========================
  361. local function _advanceRelForward()
  362.   local h = state.pose.heading % 4
  363.   if h==0 then state.pose.rel.z = state.pose.rel.z + 1
  364.   elseif h==1 then state.pose.rel.x = state.pose.rel.x + 1
  365.   elseif h==2 then state.pose.rel.z = state.pose.rel.z - 1
  366.   elseif h==3 then state.pose.rel.x = state.pose.rel.x - 1
  367.   end
  368. end
  369. local function _turnLeftRel()  state.pose.heading = (state.pose.heading + 3) % 4 end
  370. local function _turnRightRel() state.pose.heading = (state.pose.heading + 1) % 4 end
  371. local function _upRel()        state.pose.rel.y = state.pose.rel.y + 1 end
  372. local function _downRel()      state.pose.rel.y = state.pose.rel.y - 1 end
  373.  
  374. local function turnLeft()  turtle.turnLeft();  _turnLeftRel() end
  375. local function turnRight() turtle.turnRight(); _turnRightRel() end
  376. local function moveForward()
  377.   local ok = turtle.forward()
  378.   if ok then _advanceRelForward(); movedThisTick=true; tryGPS() end
  379.   return ok
  380. end
  381. local function moveUp()   local ok=turtle.up();   if ok then _upRel();   tryGPS() end return ok end
  382. local function moveDown() local ok=turtle.down(); if ok then _downRel(); tryGPS() end return ok end
  383.  
  384. --========================
  385. -- Movement + collision
  386. --========================
  387. local function jitterSleep()
  388.   local j = state.config.collisionJitter
  389.   sleep(math.random()*(j[2]-j[1]) + j[1])
  390. end
  391. local function safeForward()
  392.   local tries=0
  393.   while not moveForward() do
  394.     if turtle.detect() then
  395.       tries=tries+1; if tries>10 then return false,"blocked" end
  396.       sleep(0.3)
  397.     else
  398.       jitterSleep()
  399.     end
  400.   end
  401.   return true
  402. end
  403.  
  404. --========================
  405. -- Block probing / action resolve
  406. --========================
  407. local function getUnderInfo()
  408.   local ok, info = turtle.inspectDown()
  409.   if not ok then return nil end
  410.   info.metadata = info.metadata
  411.   if info.state and info.state.color and type(info.state.color)=="string" then
  412.     info._color = tostring(info.state.color)
  413.   end
  414.   return info
  415. end
  416.  
  417. local function matchesSignature(info, sig)
  418.   if not info or not sig or info.name ~= sig.name then return false end
  419.   if sig.meta ~= nil then return (info.metadata == sig.meta) end
  420.   if sig.color ~= nil then return (info._color == sig.color) end
  421.   return true
  422. end
  423.  
  424. -- deterministic order; "uturn_*" included
  425. local ACTION_ORDER = {
  426.   "station","routeRefuel","optRefuel",
  427.   "climb","descend",
  428.   "uturn_climb","uturn_descend",
  429.   "intersection","left","right",
  430.   "uturn_left","uturn_right",
  431.   "uturn",
  432.   "forward"
  433. }
  434.  
  435. local function resolveActionUnder()
  436.   local info = getUnderInfo()
  437.   if not info then return nil end
  438.   for _,action in ipairs(ACTION_ORDER) do
  439.     local sigs = trackmap[action]
  440.     if type(sigs)=="table" then
  441.       for _,sig in ipairs(sigs) do
  442.         if matchesSignature(info, sig) then return action end
  443.       end
  444.     end
  445.   end
  446.   return nil
  447. end
  448.  
  449. --========================
  450. -- Guard rail (after forward step)
  451. --========================
  452. local function guardRailCheckOrShutdown()
  453.   if not movedThisTick then return end
  454.   movedThisTick=false
  455.   local a = resolveActionUnder()
  456.   if not a then shutdownWithReason("Movement marker undetected") end
  457. end
  458.  
  459. --========================
  460. -- Side helpers respecting REVERSED mode
  461. --========================
  462. local function withMailboxSide(fn)
  463.   if not state.pose.reversed then
  464.     turnLeft(); local ok,err=pcall(fn); turnRight(); return ok,err
  465.   else
  466.     turnRight(); local ok,err=pcall(fn); turnLeft(); return ok,err
  467.   end
  468. end
  469. local function withFuelChestSide(fn)
  470.   if not state.pose.reversed then
  471.     turnRight(); local ok,err=pcall(fn); turnLeft(); return ok,err
  472.   else
  473.     turnLeft();  local ok,err=pcall(fn); turnRight(); return ok,err
  474.   end
  475. end
  476.  
  477. --========================
  478. -- Inventory helpers
  479. --========================
  480. local function refuelAllInSelected()
  481.   local before=turtle.getFuelLevel(); turtle.refuel(64); return turtle.getFuelLevel()-before
  482. end
  483. local function countFreeCargoSlots()
  484.   local fuelSlots=state.config.maxFuelSlots or 0
  485.   local lastFuelStart=16-fuelSlots+1
  486.   local free=0
  487.   for s=1,16 do
  488.     local isFuel=(fuelSlots>0 and s>=lastFuelStart)
  489.     if turtle.getItemCount(s)==0 and not isFuel then free=free+1 end
  490.   end
  491.   return free
  492. end
  493. local function nextFuelSlot()
  494.   local fuelSlots=state.config.maxFuelSlots or 0
  495.   if fuelSlots<=0 then return nil end
  496.   local start=16-fuelSlots+1
  497.   for s=start,16 do if turtle.getItemSpace(s)>0 then return s end end
  498.   return nil
  499. end
  500. local function nextCargoSlotFor(dest)
  501.   local fuelSlots=state.config.maxFuelSlots or 0
  502.   local startFuel=16-fuelSlots+1
  503.   for s=1,16 do
  504.     if not (fuelSlots>0 and s>=startFuel) then
  505.       local cnt=turtle.getItemCount(s)
  506.       if cnt>0 and state.slots[tostring(s)] and state.slots[tostring(s)].dest==dest and turtle.getItemSpace(s)>0 then
  507.         return s
  508.       end
  509.     end
  510.   end
  511.   for s=1,16 do
  512.     if not (fuelSlots>0 and s>=startFuel) and turtle.getItemCount(s)==0 then return s end
  513.   end
  514.   return nil
  515. end
  516. local function tagNewlyFilledSlots(dest, sender)
  517.   for s=1,16 do
  518.     local key=tostring(s); local cnt=turtle.getItemCount(s)
  519.     if cnt>0 and (not state.slots[key] or not state.slots[key].dest) then
  520.       state.slots[key]={dest=dest,count=cnt,from=sender}
  521.     elseif cnt>0 and state.slots[key] and state.slots[key].dest==dest then
  522.       state.slots[key].count=cnt; if sender and not state.slots[key].from then state.slots[key].from=sender end
  523.     end
  524.   end
  525. end
  526. local function refreshSlotCounts()
  527.   for s=1,16 do
  528.     local key=tostring(s); local cnt=turtle.getItemCount(s)
  529.     if cnt>0 then
  530.       if not state.slots[key] then state.slots[key]={dest=nil,count=cnt}
  531.       else state.slots[key].count=cnt end
  532.     else state.slots[key]=nil end
  533.   end
  534. end
  535.  
  536. --========================
  537. -- Chest I/O
  538. --========================
  539. local function dumpAllForOwner(ownerName)
  540.   local moved, freed = 0,0
  541.   local senders = {}
  542.   withMailboxSide(function()
  543.     for s=1,16 do
  544.       local tag=state.slots[tostring(s)]; local cnt=turtle.getItemCount(s)
  545.       if tag and tag.dest==ownerName and cnt>0 then
  546.         turtle.select(s); turtle.drop()
  547.         local after=turtle.getItemCount(s)
  548.         local delta=cnt-after
  549.         if delta>0 then
  550.           moved=moved+delta; if after==0 then freed=freed+1 end
  551.           local who=(tag.from or "unknown"); senders[who]=(senders[who] or 0)+delta
  552.           state.slots[tostring(s)] = (after>0) and {dest=tag.dest,count=after,from=tag.from} or nil
  553.         end
  554.       end
  555.     end
  556.   end)
  557.   state.stats.delivered=(state.stats.delivered or 0)+moved
  558.   return moved, freed, senders
  559. end
  560.  
  561. local function pickupAllForDestination(dest, senderName)
  562.   if countFreeCargoSlots()==0 then return 0,0,"full" end
  563.   local beforeCounts={} for s=1,16 do beforeCounts[s]=turtle.getItemCount(s) end
  564.   withMailboxSide(function()
  565.     while countFreeCargoSlots()>0 do
  566.       local slot=nextCargoSlotFor(dest); if not slot then break end
  567.       turtle.select(slot); if not turtle.suck() then break end
  568.       tagNewlyFilledSlots(dest, senderName)
  569.       state.stats.picked=(state.stats.picked or 0)+1
  570.     end
  571.   end)
  572.   local picked=0
  573.   for s=1,16 do local after=turtle.getItemCount(s); local before=beforeCounts[s] or 0
  574.     if after>before then picked=picked+(after-before) end
  575.   end
  576.   local reserved=0
  577.   for s=1,16 do local tag=state.slots[tostring(s)]
  578.     if tag and tag.dest==dest and turtle.getItemCount(s)>0 then reserved=reserved+1 end
  579.   end
  580.   return picked, reserved
  581. end
  582.  
  583. local function refuelFromSideBestEffort(targetLevel, mustWait, acceptNames)
  584.   acceptNames = acceptNames or {["minecraft:coal"]=true,["minecraft:coal_block"]=true}
  585.   local function isAccepted(detail) return detail and acceptNames[detail.name] end
  586.   local filled=0
  587.   withFuelChestSide(function()
  588.     local function getFuel() return turtle.getFuelLevel()=="unlimited" and math.huge or turtle.getFuelLevel() end
  589.     local function getLimit() return turtle.getFuelLimit()=="unlimited" and math.huge or turtle.getFuelLimit() end
  590.     local limit=getLimit(); targetLevel=math.min(targetLevel or limit, limit)
  591.     local function tryOnce()
  592.       if getFuel()>=targetLevel then return true end
  593.       local fslt=nextFuelSlot(); if not fslt then return false end
  594.       turtle.select(fslt); if not turtle.suck() then return false end
  595.       local det=turtle.getItemDetail(fslt)
  596.       if not isAccepted(det) then turtle.drop(); return false end
  597.       local gained=refuelAllInSelected(); filled=filled+gained
  598.       return getFuel()>=targetLevel
  599.     end
  600.     while getFuel()<targetLevel do
  601.       if not tryOnce() then if mustWait then sleep(0.8) else break end end
  602.       saveState()
  603.     end
  604.   end)
  605.   return filled
  606. end
  607.  
  608. --========================
  609. -- Disk reading (mounted /disk*, side-agnostic)
  610. --========================
  611. local function readAnyDiskJSON(possibleFiles)
  612.   for _,mount in ipairs({"/disk","/disk2","/disk3"}) do
  613.     if fs.exists(mount) and fs.isDir(mount) then
  614.       for __,fp in ipairs(possibleFiles) do
  615.         local p=mount.."/"..fp
  616.         if fs.exists(p) then
  617.           local d=loadFileJSON(p); if type(d)=="table" then return d end
  618.         end
  619.       end
  620.     end
  621.   end
  622.   return nil
  623. end
  624.  
  625. local function readStationConfigBlack() return readAnyDiskJSON({"station.json"}) end
  626. local function readStationConfigGray()  return readAnyDiskJSON({"station.optrefuel.json"}) end
  627.  
  628. --========================
  629. -- Fuel scan + arrivals logging
  630. --========================
  631. local function scanFuelInventory()
  632.   local coal,blocks,lava,pot=0,0,0,0
  633.   for s=1,16 do local det=turtle.getItemDetail(s)
  634.     if det then
  635.       if det.name=="minecraft:coal_block" then blocks=blocks+(det.count or 0); pot=pot+(det.count or 0)*800
  636.       elseif det.name=="minecraft:coal" then coal=coal+(det.count or 0); pot=pot+(det.count or 0)*80
  637.       elseif det.name=="minecraft:lava_bucket" then lava=lava+(det.count or 0); pot=pot+(det.count or 0)*1000 end
  638.     end
  639.   end
  640.   return coal,blocks,lava,pot
  641. end
  642.  
  643. 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
  644. 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
  645.  
  646. local function logArrival(nodeType)
  647.   local fuel=turtle.getFuelLevel(); local limit=turtle.getFuelLimit()
  648.   local coal,blocks,lava,pot=scanFuelInventory()
  649.   local carried=("carried fuel: coal=%d, blocks=%d%s, potential=%d"):format(coal,blocks,(lava>0 and (", lava="..lava) or ""),pot)
  650.   logLine(("Arrived at %s | fuel=%s/%s | %s | rev=%s"):format(nodeType, tostring(fuel), tostring(limit), carried, tostring(state.pose.reversed)))
  651. end
  652.  
  653. --========================
  654. -- Station/refuel logic (with monitor mirroring)
  655. --========================
  656. local function doBlackStation()
  657.   logCtx.mon = getTopMonitor()
  658.   logArrival("station")
  659.   local cfg = readStationConfigBlack()
  660.   if not cfg or not cfg.stationType then logLine("Station: no config; skipping."); logCtx.mon=nil; return end
  661.   if cfg.stationType=="dropoff" then
  662.     if not cfg.owner then logLine("Drop-off: missing 'owner' in floppy."); logCtx.mon=nil; return end
  663.     local moved,freed,senders = dumpAllForOwner(cfg.owner)
  664.     local parts={} for who,cnt in pairs(senders) do parts[#parts+1]=(who..":"..cnt) end table.sort(parts)
  665.     logLine(("Drop-off @ %s | owner=%s | items=%d | freedSlots=%d | from={%s}"):format(cfg.stationId or "unknown", cfg.owner, moved, freed, table.concat(parts,", ")))
  666.     refreshSlotCounts(); saveState()
  667.   elseif cfg.stationType=="pickup" then
  668.     if not cfg.destination then logLine("Pickup: missing 'destination' in floppy."); logCtx.mon=nil; return end
  669.     if countFreeCargoSlots()==0 then logLine("Pickup skipped: inventory full."); logCtx.mon=nil; return end
  670.     local sender = cfg.owner or (cfg.stationId or "unknown")
  671.     local picked,reserved = pickupAllForDestination(cfg.destination, sender)
  672.     logLine(("Pickup @ %s | sender=%s | dest=%s | items=%d | reservedSlots=%d"):format(cfg.stationId or "unknown", sender, cfg.destination, picked, reserved))
  673.     refreshSlotCounts(); saveState()
  674.   else
  675.     logLine("Station: unknown type '"..tostring(cfg.stationType).."'; skipping.")
  676.   end
  677.   logCtx.mon=nil
  678. end
  679.  
  680. local function doOrangeRefuel()
  681.   logCtx.mon = getTopMonitor()
  682.   logArrival("routeRefuel")
  683.   local lim=turtle.getFuelLimit(); if lim=="unlimited" then logLine("Refuel: unlimited fuel mode; skipping."); logCtx.mon=nil; return end
  684.   local before=turtle.getFuelLevel()
  685.   local gained=refuelFromSideBestEffort(lim, true, {["minecraft:coal"]=true,["minecraft:coal_block"]=true})
  686.   local after=turtle.getFuelLevel()
  687.   logLine(("Refuel complete | +%d fuel | %d → %d"):format(gained,before,after)); saveState()
  688.   logCtx.mon=nil
  689. end
  690.  
  691. local function doGrayRefuel()
  692.   logCtx.mon = getTopMonitor()
  693.   logArrival("optRefuel")
  694.   local cfg=readStationConfigGray() or {}
  695.   local lim=turtle.getFuelLimit(); if lim=="unlimited" then logLine("OptRefuel: unlimited fuel mode; skipping."); logCtx.mon=nil; return end
  696.   local target=tonumber(cfg.take) or lim
  697.   local must  = (cfg.waitForQuota==true)
  698.   local acceptSet = {}
  699.   if type(cfg.accept)=="table" then for _,n in ipairs(cfg.accept) do acceptSet[n]=true end
  700.   else acceptSet={["minecraft:coal"]=true,["minecraft:coal_block"]=true} end
  701.   local before=turtle.getFuelLevel()
  702.   local gained=refuelFromSideBestEffort(target, must, acceptSet)
  703.   local after=turtle.getFuelLevel()
  704.   logLine(("OptRefuel @ %s | target=%d | wait=%s | +%d | %d → %d"):format(cfg.stationId or "unknown", target, tostring(must), gained, before, after)); saveState()
  705.   logCtx.mon=nil
  706. end
  707.  
  708. --========================
  709. -- Chunkloader bootstrap
  710. --========================
  711. local function promptPassword() term.write("ChunkLoader password: "); return read("*") end
  712. local function startChunkLoader(password)
  713.   local candidates={"chunky","chunkloader",CHUNKY_PATH}
  714.   local prog=nil
  715.   for _,name in ipairs(candidates) do
  716.     local p=shell.resolveProgram(name) or (fs.exists(name) and name or nil)
  717.     if p then prog=p; break end
  718.   end
  719.   if not prog then
  720.     logLine("ChunkLoader helper missing; downloading pastebin 5rNAyGcU ...")
  721.     pcall(function() shell.run("pastebin","get","5rNAyGcU",CHUNKY_PATH) end)
  722.     if fs.exists(CHUNKY_PATH) then prog=CHUNKY_PATH end
  723.   end
  724.   if not prog then logLine("ChunkLoader bootstrap not found and download failed."); return false end
  725.   logLine("Starting ChunkLoader via: "..prog)
  726.   local ok, err = pcall(function() shell.run(prog, password) end)
  727.   if not ok then logLine("ChunkLoader failed: "..tostring(err)); return false end
  728.   logLine("ChunkLoader started.")
  729.   return true
  730. end
  731. local function ensureChunkLoaderOrDie()
  732.   local cfg=state.config.chunkloader or {required=true}
  733.   if not cfg.required then return end
  734.   local pw=cfg.password; if not pw or pw=="" then pw=promptPassword() end
  735.   if not pw or pw=="" then shutdownWithReason("ChunkLoader password missing"); return end
  736.   if not startChunkLoader(pw) then shutdownWithReason("ChunkLoader start failed") end
  737. end
  738.  
  739. --========================
  740. -- Action executors (exact semantics)
  741. --========================
  742. local function act_forward()            safeForward(); guardRailCheckOrShutdown() end
  743. local function act_left()               turnLeft();  safeForward(); guardRailCheckOrShutdown() end
  744. local function act_right()              turnRight(); safeForward(); guardRailCheckOrShutdown() end
  745. local function act_climb()              moveUp();    safeForward(); guardRailCheckOrShutdown() end
  746. local function act_descend()            safeForward();  guardRailCheckOrShutdown(); moveDown() end
  747. local function act_station_then_forward() doBlackStation(); safeForward(); guardRailCheckOrShutdown() end
  748. local function act_routeRefuel_then_forward() doOrangeRefuel(); safeForward(); guardRailCheckOrShutdown() end
  749. local function act_optRefuel_then_forward()   doGrayRefuel();   safeForward(); guardRailCheckOrShutdown() end
  750. local function act_uturn_toggle_and_forward()
  751.   state.pose.reversed = not state.pose.reversed; saveState()
  752.   logLine("UTURN encountered → reversed="..tostring(state.pose.reversed))
  753.   turnLeft(); turnLeft(); safeForward(); guardRailCheckOrShutdown()
  754. end
  755.  
  756. --========================
  757. -- Main handler (mapping actions with reversed rules + uturn_*)
  758. --========================
  759. local function handleAction(action)
  760.   local rev = state.pose.reversed
  761.   if action=="forward" then
  762.     act_forward()
  763.  
  764.   elseif action=="left" then
  765.     if rev then act_forward() else act_left() end
  766.  
  767.   elseif action=="right" then
  768.     if rev then act_forward() else act_right() end
  769.  
  770.   elseif action=="climb" then
  771.     if rev then act_forward() else act_climb() end
  772.  
  773.   elseif action=="descend" then
  774.     if rev then act_forward() else act_descend() end
  775.  
  776.   elseif action=="uturn_left" then
  777.     if rev then act_left() else act_forward() end
  778.  
  779.   elseif action=="uturn_right" then
  780.     if rev then act_right() else act_forward() end
  781.  
  782.   elseif action=="uturn_climb" then
  783.     if rev then act_climb() else act_forward() end
  784.  
  785.   elseif action=="uturn_descend" then
  786.     if rev then act_descend() else act_forward() end
  787.  
  788.   elseif action=="station" then
  789.     act_station_then_forward()
  790.  
  791.   elseif action=="routeRefuel" then
  792.     act_routeRefuel_then_forward()
  793.  
  794.   elseif action=="optRefuel" then
  795.     act_optRefuel_then_forward()
  796.  
  797.   elseif action=="intersection" then
  798.     -- Same rule in both modes: try Right → Straight → Left
  799.     -- (Note: "right"/"left" here are literal turns from current facing)
  800.     local function tryRight() turnRight(); local ok=safeForward(); if not ok then turnLeft() end return ok end
  801.     local function tryStraight() return safeForward() end
  802.     local function tryLeft()  turnLeft();  local ok=safeForward(); if not ok then turnRight() end return ok end
  803.     if not tryRight() then if not tryStraight() then tryLeft() end end
  804.     guardRailCheckOrShutdown()
  805.  
  806.   elseif action=="uturn" then
  807.     act_uturn_toggle_and_forward()
  808.  
  809.   else
  810.     -- Unknown tile at start: attempt forward (guard-rail will catch if truly unknown)
  811.     act_forward()
  812.   end
  813. end
  814.  
  815. --========================
  816. -- Boot + main loop
  817. --========================
  818. local function main()
  819.   loadState()
  820.   trackmap = tryLoadTrackmap()
  821.   pcall(tryGPS)           -- best-effort initial GPS snapshot
  822.   ensureChunkLoaderOrDie()
  823.   if shuttingDown then return end
  824.  
  825.   local tick=0
  826.   while true do
  827.     local action = resolveActionUnder()
  828.     handleAction(action)
  829.     tick = tick + 1
  830.     if tick%10==0 then refreshSlotCounts(); saveState() end
  831.   end
  832. end
  833.  
  834. withTerminateTrap(main)
Add Comment
Please, Sign In to add comment