karelvysinka

lama 1

Feb 12th, 2017
240
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 72.18 KB | None | 0 0
  1. --[[
  2.  
  3.     IMPORTANT: don't use this program. Aside from the fact that it may be broken
  4.     according to some people, ComputerCraft has currently a bug that can deadlock
  5.     the game when a turtle moves next to a disk drive. And that happens a lot with
  6.     JAM. Turtles moving next to a disk drive, I mean. So the chances are actually
  7.     pretty high for this to happen. Trust me, it does happen.
  8.  
  9. ]]
  10.  
  11. print("Unusable due to a CC bug as it stands (CC 1.56). Remove this code line to use it anyway at your own peril.") return
  12.  
  13.  
  14. --[[
  15.     JAM - Just Another Miner - 2013 Sangar
  16.  
  17.     This program is licensed under the MIT license.
  18.     http://opensource.org/licenses/mit-license.php
  19.  
  20.     This program is intended to run on turtles working in a very specific
  21.     environment, to collaboratively (optional) dig up blocks of interest in a
  22.     designated area. Yes, it's a mining program ;)
  23.  
  24.     Features:
  25.       - Fully resumable, thanks to LAMA and my state API. This resume is *very*
  26.         stable. It even survived multiple game crashes (out of memory).
  27.       - Interactively configurable (quarry size, torch placement).
  28.       - Supports turtles cooperatively working on the same quarry, up to the
  29.         total number of holes being dug - one turtle per hole, max. If there's
  30.         fewer turtles than holes, the holes will be worked on sequentially.
  31.       - Adding new turtles is super-easy, just place them into the dock, facing
  32.         the fuel chest (away from the disk drive) and the environment will be
  33.         installed on it and executed.
  34.  
  35.     To get started, set up the docking station like this (top-down view).
  36.  
  37.          CF       Where: CD = Chest for dropping found blocks.
  38.       CD TU CT           CF = Chest with fuel.
  39.          DD              CT = Chest with torches (only needed when used).
  40.                          DD = A disk drive with an empty floppy inside.
  41.                          TU = A turtle, looking at the fuel chest.
  42.  
  43.     Make sure the turtle in the middle is facing the fuel chest, then install
  44.     the environment like this:
  45.         > pastebin get h9KJ4DRt jam-install
  46.         > jam-install
  47.  
  48.     The program will guide you through the next steps. Always make sure there's
  49.     enough fuel (and if used, torches) in the chests, and empty the dropbox
  50.     regularly!
  51.  
  52.     To add more turtles to an existing quarry, simply place them into the
  53.     docking station like you did the first turtle, i.e. facing the fuel chest.
  54.     It will automatically be configured by the program written onto the floppy
  55.     disk when the quarry was set up and then run this program.
  56. ]]
  57.  
  58. assert(os.loadAPI("apis/bapil"))
  59. assert(os.loadAPI("apis/logger"))
  60. assert(os.loadAPI("apis/startup"))
  61. assert(os.loadAPI("apis/state"))
  62. assert(os.loadAPI("apis/lama"))
  63.  
  64. -------------------------------------------------------------------------------
  65. -- Semi-config (you normally won't want to touch these)                      --
  66. -------------------------------------------------------------------------------
  67.  
  68. -- The channel on which we'll send out status messages.
  69. local sendChannel = 10113
  70.  
  71. -- Every how many levels torch placing turtles should place a torch.
  72. local torchFrequency = 8
  73.  
  74. -- Whether to write our progress into a log file (each time the program changes
  75. -- its state it would log that). This is primarily intended for debugging.
  76. local writeLog = false
  77.  
  78. -- File in which to store current program state on the turtle, for resuming the
  79. -- program after a reboot.
  80. local stateFile = "/.jam-state"
  81.  
  82. -- The path to the file on the disk drive which we use to track how many jobs
  83. -- we have already assigned to turtles. Note that this will be prefixed with
  84. -- the disk drives mount path, so it should be a relative path.
  85. local jobFile = ".jobs"
  86.  
  87. -- Given the turtle is in it's native orientation, this is the side the chest
  88. -- we drop stuff we found into.
  89. local dropSide = lama.side.left
  90.  
  91. -- Given the turtle is in it's native orientation, this is the side the chest
  92. -- we can get fuel from.
  93. local fuelSide = lama.side.front
  94.  
  95. -- Given the turtle is in it's native orientation, this is the side the chest
  96. -- we can get new torches from.
  97. local torchSide = lama.side.right
  98.  
  99. -- Given the turtle is in it's native orientation, this is the side the floppy
  100. -- disk drive we use to track job progress on is at.
  101. local diskSide = lama.side.back
  102.  
  103. -------------------------------------------------------------------------------
  104. -- Internal variables / constants. DO NOT CHANGE THESE.                      --
  105. -------------------------------------------------------------------------------
  106.  
  107. -- The slot number in which we keep our torches.
  108. local torchSlot = 16
  109.  
  110. -- The relative level at which to move away from the dock, towards the holes.
  111. local awayLevel = 1
  112.  
  113. -- The relative level at which to move back from the holes, towards the dock.
  114. local backLevel = -1
  115.  
  116. -- Relative level at which to start digging.
  117. local jobLevel = -2
  118.  
  119. -- Same as diskSide but using redstone.getSides() constants (for disk API).
  120. local rsDiskSide = ({[lama.side.forward] = "front",
  121.                      [lama.side.right]   = "right",
  122.                      [lama.side.back]    = "back",
  123.                      [lama.side.left]    = "left"})[diskSide]
  124.  
  125. -- The log we use. Print to screen if we're not supposed to log stuff.
  126. local log = writeLog and logger.new("jam") or
  127.             {info = function(_, fmt, ...)
  128.                         print("[INFO] " .. string.format(fmt, ...))
  129.                     end,
  130.              warn = function(_, fmt, ...)
  131.                         print("[WARNING] " .. string.format(fmt, ...))
  132.                     end}
  133.  
  134. -- Forward declaration of namespaces.
  135. local private
  136.  
  137. -------------------------------------------------------------------------------
  138. -- Program states                                                            --
  139. -------------------------------------------------------------------------------
  140.  
  141. -- Start assembling our state machine.
  142. local program = state.new(stateFile)
  143.  
  144. --[[
  145.     This state is purely used for interactively setting up a dig site. It asks
  146.     the user a couple of questions, prepares the disk drive and then reboots
  147.     into the actual worker loop.
  148. ]]
  149. :add("setup", function()
  150.     -- Make sure there's a disk drive behind us.
  151.     if not disk.isPresent(rsDiskSide) or not disk.getMountPath(rsDiskSide) then
  152.         lama.turn((diskSide + 2) % 4)
  153.     end
  154.     if not private.waitForDiskDrive(0.5) then
  155.         return private.showBlueprint()
  156.     end
  157.  
  158.     -- Configure the turtle to use our local coordinate system.
  159.     lama.set(0, 0, 0, lama.side.north)
  160.  
  161.     -- If there's no job file on the disk drive we're starting from scratch.
  162.     local diskPath = disk.getMountPath(rsDiskSide)
  163.     local jobFileDisk = fs.combine(diskPath, jobFile)
  164.     assert(not fs.exists(jobFileDisk) or not fs.isDir(jobFileDisk),
  165.         "Bad disk, folder at job file location.")
  166.     if not fs.exists(jobFileDisk) then
  167.         assert(shell, "Setup must be run from the shell.")
  168.         local jobCount, placeTorches,
  169.               placeIgnored, haveIgnoreChest = private.setupDigSite()
  170.         if jobCount == nil then
  171.             -- nil is the abort signal, restart the state.
  172.             return
  173.         end
  174.  
  175.         -- Check for chests.
  176.         print("Checking for chests...")
  177.         lama.turn(dropSide)
  178.         assert(turtle.detect(),
  179.             "Bad setup detected, no drop chest found.")
  180.         lama.turn(fuelSide)
  181.         assert(turtle.detect(),
  182.             "Bad setup detected, no fuel chest found.")
  183.         if placeTorches or haveIgnoreChest then
  184.             lama.turn(torchSide)
  185.             assert(turtle.detect(),
  186.                 "Bad setup detected, no torch/ignored blocks chest found.")
  187.         end
  188.  
  189.         -- Ask for ignored blocks in ignore chest before getting started.
  190.         if haveIgnoreChest then
  191.             private.showIgnoreChestInfo(placeIgnored)
  192.         end
  193.  
  194.         -- If all went well, write the job file to the disk so that all turtles
  195.         -- placed into the dock from now on will be initialized automatically.
  196.         term.clear()
  197.         term.setCursorPos(1, 1)
  198.         print("Writing settings to disk... ")
  199.  
  200.         -- Fetch up-to-date path (may have changed after turning).
  201.         lama.turn((diskSide + 2) % 4)
  202.         private.waitForDiskDrive(0.5)
  203.         local diskPath = disk.getMountPath(rsDiskSide)
  204.         local jobFileDisk = fs.combine(diskPath, jobFile)
  205.         local file = assert(fs.open(jobFileDisk, "w"),
  206.                             "Could not open job file for writing.")
  207.         file.write(textutils.serialize({totalJobs = jobCount,
  208.                                         placeTorches = placeTorches,
  209.                                         placeIgnored = placeIgnored,
  210.                                         haveIgnoreChest = haveIgnoreChest}))
  211.         file.close()
  212.     end
  213.  
  214.     -- Next up: add this turtle as a worker to the quarry.
  215.     switchTo("add_turtle")
  216.  
  217.     -- Finish initialization via disk startup. For example, this will install
  218.     -- this program as an autorun script.
  219.     os.reboot()
  220. end)
  221.  
  222. --[[
  223.     Given an existing dig site adds a new turtle to work on it.
  224. ]]
  225. :add("add_turtle", function()
  226.     -- Align back to face away from the disk drive.
  227.     lama.turn((diskSide + 2) % 4)
  228.     private.waitForDiskDrive(0.5)
  229.     assert(disk.isPresent(rsDiskSide) and disk.getMountPath(rsDiskSide),
  230.         "Bad setup detected, no disk in disk drive behind me!")
  231.  
  232.     -- Copy global quarry settings.
  233.     local jobData = private.readJobData()
  234.     placeIgnored = placeIgnored or jobData.placeIgnored
  235.     placeTorches = placeTorches or jobData.placeTorches
  236.     haveIgnoreChest = haveIgnoreChest or jobData.haveIgnoreChest
  237.  
  238.     -- Ask for the blocks to ignore to be placed in our inventory. Remember how
  239.     -- many block types we're supposed to ignore.
  240.     if haveIgnoreChest then
  241.         ignoreCount = private.setupIgnoredBlocksFromIgnoreChest()
  242.     else
  243.         ignoreCount = private.setupIgnoredBlocks(placeTorches, placeIgnored)
  244.         -- If we got nothing (nil) we're supposed to try again.
  245.         if not ignoreCount then
  246.             return
  247.         end
  248.     end
  249.  
  250.     -- Drop any duplicates we have of the stuff we're supposed to ignore.
  251.     -- Switching to goto_drop is equivalent to drop, but more uniform flow.
  252.     switchTo("goto_drop")
  253. end)
  254.  
  255. --[[
  256.     Go back to the docking station to drop stuff we picked up.
  257. ]]
  258. :add("goto_drop", function()
  259.     log:info("Going home to deliver the goods.")
  260.     private.sendMessage("state", "goto_drop")
  261.  
  262.     private.select(1)
  263.     local x, y, z = lama.get()
  264.     local path = private.generatePath(x, y, z, 0, 0, 0, "home")
  265.     private.forceMove("goto_drop", function()
  266.         return lama.navigate(path, math.huge, true)
  267.     end)
  268.  
  269.     switchTo("drop")
  270. end)
  271.  
  272. --[[
  273.     Actually drop anything in our inventory that's surplus.
  274. ]]
  275. :add("drop", function()
  276.     log:info("Dropping it!")
  277.     private.sendMessage("state", "drop")
  278.  
  279.     if not placeIgnored or not job then
  280.         for slot = 1, ignoreCount do
  281.             if turtle.getItemCount(slot) > 1 then
  282.                 private.drop(slot, 1)
  283.             end
  284.         end
  285.     end
  286.  
  287.     local function isIgnoredBlock(slot)
  288.         if not ignoredLookup then
  289.             return false
  290.         end
  291.         for i = 1, #ignoredLookup do
  292.             if ignoredLookup[i] == slot then
  293.                 return true
  294.             end
  295.         end
  296.         return false
  297.     end
  298.     for slot = ignoreCount + 1, 16 do
  299.         if not placeTorches or slot ~= torchSlot then
  300.             if not isIgnoredBlock(slot) then
  301.                 private.drop(slot)
  302.             end
  303.         end
  304.     end
  305.  
  306.     -- Continue with our current job, or get a new one if we don't have one.
  307.     if job then
  308.         switchTo("refuel_job")
  309.     else
  310.         switchTo("get_job")
  311.         -- Turn back to original orientation.
  312.         lama.turn((diskSide + 2) % 4)
  313.         -- We reboot before each job because this way the Lua VM gets a proper
  314.         -- cleaning (not sure if that's what caused it, but running multiple
  315.         -- turtles for extended periods made my worlds crash), and because I
  316.         -- had a weird bug where the turtle would otherwise not see the disk
  317.         -- inside the disk drive until either rebooted forcefully, or I opened
  318.         -- the menu, shortly. Yes. I know. Totally insane, but 99% reproducible
  319.         -- in that one world.
  320.         os.reboot()
  321.     end
  322. end)
  323.  
  324. --[[
  325.     Tries to get a job, on success refuels and executes it, otherwise refuels
  326.     to move to retirement position (out of the way of other turtles).
  327. ]]
  328. :add("get_job", function()
  329.     log:info("Looking for a new job.")
  330.     private.sendMessage("state", "get_job")
  331.  
  332.     -- Make sure the disk drive is behind us.
  333.     assert(private.waitForDiskDrive(5),
  334.         "Bad setup detected, no disk in disk drive behind me!")
  335.  
  336.     -- Returns a timestamp based on the ingame time (in ticks).
  337.     local function timestamp()
  338.         return (os.day() * 24 + os.time()) * 1000
  339.     end
  340.  
  341.     -- Get the current job information.
  342.     local jobData = private.readJobData()
  343.  
  344.     -- If we finished a job, clear it from the list of active jobs.
  345.     if finishedJob and jobData.activeJobs and
  346.        jobData.activeJobs[finishedJob]
  347.     then
  348.         -- Remember how long the last couple of jobs took to finish. This is
  349.         -- useful for predicting when an active job has taken an unexpectedly
  350.         -- long time and may require the player's investigation (via some
  351.         -- stationary computer attached to the disk drive for example).
  352.         local started = jobData.activeJobs[finishedJob]
  353.         local finished = timestamp()
  354.         local duration = finished - started
  355.         jobData.jobDurations = jobData.jobDurations or {}
  356.         table.insert(jobData.jobDurations, 1, duration)
  357.         if #jobData.jobDurations > 10 then
  358.             table.remove(jobData.jobDurations)
  359.         end
  360.         jobData.activeJobs[finishedJob] = nil
  361.         finishedJob = nil
  362.     end
  363.  
  364.     -- Try to get a new job.
  365.     local nextJob = jobData.nextJob or 1
  366.     if nextJob <= jobData.totalJobs then
  367.         job = nextJob
  368.         jobData.activeJobs = jobData.activeJobs or {}
  369.         jobData.activeJobs[job] = timestamp()
  370.         jobData.nextJob = job + 1
  371.         if job == 1 then
  372.             jobData.startTime = timestamp()
  373.         end
  374.         switchTo("refuel_job")
  375.         log:info("Got one! Our new job ticket is #%d.", job)
  376.         private.sendMessage("job", job)
  377.     else
  378.         endSlot = jobData.finished or 0
  379.         jobData.finished = endSlot + 1
  380.         jobData.stopTime = timestamp()
  381.         switchTo("refuel_end")
  382.     end
  383.  
  384.     -- Write new state back to disk.
  385.     local diskPath = disk.getMountPath(rsDiskSide)
  386.     local jobFileDisk = fs.combine(diskPath, jobFile)
  387.     local file = assert(fs.open(jobFileDisk, "w"),
  388.                   "Bad disk, failed to open job file for writing.")
  389.     file.write(textutils.serialize(jobData))
  390.     file.close()
  391. end)
  392.  
  393. --[[
  394.     Gets enough fuel to get to and back from our job.
  395. ]]
  396. :add("refuel_job", function()
  397.     log:info("Getting enough fuel to make it to our job and back.")
  398.     private.sendMessage("state", "refuel_job")
  399.     private.refuel(private.computeExpeditionCost(job), ignoreCount)
  400.     switchTo("restock_torches")
  401. end)
  402.  
  403. --[[
  404.     Refills our torch stack.
  405. ]]
  406. :add("restock_torches", function()
  407.     if placeTorches then
  408.         log:info("Picking up some torches to bring light into the dark.")
  409.         private.sendMessage("state", "restock_torches")
  410.         private.restockTorches(ignoreCount)
  411.     end
  412.     switchTo("goto_job")
  413. end)
  414.  
  415. --[[
  416.     Moves the turtle to the top of the hole it should dig.
  417. ]]
  418. :add("goto_job", function()
  419.     local tx, tz = private.computeCoordinates(job)
  420.     log:info("On my way to my workplace @ (%d, %d)!", tx, tz)
  421.     private.sendMessage("state", "goto_job")
  422.  
  423.     private.select(1)
  424.     local x, y, z = lama.get()
  425.     local path = private.generatePath(x, y, z, tx, jobLevel, tz, "job")
  426.     private.forceMove("goto_job", function()
  427.         return lama.navigate(path, math.huge, true, true)
  428.     end)
  429.  
  430.     if resumeDigUp then
  431.         switchTo("dig_up")
  432.     else
  433.         switchTo("dig_down")
  434.     end
  435. end)
  436.  
  437. --[[
  438.     Digs a hole down until we hit bedrock, returns to drop stuff if full and
  439.     returns to where we left off.
  440. ]]
  441. :add("dig_down", function()
  442.     log:info("Looking for bedrock, can offer dancing skills.")
  443.     private.sendMessage("state", "dig_down")
  444.  
  445.     if resumeDigDown then
  446.         log:info("Resuming from where I took a break.")
  447.         private.select(1)
  448.         while lama.getY() > resumeDigDown do
  449.             lama.down(math.huge, true)
  450.         end
  451.         resumeDigDown = nil
  452.     end
  453.  
  454.     repeat
  455.         local newSamples = private.digLevel(ignoreCount, ignoreSamples, true)
  456.         if newSamples then
  457.             ignoreSamples = newSamples
  458.             save()
  459.         end
  460.  
  461.         if private.isInventoryFull() then
  462.             -- We may sample the blocks on the level we're on twice (again when
  463.             -- coming back), but that's an acceptable simplification.
  464.             resumeDigDown = lama.getY()
  465.             return switchTo("goto_drop")
  466.         end
  467.  
  468.         private.select(1)
  469.         local brokeStuff, pickedSomethingUp =
  470.             private.suckAndDig(turtle.suckDown, turtle.digDown)
  471.         if (placeIgnored or placeTorches) and
  472.            pickedSomethingUp and not groundLevel
  473.         then
  474.             groundLevel = lama.getY()
  475.             save()
  476.         end
  477.  
  478.         -- Only try to move down if we don't break anything while doing so. If
  479.         -- suckAndDig() may return false even though there is something if our
  480.         -- inventory is full and we should go empty it, so check if there's no
  481.         -- block before we move.
  482.         local couldMove = false
  483.         if brokeStuff or not turtle.detectDown() then
  484.             couldMove = lama.down(math.huge, true)
  485.         end
  486.     until not brokeStuff and not couldMove
  487.  
  488.     switchTo("rebuild_ignore_lookup")
  489. end)
  490.  
  491. --[[
  492.     Sorts the blocks we're supposed to ignore, if any, so that the most
  493.     frequently encountered ones are at the lower inventory positions (for
  494.     faster early exists when comparing).
  495. ]]
  496. :add("rebuild_ignore_lookup", function()
  497.     if placeIgnored and groundLevel then
  498.         log:info("Checking for overflow of ignorance.")
  499.         private.sendMessage("state", "rebuild_ignore_lookup")
  500.  
  501.         -- Swaps two slots in the inventory.
  502.         local function swap(slotA, slotB)
  503.             if slotA == slotB then
  504.                 return
  505.             end
  506.  
  507.             -- Find an empty slot to work with as a temporary buffer.
  508.             local slotTmp
  509.             for slot = ignoreCount + 1, 16 do
  510.                 if turtle.getItemCount(slot) == 0 then
  511.                     slotTmp = slot
  512.                     break
  513.                 end
  514.             end
  515.             if not slotTmp then
  516.                 return false
  517.             end
  518.  
  519.             private.select(slotA)
  520.             turtle.transferTo(slotTmp)
  521.  
  522.             private.select(slotB)
  523.             turtle.transferTo(slotA)
  524.  
  525.             private.select(slotTmp)
  526.             turtle.transferTo(slotB)
  527.  
  528.             return true
  529.         end
  530.  
  531.         -- Build a look-up table of slots that also contain ignored items in
  532.         -- case we dug up so many we had an overflow. This is used for to avoid
  533.         -- having to check the complete inventory when placing ignored blocks
  534.         -- while moving up (which would be horribly slow).
  535.         local count, nextSlot = 0, 16
  536.         local lookup = {}
  537.         for slot = 16, ignoreCount + 1, -1 do
  538.             if turtle.getItemCount(slot) > 0 and
  539.                not placeTorches or slot ~= torchSlot
  540.             then
  541.                 -- Got a candidate, compare it to all known ignored blocks.
  542.                 private.select(slot)
  543.                 for ignoreSlot = 1, ignoreCount do
  544.                     if turtle.compareTo(ignoreSlot) then
  545.                         -- This is an ignored block. Try to push it to the back
  546.                         -- of the inventory, to avoid ripping holes into the
  547.                         -- inventory list when we deplete the stack, which can
  548.                         -- lead to getting two stacks of the same item type of
  549.                         -- dug up blocks (e.g.: coal at 5, additional cobble at
  550.                         -- 4, cobble depletes, new coal found, goes to 4 ->
  551.                         -- both 4 and 5 contain coal, where if cobble were
  552.                         -- behind the coal they'd be merged (assuming what coal
  553.                         -- we had wasn't a full stack already, of course).
  554.                         count = count + turtle.getItemCount(slot)
  555.                         if swap(slot, nextSlot) then
  556.                             table.insert(lookup, 1, nextSlot)
  557.                             nextSlot = nextSlot - 1
  558.                         else
  559.                             table.insert(lookup, 1, slot)
  560.                         end
  561.                         break
  562.                     end
  563.                 end
  564.             end
  565.         end
  566.         if #lookup > 0 then
  567.             ignoredLookup = lookup
  568.         end
  569.         -- Add the counts for the excess items in the stacks of the reference
  570.         -- slots (we only need to keep one of these).
  571.         for slot = 1, ignoreCount do
  572.             count = count + (turtle.getItemCount(slot) - 1)
  573.         end
  574.         -- Remember the level at which we can start placing ignored blocks so
  575.         -- that we have enough up to the surface.
  576.         placeIgnoredLevel = groundLevel - count
  577.     end
  578.  
  579.     switchTo("dig_up")
  580. end)
  581.  
  582. --[[
  583.     Digs back up, picking up whatever isn't ignored. Returns home to drop
  584.     stuff if inventory is full to resume where it we off.
  585. ]]
  586. :add("dig_up", function()
  587.     log:info("Moving up in the world.")
  588.     private.sendMessage("state", "dig_up")
  589.  
  590.     -- Get back to where we stopped if we're resuming our job.
  591.     if resumeDigUp then
  592.         log:info("Resuming from where I took a break.")
  593.         private.select(1)
  594.         while lama.getY() > resumeDigUp do
  595.             lama.down(math.huge, true)
  596.         end
  597.         resumeDigUp = nil
  598.     end
  599.  
  600.     -- Break after block placement to guarantee we always place as necessary.
  601.     while true do
  602.         -- Try placing before moving, so that we can be sure we placed our
  603.         -- thing if we restarted after finishing a move, but before placing.
  604.         if groundLevel and lama.getY() <= groundLevel then
  605.             -- Note that placeIgnored and placeTorches are mutually exclusive.
  606.             if placeIgnored and not placeIgnoredLevel or
  607.                lama.getY() > placeIgnoredLevel
  608.             then
  609.                 -- Once we're above the level we can start placing our ignored
  610.                 -- blocks from we unset the variable to reduce state file size.
  611.                 if placeIgnoredLevel then
  612.                     placeIgnoredLevel = nil
  613.                     save()
  614.                 end
  615.  
  616.                 -- Figure out which slot to place from, starting with overflow
  617.                 -- slots (stored in ignoreLookup, determined in sort state).
  618.                 local slot
  619.                 if ignoredLookup then
  620.                     slot = ignoredLookup[#ignoredLookup]
  621.                 else
  622.                     -- No more lookup-blocks. Draw from our reference stacks.
  623.                     for i = 1, ignoreCount do
  624.                         if turtle.getItemCount(i) > 1 then
  625.                             slot = i
  626.                             break
  627.                         end
  628.                     end
  629.                 end
  630.                 -- If no-one stole items from our inventory and we didn't
  631.                 -- miscalculate we should always have a slot by now.
  632.                 if slot then
  633.                     private.select(slot)
  634.                     turtle.placeDown()
  635.  
  636.                     if ignoredLookup then
  637.                         if turtle.getItemCount(slot) == 0 then
  638.                             table.remove(ignoredLookup)
  639.                         end
  640.                         if #ignoredLookup == 0 then
  641.                             ignoredLookup = nil
  642.                         end
  643.                         save()
  644.                     end
  645.                 else
  646.                     groundLevel = nil
  647.                 end
  648.             elseif placeTorches and lama.getY() % torchFrequency == 0 then
  649.                 private.select(torchSlot)
  650.                 turtle.placeDown()
  651.             end
  652.         else
  653.             -- Reset after coming above ground level, to make the check fail
  654.             -- at the first part and to reduce state file size.
  655.             groundLevel = nil
  656.         end
  657.  
  658.         -- Dig out the level.
  659.         local newSamples = private.digLevel(ignoreCount, ignoreSamples, false)
  660.         if newSamples then
  661.             ignoreSamples = newSamples
  662.             save()
  663.         end
  664.  
  665.         -- Move up until we're back at our starting location.
  666.         if lama.getY() >= jobLevel then
  667.             break
  668.         end
  669.  
  670.         if private.isInventoryFull() then
  671.             -- We may sample the blocks on the level we're on twice (again when
  672.             -- coming back), but that's an acceptable simplification.
  673.             resumeDigUp = lama.getY()
  674.             return switchTo("goto_drop")
  675.         end
  676.  
  677.         private.select(1)
  678.         private.forceMove("dig_up", function()
  679.             return lama.up(math.huge, true)
  680.         end)
  681.     end
  682.  
  683.     finishedJob = job
  684.     groundLevel = nil
  685.     placeIgnoredLevel = nil
  686.     ignoredLookup = nil
  687.     job = nil
  688.     switchTo("goto_drop")
  689. end)
  690.  
  691. --[[
  692.     Gets enough fuel to move to the graveyard.
  693. ]]
  694. :add("refuel_end", function()
  695.     log:info("Getting my last rites. Fuel! I meant fuel.")
  696.     private.sendMessage("state", "refuel_end")
  697.  
  698.     -- Generate the path to our final resting place: two up, n forward and one
  699.     -- to the side (so future finishing turtles can pass us by).
  700.     local x, y, z = lama.get()
  701.     local path = {
  702.         {x = x, y = y, z = z},
  703.         {x = 0, y = 2, z = 0},
  704.         {x = 0, y = 2, z = endSlot},
  705.         {x = 1, y = 2, z = endSlot}
  706.     }
  707.     private.refuel(private.computeFuelNeeded(path), ignoreCount)
  708.     switchTo("clear_inventory")
  709. end)
  710.  
  711. --[[
  712.     Clear our inventory when we're done.
  713. ]]
  714. :add("clear_inventory", function()
  715.     log:info("Clearing out my inventory since I'm done with the world.")
  716.     private.sendMessage("state", "clear_inventory")
  717.  
  718.     for slot = 1, 16 do
  719.         private.drop(slot)
  720.     end
  721.     private.select(1)
  722.     switchTo("goto_end")
  723. end)
  724.  
  725. --[[
  726.     Moves to *in front of* the slot where we'll shut down.
  727. ]]
  728. :add("goto_end", function()
  729.     log:info("Moving out of the way, I'm just useless junk anyway.")
  730.     private.sendMessage("state", "goto_end")
  731.  
  732.     local x, y, z = lama.get()
  733.     local path = private.generatePath(x, y, z, 0, 2, endSlot, "end")
  734.     private.forceMove("goto_end", function()
  735.         return lama.navigate(path, math.huge, true)
  736.     end)
  737.  
  738.     switchTo("end")
  739. end)
  740.  
  741. --[[
  742.     Moves into the actual slot where we'll shut down and then die quietly.
  743. ]]
  744. :add("end", function()
  745.     log:info("That's it, I'm going to end it right here. It was a nice life.")
  746.     private.sendMessage("state", "end")
  747.  
  748.     private.forceMove("end", function()
  749.         return lama.moveto(1, 2, endSlot, lama.side.north, math.huge, true)
  750.     end)
  751.  
  752.     startup.disable("jam")
  753.  
  754.     switchTo(nil)
  755. end)
  756.  
  757. -------------------------------------------------------------------------------
  758. -------------------------------------------------------------------------------
  759. -- Utility functions                                                         --
  760. -------------------------------------------------------------------------------
  761. -------------------------------------------------------------------------------
  762.  
  763. -- Private namespace.
  764. private = {}
  765.  
  766. -------------------------------------------------------------------------------
  767. -- Quarry and turtle setup                                                   --
  768. -------------------------------------------------------------------------------
  769.  
  770. --[[
  771.     Show instructions on how to set up a JAM quarry.
  772. ]]
  773. function private.showBlueprint()
  774.     term.clear()
  775.     term.setCursorPos(1, 1)
  776.     write("To use JAM you need to build a small  \n" ..
  777.           "docking station. Build it like this:  \n" ..
  778.           "                                      \n" ..
  779.           "    CF     Where                      \n" ..
  780.           " CD TU CT  CD: Chest for dropping     \n" ..
  781.           "    DD         found blocks/items.    \n" ..
  782.           "           CF: Chest with turtle fuel.\n" ..
  783.           " CT: Chest with torches or blocks to  \n" ..
  784.           "     ignore (depends on config).      \n" ..
  785.           " DD: Disk drive with an empty disk.   \n" ..
  786.           " TU: This turtle, back to the drive.  \n" ..
  787.           "                                      \n" ..
  788.           "When done, press [Enter] to continue.")
  789.     private.prompt({keys.enter}, {})
  790. end
  791.  
  792. --[[
  793.     Show information about how to configure ignored blocks.
  794. ]]
  795. function private.showIgnoreChestInfo(placeIgnored)
  796.     term.clear()
  797.     term.setCursorPos(1, 1)
  798.     print("Please put the block types that you   \n" ..
  799.           "want to avoid into the torch chest or \n" ..
  800.           "my inventory. Insert as many each, as \n" ..
  801.           "you wish to employ turtles (normally  \n" ..
  802.           "one stack each should be plenty).   \n\n" ..
  803.           "When done, press [Enter] to confirm.\n\n" ..
  804.           "Recommended: Stone, Dirt and possibly \n" ..
  805.           "Gravel or Sand (depends on the biome).\n" ..
  806.           (placeIgnored and
  807.           "You may also want to add Cobblestone, \n" ..
  808.           "to allow placing it while moving up."
  809.           or "" ))
  810.     private.prompt({keys.enter}, {})
  811. end
  812.  
  813. --[[
  814.     Asks the user for the size of the dig site and configures the floppy disk.
  815.  
  816.     This function never returns, it reboots the turtle after it has run.
  817. ]]
  818. function private.setupDigSite()
  819.     -- Label the disk if it hasn't been labeled yet.
  820.     if not disk.getLabel(rsDiskSide) or disk.getLabel(rsDiskSide) == "" then
  821.         disk.setLabel(rsDiskSide, "JAM Job State")
  822.     end
  823.     local diskPath = disk.getMountPath(rsDiskSide)
  824.  
  825.     -- Ask the user how big of a dig site he'd like to create.
  826.     term.clear()
  827.     term.setCursorPos(1, 1)
  828.     print("  Welcome to JAM: Just another Miner!\n\n" ..
  829.           "Please tell me how large an area you  \n" ..
  830.           "want to mine. The area will always be \n" ..
  831.           "squared. Use the arrow keys to adjust \n" ..
  832.           "the size, then press [Enter].         \n")
  833.     local _, y = term.getCursorPos()
  834.     local format = {"  Size: %d (%d chunk, %d hole)\n",
  835.                     "  Size: %d (%d chunks, %d holes)\n"}
  836.     print("\n")
  837.     write("[left/right: one, up/down: five steps]\n\n" ..
  838.           "(Note: chunks refers to the number of \n" ..
  839.           "affected ones, not the worked on area)")
  840.     local size, radius, chunks, jobCount = 1
  841.     repeat
  842.         local rx, ry = term.getCursorPos()
  843.         radius = size * 3
  844.         chunks = math.ceil(radius / 16)
  845.         chunks = chunks * chunks
  846.         jobCount = private.computeJobCount(radius)
  847.         term.setCursorPos(1, y)
  848.         term.clearLine()
  849.         write(string.format(size == 1 and format[1] or format[2],
  850.                             radius, chunks, jobCount))
  851.         -- For termination not f*cking up the shell.
  852.         term.setCursorPos(rx, ry)
  853.  
  854.         local _, code = os.pullEvent("key")
  855.         if code == keys.right then
  856.             -- Let's bound the maximum dig site size to the area of loaded
  857.             -- chunks around a single player...
  858.             size = math.min(55, size + 2)
  859.         elseif code == keys.left then
  860.             size = math.max(1, size - 2)
  861.         elseif code == keys.up then
  862.             size = math.min(55, size + 10)
  863.         elseif code == keys.down then
  864.             size = math.max(1, size - 10)
  865.         end
  866.     until code == keys.enter
  867.  
  868.     local upOption = 0
  869.     repeat
  870.         term.clear()
  871.         term.setCursorPos(1, 1)
  872.         print("While making their way up out of the\n" ..
  873.               "holes they dug, do you want your    \n" ..
  874.               "turtles to:                         \n")
  875.         if upOption == 0 then
  876.             print(" > Fill the hole with ignored blocks!\n\n" ..
  877.                   "   Place torches every now and then?\n\n" ..
  878.                   "   Just dig as fast as they can?      \n")
  879.         elseif upOption == 1 then
  880.             print("   Fill the hole with ignored blocks?\n\n" ..
  881.                   " > Place torches every now and then!\n\n" ..
  882.                   "   Just dig as fast as they can?      \n")
  883.         elseif upOption == 2 then
  884.             print("   Fill the hole with ignored blocks?\n\n" ..
  885.                   "   Place torches every now and then?\n\n" ..
  886.                   " > Just dig as fast as they can!      \n")
  887.         end
  888.         print("Please select one with the arrow keys \n" ..
  889.               "and press [Enter] to confirm.")
  890.         local _, code = os.pullEvent("key")
  891.         if code == keys.up then
  892.             upOption = (upOption - 1) % 3
  893.         elseif code == keys.down then
  894.             upOption = (upOption + 1) % 3
  895.         end
  896.     until code == keys.enter
  897.     local placeIgnored = upOption == 0
  898.     local placeTorches = upOption == 1
  899.  
  900.     local haveIgnoreChest = false
  901.     if not placeTorches then
  902.         term.clear()
  903.         term.setCursorPos(1, 1)
  904.         print("Since the turtles won't need one of \n" ..
  905.               "the chests for torches, you can     \n" ..
  906.               "instead place blocks you want your  \n" ..
  907.               "turtles to ignore in that chest.    \n" ..
  908.               "This way you won't have to add them \n" ..
  909.               "to each one individually. You'll    \n" ..
  910.               "have to place exacly one stack of at\n" ..
  911.               "least a size equal to the number of \n" ..
  912.               "turtles you plan to use into the    \n" ..
  913.               "torch chest.\n")
  914.         write("Do you want to do this? [Y/n] > ")
  915.         haveIgnoreChest = private.prompt()
  916.     end
  917.  
  918.     term.clear()
  919.     term.setCursorPos(1, 1)
  920.     print("Please confirm your settings\n\n" ..
  921.           "  Radius: " .. radius .. "\n" ..
  922.           "    Affected chunks: " .. chunks .. "\n" ..
  923.           "    Holes/Jobs: " .. jobCount     .. "\n" ..
  924.           "  Place blocks: " .. tostring(placeIgnored) .. "\n" ..
  925.           "  Place torches: " .. tostring(placeTorches) .. "\n" ..
  926.           "  Blocks to ignore in chest: " .. tostring(haveIgnoreChest))
  927.     write("\nIs this correct? [Y/n] > ")
  928.     if not private.prompt() then
  929.         -- nil causes restart.
  930.         return nil
  931.     end
  932.  
  933.     -- Set up the disk so that it installs the script to any
  934.     -- turtle that starts up while in front of it.
  935.     term.clear()
  936.     term.setCursorPos(1, 1)
  937.     print("Writing initialization data to disk...")
  938.  
  939.     local install = {
  940.         {from = bapil.resolveAPI("apis/bapil"),
  941.          to   = fs.combine(diskPath, "apis/bapil")},
  942.         {from = bapil.resolveAPI("apis/lama"),
  943.          to   = fs.combine(diskPath, "apis/lama")},
  944.         {from = bapil.resolveAPI("apis/logger"),
  945.          to   = fs.combine(diskPath, "apis/logger")},
  946.         {from = bapil.resolveAPI("apis/stacktrace"),
  947.          to   = fs.combine(diskPath, "apis/stacktrace")},
  948.         {from = bapil.resolveAPI("apis/startup"),
  949.          to   = fs.combine(diskPath, "apis/startup")},
  950.         {from = bapil.resolveAPI("apis/state"),
  951.          to   = fs.combine(diskPath, "apis/state")},
  952.         {from = shell.getRunningProgram(),
  953.          to   = fs.combine(diskPath, "programs/jam")},
  954.         {from = "programs/jam-status",
  955.          to   = fs.combine(diskPath, "programs/jam-status")},
  956.         -- We write a startup file to the disk drive that will install our
  957.         -- environment to any new turtles started up in front of it. This is
  958.         -- pretty much the same we do here, in reverse.
  959.         {content = string.format(
  960. [=[-- Ignore anything that isn't a turtle.
  961. if not turtle then
  962.     return fs.exists("/startup") and dofile("/startup")
  963. end
  964.  
  965. -- Ignore if we're on the wrong side. Otherwise get the mount path of the disk.
  966. local diskSide = %q
  967. local diskPath = disk.getMountPath(diskSide)
  968. if not disk.isPresent(diskSide) then
  969.     return fs.exists("/startup") and dofile("/startup")
  970. end
  971.  
  972. -- Update the environment, copy APIs, create startup file, that kind of stuff.
  973. print("Updating JAM installation...")
  974.  
  975. -- Set label if necessary.
  976. if os.getComputerLabel() == nil or os.getComputerLabel() == "" then
  977.     os.setComputerLabel("JAM-" .. os.getComputerID())
  978. end
  979.  
  980. -- List of files we put on our turtles.
  981. local install = {
  982.     {from = fs.combine(diskPath, "apis/bapil"),
  983.      to   = "/apis/bapil"},
  984.     {from = fs.combine(diskPath, "apis/lama"),
  985.      to   = "/apis/lama"},
  986.     {from = fs.combine(diskPath, "apis/logger"),
  987.      to   = "/apis/logger"},
  988.     {from = fs.combine(diskPath, "apis/stacktrace"),
  989.      to   = "/apis/stacktrace"},
  990.     {from = fs.combine(diskPath, "apis/startup"),
  991.      to   = "/apis/startup"},
  992.     {from = fs.combine(diskPath, "apis/state"),
  993.      to   = "/apis/state"},
  994.     {from = fs.combine(diskPath, "programs/jam"),
  995.      to   = "/programs/jam"},
  996.     {content =
  997. [[if startup then return false end
  998. os.loadAPI("apis/bapil")
  999. bapil.hijackOSAPI()
  1000. shell.setPath(shell.path() .. ":/programs")
  1001. assert(os.loadAPI("apis/stacktrace"))
  1002. _G.pcall = stacktrace.tpcall
  1003. assert(os.loadAPI("apis/startup"))
  1004. -- Avoid tail-recursion optimization removing our environment from the stack.
  1005. return startup.run() or false]],
  1006.      to = "/startup", keep = true}
  1007. }
  1008.  
  1009. local function areDifferent(f1, f2)
  1010.     local s1 = fs.getSize(f1)
  1011.     local s2 = fs.getSize(f2)
  1012.     if s1 == 512 or s2 == 512 then return true end
  1013.     s1 = s1 - fs.getName(f1):len()
  1014.     s2 = s2 - fs.getName(f2):len()
  1015.     return s1 ~= s2
  1016. end
  1017.  
  1018. local requiredDiskSpace, folders = 0, {}
  1019. for _, entry in ipairs(install) do
  1020.     assert(not fs.exists(entry.to) or not fs.isDir(entry.to),
  1021.         "Bad turtle, folder in location " .. entry.to)
  1022.     local folder = entry.to:sub(1, #entry.to - fs.getName(entry.to):len())
  1023.     assert(not fs.exists(folder) or fs.isDir(folder),
  1024.            "Bad turtle, file in location " .. folder)
  1025.     if not fs.exists(folder) and not folders[folder] then
  1026.         requiredDiskSpace = requiredDiskSpace + 512
  1027.         folders[folder] = true
  1028.     end
  1029.  
  1030.     if not fs.exists(entry.to) or not entry.keep then
  1031.         if entry.from then
  1032.             assert(fs.exists(entry.from) and not fs.isDir(entry.from),
  1033.                 "Bad disk, missing file " .. entry.from)
  1034.             if not fs.exists(entry.to) or
  1035.                areDifferent(entry.from, entry.to)
  1036.             then
  1037.                 fs.delete(entry.to)
  1038.                 requiredDiskSpace = requiredDiskSpace + fs.getSize(entry.from)
  1039.             end
  1040.         elseif entry.content then
  1041.             if not fs.exists(entry.to) or
  1042.                areDifferent(#entry.content, entry.to)
  1043.             then
  1044.                 fs.delete(entry.to)
  1045.                 requiredDiskSpace = requiredDiskSpace + #entry.content
  1046.             end
  1047.         end
  1048.     end
  1049. end
  1050.  
  1051. if requiredDiskSpace > fs.getFreeSpace(diskPath) then
  1052.     print("Not enough space on turtle. Please make some and try again.")
  1053.     return false
  1054. end
  1055.  
  1056. for folder, _ in pairs(folders) do fs.makeDir(folder) end
  1057. for _, entry in ipairs(install) do
  1058.     print(" > " .. entry.to)
  1059.     if entry.from then
  1060.         if not fs.exists(entry.to) then
  1061.             fs.copy(entry.from, entry.to)
  1062.         end
  1063.     elseif entry.content then
  1064.         if not fs.exists(entry.to) then
  1065.             local file = fs.open(entry.to, "w")
  1066.             file.write(entry.content)
  1067.             file.close()
  1068.         end
  1069.     end
  1070.     os.sleep(0.1)
  1071. end
  1072.  
  1073. -- Run local startup script.
  1074. if not dofile("/startup") then return false end
  1075.  
  1076. -- The local startup file should have loaded our APIs.
  1077. assert(startup, "Bad installation, startup API not loaded automatically.")
  1078.  
  1079. -- Overwrite this program in case of updates (different file size).
  1080. if not startup.isEnabled("jam") then
  1081.     startup.remove("jam")
  1082.     startup.addFile("jam", 20, "programs/jam")
  1083.     print("Done installing JAM, rebooting...")
  1084.     os.sleep(1)
  1085.     os.reboot()
  1086. end]=], rsDiskSide),
  1087.          to = fs.combine(diskPath, "startup")}
  1088.     }
  1089.  
  1090.     -- Compute total disk space required and which folders we need to create.
  1091.     local requiredDiskSpace, folders = 0, {}
  1092.     for _, entry in ipairs(install) do
  1093.         assert(not fs.exists(entry.to) or not fs.isDir(entry.to),
  1094.             "Bad disk, folder in location " .. entry.to)
  1095.         local folder = entry.to:sub(1, #entry.to - fs.getName(entry.to):len())
  1096.         assert(not fs.exists(folder) or fs.isDir(folder),
  1097.                "Bad disk, file in location " .. folder)
  1098.         if not fs.exists(folder) and not folders[folder] then
  1099.             requiredDiskSpace = requiredDiskSpace + 512
  1100.             folders[folder] = true
  1101.         end
  1102.  
  1103.         if entry.from then
  1104.             assert(fs.exists(entry.from) and not fs.isDir(entry.from),
  1105.                 "Bad setup, missing file " .. entry.from)
  1106.             if not fs.exists(entry.to) or
  1107.                private.areDifferent(entry.from, entry.to)
  1108.             then
  1109.                 fs.delete(entry.to)
  1110.                 requiredDiskSpace = requiredDiskSpace + fs.getSize(entry.from)
  1111.             end
  1112.         elseif entry.content then
  1113.             if not fs.exists(entry.to) or
  1114.                private.areDifferent(#entry.content, entry.to)
  1115.             then
  1116.                 fs.delete(entry.to)
  1117.                 requiredDiskSpace = requiredDiskSpace + #entry.content
  1118.             end
  1119.         end
  1120.     end
  1121.  
  1122.     print("Additional disk space required: " ..
  1123.           math.ceil(requiredDiskSpace / 1024) .. "KB.")
  1124.     if requiredDiskSpace > fs.getFreeSpace(diskPath) then
  1125.         print("\nNot enough space on attached disk.\n")
  1126.         if not private.formatDisk() then
  1127.             -- nil causes restart.
  1128.             return nil
  1129.         end
  1130.     end
  1131.  
  1132.     for folder, _ in pairs(folders) do
  1133.         fs.makeDir(folder)
  1134.     end
  1135.     for _, entry in ipairs(install) do
  1136.         print(" > " .. entry.to)
  1137.         if entry.from then
  1138.             if not fs.exists(entry.to) then
  1139.                 fs.copy(entry.from, entry.to)
  1140.             end
  1141.         elseif entry.content then
  1142.             if not fs.exists(entry.to) then
  1143.                 local file = fs.open(entry.to, "w")
  1144.                 file.write(entry.content)
  1145.                 file.close()
  1146.             end
  1147.         end
  1148.         os.sleep(0.1)
  1149.     end
  1150.  
  1151.     return jobCount, placeTorches, placeIgnored, haveIgnoreChest
  1152. end
  1153.  
  1154. --[[
  1155.     This function asks the user to put blocks that should be ignored into the
  1156.     turtle's inventory and does some sanity checks on the found blocks.
  1157.  
  1158.     @return the number of block types to ignore.
  1159. ]]
  1160. function private.setupIgnoredBlocks(placeTorches, placeIgnored)
  1161.     -- If we come here a dig site has already been set up, so we just need to
  1162.     -- know which block types to ignore.
  1163.     term.clear()
  1164.     term.setCursorPos(1, 1)
  1165.     print("Please put one of each block type that\n" ..
  1166.           "you want to avoid into my inventory.  \n" ..
  1167.           "When done, press [Enter] to confirm.\n\n" ..
  1168.           "Oh, and you can place the blocks into \n" ..
  1169.           "any slot you want, I'll put them into \n" ..
  1170.           "the right ones myself \\o/          \n\n" ..
  1171.           "Recommended: Stone, Dirt and possibly \n" ..
  1172.           "Gravel or Sand (depends on the biome).\n" ..
  1173.           (placeIgnored and
  1174.           "You may also want to add Cobblestone, \n" ..
  1175.           "to allow placing it while moving up."
  1176.           or "" ))
  1177.     private.prompt({keys.enter}, {})
  1178.     print("OK, let me see...")
  1179.  
  1180.     -- Move stuff from anywhere in the inventory into the first slots, so that
  1181.     -- we have a continuous interval of occupied slots.
  1182.     local slot, free = 1
  1183.     while slot <= 16 do
  1184.         if turtle.getItemCount(slot) > 0 then
  1185.             private.select(slot)
  1186.             for i = 1, slot - 1 do
  1187.                 if turtle.compareTo(i) then
  1188.                     if not turtle.transferTo(i) or
  1189.                        turtle.getItemCount(slot) > 0
  1190.                     then
  1191.                         term.clear()
  1192.                         term.setCursorPos(1, 1)
  1193.                         print("Well, I tried to reduce the redundant  \n" ..
  1194.                               "stuff you gave me into one pile, but I \n" ..
  1195.                               "failed horribly. Cut me some slack. ;) \n" ..
  1196.                               "Please try only giving one block per   \n" ..
  1197.                               "block type, m'kay?                     \n")
  1198.                         print("Press [Enter] to try again m(.,.)m")
  1199.                         private.prompt({keys.enter}, {})
  1200.                         -- Restarts the state.
  1201.                         return nil
  1202.                     end
  1203.                     break
  1204.                 end
  1205.             end
  1206.             if free and turtle.getItemCount(slot) > 0 then
  1207.                 private.select(slot)
  1208.                 turtle.transferTo(free)
  1209.                 slot = free
  1210.                 free = nil
  1211.             end
  1212.         end
  1213.         -- Might have become free while merging blocks.
  1214.         if turtle.getItemCount(slot) == 0 and free == nil then
  1215.             free = slot
  1216.         end
  1217.         slot = slot + 1
  1218.     end
  1219.  
  1220.     -- See how many blocks we have.
  1221.     local occupied = 0
  1222.     for slot = 1, 16 do
  1223.         if turtle.getItemCount(slot) > 0 then
  1224.             occupied = occupied + 1
  1225.         end
  1226.     end
  1227.  
  1228.     term.clear()
  1229.     term.setCursorPos(1, 1)
  1230.     if occupied == 0 then
  1231.         print("Woah, you want me to fetch, like,     \n" ..
  1232.               "everything? Even stone? That'll mean a\n" ..
  1233.               "lot of additional running back and    \n" ..
  1234.               "and forth, which is less efficient /o\\\n\n")
  1235.         write("Are you sure? [y/N] > ")
  1236.         if not private.prompt({keys.y}, {keys.n, keys.enter}) then
  1237.             -- Restarts the state.
  1238.             return nil
  1239.         end
  1240.         print("Oh boy... well, here goes nothing!")
  1241.     elseif occupied == 16 then
  1242.         print("You can't be serious, right? There's \n" ..
  1243.               "just no way this could possibly work.\n" ..
  1244.               "I need at least one free slot into   \n" ..
  1245.               "which I can put the stuff I dig up.  \n")
  1246.         print("Press [Enter] to try again -.-")
  1247.         private.prompt({keys.enter}, {})
  1248.         -- Restarts the state.
  1249.         return nil
  1250.     elseif occupied == 15 and placeTorches then
  1251.         print("Nice try, but this can't work in my  \n" ..
  1252.               "current configuration, since I need  \n" ..
  1253.               "one additional slot for the torches  \n" ..
  1254.               "I'm supposed to place. So I need some\n" ..
  1255.               "more room to store the stuff I dig up\n" ..
  1256.               "in. Meaning at least two free slots  \n" ..
  1257.               "in total. Can you do that for me?    \n")
  1258.         print("Press [Enter] to try again :/")
  1259.         private.prompt({keys.enter}, {})
  1260.         -- Restarts the state.
  1261.         return nil
  1262.     elseif placeTorches and occupied >= torchSlot then
  1263.         print("Unless you messed with the code this \n" ..
  1264.               "should not happen: the inventory     \n" ..
  1265.               "range used for blocks to ignore      \n" ..
  1266.               "intersects with the torch. You'll    \n" ..
  1267.               "have to specify less than " ..
  1268.               (torchSlot - 1) .. " block   \n" ..
  1269.               "types. Press [Enter] to try again... \n")
  1270.         private.prompt({keys.enter}, {})
  1271.         -- Restarts the state.
  1272.         return nil
  1273.     elseif occupied > 6 then
  1274.         print("While this is technically a feasible \n" ..
  1275.               "configuration, I'd highly recommend  \n" ..
  1276.               "adding fewer blocks to ignore. The   \n" ..
  1277.               "more blocks I have to check the more \n" ..
  1278.               "often I have to go back to clear my  \n" ..
  1279.               "inventory, and the longer it takes   \n" ..
  1280.               "to check if I may dig up a block ^.- \n\n" ..
  1281.               "Is this really what you want?          ")
  1282.         write("[Y/n] > ")
  1283.         if not private.prompt() then
  1284.             -- Restarts the state.
  1285.             return nil
  1286.         end
  1287.         print("Well, if you say so... ley's do this!")
  1288.     else
  1289.         local agree = {
  1290.             "Sounds reasonable.",
  1291.             "Yeah, that seems about right.",
  1292.             "As you command, my master!",
  1293.             "I humbly obey.",
  1294.             "Wow, you've got taste.",
  1295.             "Off I go then!",
  1296.             "Here I go!",
  1297.             "Work, work."
  1298.         }
  1299.         print(agree[math.random(1, #agree)])
  1300.     end
  1301.     os.sleep(1.5)
  1302.     return occupied
  1303. end
  1304.  
  1305. --[[
  1306.     An automated version for getting the blocks we should ignore.
  1307. ]]
  1308. function private.setupIgnoredBlocksFromIgnoreChest()
  1309.     lama.turn(torchSide)
  1310.     private.select(1)
  1311.  
  1312.     assert(turtle.detect(), "Chest with ignored blocks disappeared!")
  1313.  
  1314.     -- If we're resuming in here, it means we only have stuff we're supposed
  1315.     -- to ignore in our inventory. Dump it back and start over.
  1316.     for slot = 1, 16 do
  1317.         if turtle.getItemCount(slot) > 0 then
  1318.             private.select(slot)
  1319.             assert(turtle.drop(), "Chest with blocks to ignore is full!")
  1320.         end
  1321.     end
  1322.  
  1323.     -- Suck as often as we can. If there's more, silently ignore it.
  1324.     private.select(1)
  1325.     for slot = 1, 15 do
  1326.         if not turtle.suck() then
  1327.             -- Stop if there's nothing else in there.
  1328.             break
  1329.         end
  1330.     end
  1331.     -- Put everything except one item back.
  1332.     for slot = 1, 15 do
  1333.         if turtle.getItemCount(slot) > 0 then
  1334.             private.select(slot)
  1335.             turtle.drop(turtle.getItemCount(slot) - 1)
  1336.         else
  1337.             break
  1338.         end
  1339.     end
  1340.  
  1341.     -- Merge duplicates, compact the range and dump duplicates.
  1342.     for slot = 2, 16 do
  1343.         -- Ignore empty slots.
  1344.         if turtle.getItemCount(slot) > 0 then
  1345.             -- Compare to all preceding slots
  1346.             for otherSlot = 1, slot - 1 do
  1347.                 if turtle.compareTo(otherSlot) then
  1348.                     -- Got one of the same kind, try to merge.
  1349.                     if not turtle.transferTo(otherSlot) or
  1350.                        turtle.getItemCount(slot) > 0
  1351.                     then
  1352.                         -- Could not merge (other stack already full) or still
  1353.                         -- have some left (other stack is now full and there's
  1354.                         -- still some left here). Drop the remainder.
  1355.                         assert(turtle.drop(),
  1356.                             "Chest with blocks to ignore is full!")
  1357.                     end
  1358.                 end
  1359.             end
  1360.         end
  1361.         -- If there's still something in the slot we didn't find anything of
  1362.         -- the same kind in our inventory. Look for an empty slot at a lower
  1363.         -- index to get a continuous segment.
  1364.         if turtle.getItemCount(slot) > 0 then
  1365.             for otherSlot = 1, slot - 1 do
  1366.                 if turtle.getItemCount(otherSlot) == 0 then
  1367.                     turtle.transferTo(otherSlot)
  1368.                     break
  1369.                 end
  1370.             end
  1371.         end
  1372.     end
  1373.  
  1374.     -- Count the remaining number of ignored block types and return that.
  1375.     local count = 0
  1376.     for slot = 1, 16 do
  1377.         if turtle.getItemCount(slot) > 0 then
  1378.             count = count + 1
  1379.         else
  1380.             break
  1381.         end
  1382.     end
  1383.     return count
  1384. end
  1385.  
  1386. --[[
  1387.     Reads a single key input from the user and returns whether the prompt was
  1388.     confirmed or denied.
  1389.  
  1390.     @param yesKeys a single or multiple keys that indicate success.
  1391.     @param noKeys a single or multiple keys that indicate denial.
  1392.     @return whether the user accepted or denied the prompt.
  1393. ]]
  1394. function private.prompt(yesKeys, noKeys)
  1395.     yesKeys = yesKeys or {keys.y, keys.enter}
  1396.     noKeys = noKeys or {keys.n}
  1397.     if not type(yesKeys) == "table" then
  1398.         yesKeys = {yesKeys}
  1399.     end
  1400.     if not type(noKeys) == "table" then
  1401.         noKeys = {noKeys}
  1402.     end
  1403.     local result
  1404.     term.setCursorBlink(true)
  1405.     repeat
  1406.         local _, code = os.pullEvent("key")
  1407.         for _, k in pairs(yesKeys) do
  1408.             if code == k then
  1409.                 result = true
  1410.                 break
  1411.             end
  1412.         end
  1413.         for _, k in pairs(noKeys) do
  1414.             if code == k then
  1415.                 result = false
  1416.                 break
  1417.             end
  1418.         end
  1419.     until result ~= nil
  1420.     term.setCursorBlink(false)
  1421.     return result
  1422. end
  1423.  
  1424. --[[
  1425.     Checks if two files are potentially different by comparing their file
  1426.     sizes.
  1427. ]]
  1428. function private.areDifferent(file1, file2)
  1429.     local s1 = type(file1) == "string" and fs.getSize(file1) or file1
  1430.     local s2 = fs.getSize(file2)
  1431.     if s1 == 512 or s2 == 512 then
  1432.         return true
  1433.     end
  1434.     s1 = type(file1) == "string" and s1 - fs.getName(file1):len() or s1
  1435.     s2 = s2 - fs.getName(file2):len()
  1436.     return s1 ~= s2
  1437. end
  1438.  
  1439. --[[
  1440.     Formats the attached disk drive.
  1441. ]]
  1442. function private.formatDisk()
  1443.     if not disk.isPresent(rsDiskSide) then
  1444.         return true
  1445.     end
  1446.     local diskPath = disk.getMountPath(rsDiskSide)
  1447.     if #fs.list(diskPath) < 1 then
  1448.         return true
  1449.     end
  1450.     print("This will format the disk in the      \n" ..
  1451.           "attached disk drive. All data will be \n" ..
  1452.           "irretrievably lost.                 \n\n" ..
  1453.           "Are you sure? [Y/n]")
  1454.     if not private.prompt() then
  1455.         print("Aborting.")
  1456.         -- Eat key event so it doesn't propagate into the shell.
  1457.         os.sleep(0.1)
  1458.         return false
  1459.     end
  1460.     print("Formatting disk...")
  1461.     for _, file in pairs(fs.list(diskPath)) do
  1462.         local path = fs.combine(diskPath, file)
  1463.         print(" > " .. file .. (fs.isDir(path) and "/*" or ""))
  1464.         fs.delete(path)
  1465.         os.sleep(0.1)
  1466.     end
  1467.     return true
  1468. end
  1469.  
  1470. --[[
  1471.     Resets the disk and program and removes our startup script.
  1472. ]]
  1473. function private.reset()
  1474.     program:reset()
  1475.     startup.remove("jam")
  1476.     if not disk.isPresent(rsDiskSide) or not disk.getMountPath(rsDiskSide) then
  1477.         lama.turn((diskSide + 2) % 4)
  1478.     end
  1479.     private.waitForDiskDrive(0.5)
  1480.     return private.formatDisk()
  1481. end
  1482.  
  1483. -------------------------------------------------------------------------------
  1484. -- Job position related stuff                                                --
  1485. -------------------------------------------------------------------------------
  1486.  
  1487. --[[
  1488.     Utility function to determine how many holes we can dig so that our spiral
  1489.     doesn't exceed the specified bounds.
  1490.  
  1491.     @param squareSize the size of the bounding square our dig operation has
  1492.         to fit into.
  1493.     @return the number of holes we will dig.
  1494.     @private
  1495. ]]
  1496. function private.computeJobCount(squareSize)
  1497.     if squareSize < 0 then
  1498.         return 0
  1499.     end
  1500.     -- Subtract one for the center, then divide by six, because each following
  1501.     -- layer adds an additional six blocks.
  1502.     local spiralRadius = math.floor((squareSize - 1) / 6)
  1503.     -- Since we still have a square and we now know the number of layers, the
  1504.     -- number of holes is simply the length of one side squared.
  1505.     local sideLength = spiralRadius * 2 + 1
  1506.     return sideLength * sideLength
  1507. end
  1508.  
  1509. --[[
  1510.     Computes the actual x and y coordinates of the nth hole.
  1511.  
  1512.     @param n the number of the hole to compute the coordinates for.
  1513.     @return (x, y) being heightless the coordinates of the nth hole.
  1514.     @private
  1515. ]]
  1516. function private.computeCoordinates(n)
  1517.     -- Adjust to zero indexed system.
  1518.     n = n - 1
  1519.     -- If we're at the origin we can return right away. In fact, we should,
  1520.     -- since we'd get a division by zero in the following...
  1521.     if n < 1 then
  1522.         return 0, 0
  1523.     end
  1524.     -- Compute the coordinates on a plain old rectangular spiral, first.
  1525.     local shell = math.floor((math.sqrt(n) + 1) / 2)
  1526.     local tmp = (2 * shell - 1); tmp = tmp * tmp
  1527.     local leg = math.floor((n - tmp) / (2 * shell))
  1528.     local element = (n - tmp) - (2 * shell * leg) - shell + 1
  1529.     local x, y
  1530.     if leg == 0 then
  1531.         x, y = shell, element
  1532.     elseif leg == 1 then
  1533.         x, y = -element, shell
  1534.     elseif leg == 2 then
  1535.         x, y = -shell, -element
  1536.     else
  1537.         x, y = element, -shell
  1538.     end
  1539.     -- Then map it to our knights move grid.
  1540.     return x * 2 - y, y * 2 + x
  1541. end
  1542.  
  1543. --[[
  1544.     Computes how much fuel we should stock up on for making sure we can safely
  1545.     travel to the specified dig site, dig it up, and come back.
  1546. ]]
  1547. function private.computeExpeditionCost(job)
  1548.     local x, z = private.computeCoordinates(job)
  1549.     -- Start at home, move to the job location (layer enforced by function).
  1550.     local path = private.generatePath(0, 0, 0, x, jobLevel, z, "job")
  1551.      -- Digging faaar down, worst case scenario.
  1552.     table.insert(path, {x = x, y = -255, z = z})
  1553.     -- Append our way back home (again, move layer enforced by generatePath()).
  1554.     local pathBack = private.generatePath(x, jobLevel, z, 0, 0, 0, "home")
  1555.     for _, point in ipairs(pathBack) do
  1556.         table.insert(path, point)
  1557.     end
  1558.     return private.computeFuelNeeded(path)
  1559. end
  1560.  
  1561. --[[
  1562.     Computes how much fuel is needed to travel along the specified path.
  1563.  
  1564.     @param path the path to compute the fuel requirement for.
  1565.     @return the required fuel to travel the path.
  1566.     @private
  1567. ]]
  1568. function private.computeFuelNeeded(path)
  1569.     local function fuel(from, to)
  1570.         local dx = math.abs(to.x - from.x)
  1571.         local dy = math.abs(to.y - from.y)
  1572.         local dz = math.abs(to.z - from.z)
  1573.         return dx + dy + dz
  1574.     end
  1575.     local result = 0
  1576.     local previous = nil
  1577.     for _, current in ipairs(path) do
  1578.         if previous then
  1579.             result = result + fuel(previous, current)
  1580.         end
  1581.         previous = current
  1582.     end
  1583.     return result
  1584. end
  1585.  
  1586. --[[
  1587.     Computes a path leading from or to the docking station, from and to to the
  1588.     specified coordinates.
  1589.  
  1590.     When moving away from the docking station we move in the layer above it,
  1591.     when returning to it in the layer below it.
  1592. ]]
  1593. function private.generatePath(sx, sy, sz, tx, ty, tz, target)
  1594.     if sx == tx and sz == tz then
  1595.         if sy == ty then
  1596.             return {}
  1597.         end
  1598.         return {{x = tx, y = ty, z = tz}}
  1599.     end
  1600.     local layer
  1601.     if target == "home" then
  1602.         layer = backLevel
  1603.     elseif target == "end" then
  1604.         layer = 2
  1605.     elseif target == "job" then
  1606.         layer = awayLevel
  1607.     else
  1608.         error("'target' is invalid")
  1609.     end
  1610.     return {
  1611.         -- Start at the base station.
  1612.         {x = sx, y = sy,    z = sz},
  1613.         -- Move one up to the layer we use for moving away.
  1614.         {x = sx, y = layer, z = sz},
  1615.         -- Move to above the actual coordinates but on the same layer.
  1616.         {x = tx, y = layer, z = tz},
  1617.         -- Move down to where we want to go.
  1618.         {x = tx, y = ty,    z = tz}
  1619.     }
  1620. end
  1621.  
  1622. -------------------------------------------------------------------------------
  1623. -- Working turtle stuff                                                      --
  1624. -------------------------------------------------------------------------------
  1625.  
  1626. --[[
  1627.     Sends a message via WIFI if available.
  1628.  
  1629.     @param the message category, i.e. the type of the message.
  1630.     @param the actual message content.
  1631. ]]
  1632. function private.sendMessage(category, message)
  1633.     if private.noModem then
  1634.         return
  1635.     elseif not private.modem then
  1636.         if peripheral.isPresent("right") and
  1637.            peripheral.getType("right") == "modem"
  1638.         then
  1639.             private.modem = peripheral.wrap("right")
  1640.         elseif peripheral.isPresent("left") and
  1641.                peripheral.getType("left") == "modem"
  1642.         then
  1643.             private.modem = peripheral.wrap("left")
  1644.         else
  1645.             -- Don't try again.
  1646.             private.noModem = true
  1647.             return
  1648.         end
  1649.     end
  1650.     local packet = {}
  1651.     packet.source = os.getComputerID()
  1652.     packet.sourceLabel = os.getComputerLabel()
  1653.     packet.category = category
  1654.     packet.message = message
  1655.     private.modem.transmit(sendChannel, 0, textutils.serialize(packet))
  1656. end
  1657.  
  1658. function private.waitForDiskDrive(secondsToWait)
  1659.     -- Wait a bit to finish moving and connect to the disk drive. This is
  1660.     -- really weird, but apparently a disk drive can be 'present' before it is
  1661.     -- mounted. So we wait until it's actually mounted... damn timining
  1662.     -- problems. We limit our number of tries, in case the disk drive was
  1663.     -- removed or - by some *very* slim chance - we're not in the right spot.
  1664.     -- Try for a total of five seconds, so lag is a non-issue.
  1665.     for i = 1, secondsToWait * 10 do
  1666.         if disk.isPresent(rsDiskSide) and disk.getMountPath(rsDiskSide) then
  1667.             return true
  1668.         end
  1669.         os.sleep(0.1)
  1670.     end
  1671.     return false
  1672. end
  1673.  
  1674. function private.readJobData()
  1675.     local diskPath = disk.getMountPath(rsDiskSide)
  1676.     local jobFileDisk = fs.combine(diskPath, jobFile)
  1677.  
  1678.     local file = assert(fs.open(jobFileDisk, "r"),
  1679.                         "Bad disk, could not find job file.")
  1680.     local jobData = textutils.unserialize(file.readAll())
  1681.     file.close()
  1682.  
  1683.     assert(type(jobData) == "table", "Bad disk, invalid job data.")
  1684.  
  1685.     return jobData
  1686. end
  1687.  
  1688. --[[
  1689.     Checks if all inventory slots are occupied.
  1690. ]]
  1691. function private.isInventoryFull()
  1692.     for slot = 1, 16 do
  1693.         if turtle.getItemCount(slot) == 0 then
  1694.             return false
  1695.         end
  1696.     end
  1697.     return true
  1698. end
  1699.  
  1700. --[[
  1701.     Gets the total number of items in the turtles inventory.
  1702. ]]
  1703. function private.itemCount()
  1704.     local count = 0
  1705.     for slot = 1, 16 do
  1706.         count = count + turtle.getItemCount(slot)
  1707.     end
  1708.     return count
  1709. end
  1710.  
  1711. --[[
  1712.     Replacement for turtle.select() that tracks the currently selected slot so
  1713.     selecting the same slot again becomes a no-op.
  1714. ]]
  1715. function private.select(slot)
  1716.     if slot ~= private.selectedSlot then
  1717.         turtle.select(slot)
  1718.         private.selectedSlot = slot
  1719.     end
  1720. end
  1721.  
  1722. --[[
  1723.     Tries to empty out any inventories and then dig up the block.
  1724. ]]
  1725. function private.suckAndDig(suck, dig)
  1726.     suck = suck or turtle.suck
  1727.     dig = dig or turtle.dig
  1728.     while true do
  1729.         if private.isInventoryFull() then
  1730.             return nil
  1731.         end
  1732.         if not suck() then
  1733.             local count = private.itemCount()
  1734.             local result = dig()
  1735.             return result, count ~= private.itemCount()
  1736.         end
  1737.     end
  1738. end
  1739.  
  1740. --[[
  1741.     Sorts the lookup table for ignore slots so that we iterate them in the
  1742.     order of their frequencies.
  1743.  
  1744.     This takes some measures to avoid rebuilding the sort order
  1745. ]]
  1746. function private.computeIgnoreOrder(ignoreCount, ignoreSamples)
  1747.     if not private.orderedIgnoreSlots then
  1748.         -- Re-use the table to go easy on the GC.
  1749.         private.orderedIgnoreSlots = {}
  1750.         for i = 1, ignoreCount do
  1751.             table.insert(private.orderedIgnoreSlots, i)
  1752.         end
  1753.     end
  1754.     -- This is done quite often, but since this table will be really small it
  1755.     -- doesn't matter.
  1756.     if ignoreSamples then
  1757.         local function comparator(slotA, slotB)
  1758.             return (ignoreSamples[slotA] or 0) > (ignoreSamples[slotB] or 0)
  1759.         end
  1760.         table.sort(private.orderedIgnoreSlots, comparator)
  1761.     end
  1762.     return private.orderedIgnoreSlots
  1763. end
  1764.  
  1765. --[[
  1766.     Used to repeat a move until it succeeds. This *should* be unnecessary. I'm
  1767.     pretty sure I fixed LAMA up so that this it cannot happen, unless there's
  1768.     really an unbreakable block in our way all of a sudden (player placed?).
  1769.     But you just never know, so let's just keep trying hard...
  1770. ]]
  1771. function private.forceMove(where, move)
  1772.     repeat
  1773.         local success, reason = move()
  1774.         if not success then
  1775.             log:warn("%s: unexpectedly couldn't move (%s)", where, reason)
  1776.             -- This really shouldn't happen ever anyway, but if it does, don't
  1777.             -- spam too hard.
  1778.             os.sleep(30)
  1779.         end
  1780.     until success
  1781. end
  1782.  
  1783. --[[
  1784.     -- Digs out a level either on the way up or down.
  1785. ]]
  1786. function private.digLevel(ignoreCount, ignoreSamples, goingDown)
  1787.     -- Dig out two sides each while going down and up. Minimize turns by
  1788.     -- alternating the order between even and odd levels.
  1789.     local sides = ({
  1790.         -- Downwards.
  1791.         [true]  = {
  1792.             -- Even layer.
  1793.             [true] = {lama.side.north, lama.side.east},
  1794.             -- Odd layer.
  1795.             [false] = {lama.side.east, lama.side.north}
  1796.         },
  1797.         -- Upwards.
  1798.         [false] = {
  1799.             -- Even layer.
  1800.             [true] = {lama.side.south, lama.side.west},
  1801.             -- Odd layer.
  1802.             [false] = {lama.side.west, lama.side.south}
  1803.         }
  1804.     })[goingDown][lama.getY() % 2 == 0]
  1805.  
  1806.     -- Get the order in which to check the ignored items.
  1807.     local order = private.computeIgnoreOrder(ignoreCount, ignoreSamples)
  1808.  
  1809.     local droppedChanged = false
  1810.     for _, side in ipairs(sides) do
  1811.         lama.turn(side)
  1812.         if turtle.detect() then
  1813.             -- There's something there! Check if we should ignore it.
  1814.             local ignored = false
  1815.             for i = 1, ignoreCount do
  1816.                 local slot = order[i]
  1817.                 private.select(slot)
  1818.                 if turtle.compare() then
  1819.                     -- Yep, ignore this one. Update our sampling.
  1820.                     ignored = true
  1821.                     ignoreSamples = ignoreSamples or {}
  1822.                     ignoreSamples[slot] = (ignoreSamples[slot] or 0) + 1
  1823.                     droppedChanged = true
  1824.                     break
  1825.                 end
  1826.             end
  1827.             if not ignored then
  1828.                 private.select(ignoreCount + 1)
  1829.                 if private.suckAndDig() == nil then
  1830.                     -- Only returns nil if the inventory is full. Early exit in
  1831.                     -- that case to potentially avoid an unnecessary turn.
  1832.                     break
  1833.                 end
  1834.             end
  1835.         end
  1836.     end
  1837.     return droppedChanged and ignoreSamples or nil
  1838. end
  1839.  
  1840. --[[
  1841.     Drop the contents of the specified slot, keeping only a specific amount.
  1842.     This keeps trying to drop until it succeeds.
  1843. ]]
  1844. function private.drop(slot, keep)
  1845.     keep = keep or 0
  1846.     -- Don't spam the log. We DO spam the WIFI, though, since that can fail.
  1847.     local didLogFull = false
  1848.     while turtle.getItemCount(slot) > keep do
  1849.         lama.turn(dropSide)
  1850.         assert(turtle.detect(), "The dropbox disappeared!")
  1851.         private.select(slot)
  1852.         if not turtle.drop(turtle.getItemCount(slot) - keep) then
  1853.             private.sendMessage("full", "The drop chest is full!")
  1854.             if not didLogFull then
  1855.                 didLogFull = true
  1856.                 log:warn("The drop chest is full! Waiting...")
  1857.             end
  1858.             os.sleep(5)
  1859.         end
  1860.     end
  1861. end
  1862.  
  1863. --[[
  1864.     Refuels the turtle to the specified fuel level. This keeps retrying until
  1865.     it succeeds.
  1866. ]]
  1867. function private.refuel(needed, ignoreCount)
  1868.     -- Don't spam the log. We DO spam the WIFI, though, since that can fail.
  1869.     local didLogNoFuel = false
  1870.     local fuelSlot = ignoreCount + 1
  1871.     while turtle.getFuelLevel() < needed do
  1872.         private.select(fuelSlot)
  1873.         if turtle.getItemCount(fuelSlot) == 0 then
  1874.             lama.turn(fuelSide)
  1875.             assert(turtle.detect(), "The fuel chest disappeared!")
  1876.             if not turtle.suck() then
  1877.                 private.sendMessage("fuel", "We're all out fuel!")
  1878.                 if not didLogNoFuel then
  1879.                     didLogNoFuel = true
  1880.                     log:warn("The fuel chest is empty! Waiting...")
  1881.                 end
  1882.                 os.sleep(5)
  1883.             end
  1884.         else
  1885.             while turtle.getItemCount(fuelSlot) > 0 and
  1886.                   turtle.getFuelLevel() < needed
  1887.             do
  1888.                 if not lama.refuel(1) then
  1889.                     private.drop(fuelSlot)
  1890.                     break
  1891.                 end
  1892.             end
  1893.             turtle.drop()
  1894.         end
  1895.     end
  1896.     private.drop(fuelSlot)
  1897. end
  1898.  
  1899. --[[
  1900.     Restock on torches to get a full stack. This keeps retrying until it
  1901.     succeeds.
  1902. ]]
  1903. function private.restockTorches(ignoreCount)
  1904.     -- Don't spam the log. We DO spam the WIFI, though, since that can fail.
  1905.     local didLogNoTorches = false
  1906.     local tempSlot = ignoreCount + 1
  1907.     while turtle.getItemCount(torchSlot) < (256 / torchFrequency) do
  1908.         -- Select an empty slot because we cannot control how much we pull,
  1909.         -- then replenish our actual torch stack from that and drop the rest.
  1910.         private.select(tempSlot)
  1911.         -- Make sure our temp slot is empty or has torches in it.
  1912.         if turtle.getItemCount(tempSlot) > 0 and
  1913.            -- If we don't have any torches left we cannot be sure that whatever
  1914.            -- we have in our temp slot are torches, so we drop them. Otherwise
  1915.            -- we assume whatever is in our torch slot are guaranteed to be
  1916.            -- torches.
  1917.            (turtle.getItemCount(torchSlot) == 0 or
  1918.             not turtle.compareTo(torchSlot))
  1919.         then
  1920.             private.drop(tempSlot)
  1921.         end
  1922.         lama.turn(torchSide)
  1923.         assert(turtle.detect(), "The torch chest disappeared!")
  1924.         -- At this point we can be sure that if there's something in our temp
  1925.         -- slot it's torches, so use them up first, before sucking up more.
  1926.         if turtle.getItemCount(tempSlot) > 0 or turtle.suck() then
  1927.             turtle.transferTo(torchSlot)
  1928.             -- Put back any surplus.
  1929.             turtle.drop()
  1930.         else
  1931.             private.sendMessage("torches", "We're all out of torches!")
  1932.             if not didLogNoTorches then
  1933.                 didLogNoTorches = true
  1934.                 log:warn("The torch chest is empty! Waiting...")
  1935.             end
  1936.             os.sleep(5)
  1937.         end
  1938.     end
  1939.  
  1940.     private.drop(tempSlot)
  1941. end
  1942.  
  1943. -------------------------------------------------------------------------------
  1944. -- Environment checking                                                      --
  1945. -------------------------------------------------------------------------------
  1946.  
  1947. assert(turtle, "JAM only works on turtles.")
  1948.  
  1949. assert(type(stateFile) == "string" and stateFile ~= "",
  1950.     "The setting 'stateFile' must be a non-empty string.")
  1951.  
  1952. assert(type(jobFile) == "string" and jobFile ~= "",
  1953.     "The setting 'jobFile' must be a non-empty string.")
  1954.  
  1955. assert(rawget(lama.side, dropSide),
  1956.     "The setting 'dropSide' must be a valid lama.side.")
  1957.  
  1958. assert(rawget(lama.side, fuelSide),
  1959.     "The setting 'fuelSide' must be a valid lama.side.")
  1960.  
  1961. assert(rawget(lama.side, torchSide),
  1962.     "The setting 'torchSide' must be a valid lama.side.")
  1963.  
  1964. assert(rawget(lama.side, diskSide),
  1965.     "The setting 'diskSide' must be a valid lama.side.")
  1966.  
  1967. assert(dropSide ~= fuelSide and fuelSide ~= diskSide and
  1968.        dropSide ~= diskSide and diskSide ~= torchSide and
  1969.        dropSide ~= torchSide and fuelSide ~= torchSide,
  1970.        "Duplicate side configuration detected. Make sure the side for each " ..
  1971.        "docking bay element (disk, drop, fuel and torch chest) is different.")
  1972.  
  1973. assert(type(rsDiskSide) == "string" and rsDiskSide ~= "",
  1974.     "'rsDiskSide' is invalid; did you mess with the code?")
  1975.  
  1976. -------------------------------------------------------------------------------
  1977. -- Initialization                                                            --
  1978. -------------------------------------------------------------------------------
  1979.  
  1980. -- Command line argument parsing.
  1981. local args = {...}
  1982. if args[1] == "reset" then
  1983.     return private.reset()
  1984. elseif args[1] == "install" then
  1985.     if not private.reset() then
  1986.         return false
  1987.     end
  1988. elseif #args > 0 then
  1989.     print("Usage: jam [command]")
  1990.     print("Commands:")
  1991.     print("  reset   - resets the program state")
  1992.     print("            and formats the disk.")
  1993.     print("  install - resets and then runs JAM.")
  1994.     print()
  1995.     print("If no command is given, JAM is run")
  1996.     print("normally, either running the wizard")
  1997.     print("or resuming from a previous state.")
  1998.     return false
  1999. end
  2000.  
  2001. -- Make sure LAMA is initialized.
  2002. lama.init()
  2003.  
  2004. -- Run our program.
  2005. program:run()
  2006.  
  2007. -- If the program returns normally we've reached our final position, shut down.
  2008. os.shutdown()
Add Comment
Please, Sign In to add comment