Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --[[
- IMPORTANT: don't use this program. Aside from the fact that it may be broken
- according to some people, ComputerCraft has currently a bug that can deadlock
- the game when a turtle moves next to a disk drive. And that happens a lot with
- JAM. Turtles moving next to a disk drive, I mean. So the chances are actually
- pretty high for this to happen. Trust me, it does happen.
- ]]
- 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
- --[[
- JAM - Just Another Miner - 2013 Sangar
- This program is licensed under the MIT license.
- http://opensource.org/licenses/mit-license.php
- This program is intended to run on turtles working in a very specific
- environment, to collaboratively (optional) dig up blocks of interest in a
- designated area. Yes, it's a mining program ;)
- Features:
- - Fully resumable, thanks to LAMA and my state API. This resume is *very*
- stable. It even survived multiple game crashes (out of memory).
- - Interactively configurable (quarry size, torch placement).
- - Supports turtles cooperatively working on the same quarry, up to the
- total number of holes being dug - one turtle per hole, max. If there's
- fewer turtles than holes, the holes will be worked on sequentially.
- - Adding new turtles is super-easy, just place them into the dock, facing
- the fuel chest (away from the disk drive) and the environment will be
- installed on it and executed.
- To get started, set up the docking station like this (top-down view).
- CF Where: CD = Chest for dropping found blocks.
- CD TU CT CF = Chest with fuel.
- DD CT = Chest with torches (only needed when used).
- DD = A disk drive with an empty floppy inside.
- TU = A turtle, looking at the fuel chest.
- Make sure the turtle in the middle is facing the fuel chest, then install
- the environment like this:
- > pastebin get h9KJ4DRt jam-install
- > jam-install
- The program will guide you through the next steps. Always make sure there's
- enough fuel (and if used, torches) in the chests, and empty the dropbox
- regularly!
- To add more turtles to an existing quarry, simply place them into the
- docking station like you did the first turtle, i.e. facing the fuel chest.
- It will automatically be configured by the program written onto the floppy
- disk when the quarry was set up and then run this program.
- ]]
- assert(os.loadAPI("apis/bapil"))
- assert(os.loadAPI("apis/logger"))
- assert(os.loadAPI("apis/startup"))
- assert(os.loadAPI("apis/state"))
- assert(os.loadAPI("apis/lama"))
- -------------------------------------------------------------------------------
- -- Semi-config (you normally won't want to touch these) --
- -------------------------------------------------------------------------------
- -- The channel on which we'll send out status messages.
- local sendChannel = 10113
- -- Every how many levels torch placing turtles should place a torch.
- local torchFrequency = 8
- -- Whether to write our progress into a log file (each time the program changes
- -- its state it would log that). This is primarily intended for debugging.
- local writeLog = false
- -- File in which to store current program state on the turtle, for resuming the
- -- program after a reboot.
- local stateFile = "/.jam-state"
- -- The path to the file on the disk drive which we use to track how many jobs
- -- we have already assigned to turtles. Note that this will be prefixed with
- -- the disk drives mount path, so it should be a relative path.
- local jobFile = ".jobs"
- -- Given the turtle is in it's native orientation, this is the side the chest
- -- we drop stuff we found into.
- local dropSide = lama.side.left
- -- Given the turtle is in it's native orientation, this is the side the chest
- -- we can get fuel from.
- local fuelSide = lama.side.front
- -- Given the turtle is in it's native orientation, this is the side the chest
- -- we can get new torches from.
- local torchSide = lama.side.right
- -- Given the turtle is in it's native orientation, this is the side the floppy
- -- disk drive we use to track job progress on is at.
- local diskSide = lama.side.back
- -------------------------------------------------------------------------------
- -- Internal variables / constants. DO NOT CHANGE THESE. --
- -------------------------------------------------------------------------------
- -- The slot number in which we keep our torches.
- local torchSlot = 16
- -- The relative level at which to move away from the dock, towards the holes.
- local awayLevel = 1
- -- The relative level at which to move back from the holes, towards the dock.
- local backLevel = -1
- -- Relative level at which to start digging.
- local jobLevel = -2
- -- Same as diskSide but using redstone.getSides() constants (for disk API).
- local rsDiskSide = ({[lama.side.forward] = "front",
- [lama.side.right] = "right",
- [lama.side.back] = "back",
- [lama.side.left] = "left"})[diskSide]
- -- The log we use. Print to screen if we're not supposed to log stuff.
- local log = writeLog and logger.new("jam") or
- {info = function(_, fmt, ...)
- print("[INFO] " .. string.format(fmt, ...))
- end,
- warn = function(_, fmt, ...)
- print("[WARNING] " .. string.format(fmt, ...))
- end}
- -- Forward declaration of namespaces.
- local private
- -------------------------------------------------------------------------------
- -- Program states --
- -------------------------------------------------------------------------------
- -- Start assembling our state machine.
- local program = state.new(stateFile)
- --[[
- This state is purely used for interactively setting up a dig site. It asks
- the user a couple of questions, prepares the disk drive and then reboots
- into the actual worker loop.
- ]]
- :add("setup", function()
- -- Make sure there's a disk drive behind us.
- if not disk.isPresent(rsDiskSide) or not disk.getMountPath(rsDiskSide) then
- lama.turn((diskSide + 2) % 4)
- end
- if not private.waitForDiskDrive(0.5) then
- return private.showBlueprint()
- end
- -- Configure the turtle to use our local coordinate system.
- lama.set(0, 0, 0, lama.side.north)
- -- If there's no job file on the disk drive we're starting from scratch.
- local diskPath = disk.getMountPath(rsDiskSide)
- local jobFileDisk = fs.combine(diskPath, jobFile)
- assert(not fs.exists(jobFileDisk) or not fs.isDir(jobFileDisk),
- "Bad disk, folder at job file location.")
- if not fs.exists(jobFileDisk) then
- assert(shell, "Setup must be run from the shell.")
- local jobCount, placeTorches,
- placeIgnored, haveIgnoreChest = private.setupDigSite()
- if jobCount == nil then
- -- nil is the abort signal, restart the state.
- return
- end
- -- Check for chests.
- print("Checking for chests...")
- lama.turn(dropSide)
- assert(turtle.detect(),
- "Bad setup detected, no drop chest found.")
- lama.turn(fuelSide)
- assert(turtle.detect(),
- "Bad setup detected, no fuel chest found.")
- if placeTorches or haveIgnoreChest then
- lama.turn(torchSide)
- assert(turtle.detect(),
- "Bad setup detected, no torch/ignored blocks chest found.")
- end
- -- Ask for ignored blocks in ignore chest before getting started.
- if haveIgnoreChest then
- private.showIgnoreChestInfo(placeIgnored)
- end
- -- If all went well, write the job file to the disk so that all turtles
- -- placed into the dock from now on will be initialized automatically.
- term.clear()
- term.setCursorPos(1, 1)
- print("Writing settings to disk... ")
- -- Fetch up-to-date path (may have changed after turning).
- lama.turn((diskSide + 2) % 4)
- private.waitForDiskDrive(0.5)
- local diskPath = disk.getMountPath(rsDiskSide)
- local jobFileDisk = fs.combine(diskPath, jobFile)
- local file = assert(fs.open(jobFileDisk, "w"),
- "Could not open job file for writing.")
- file.write(textutils.serialize({totalJobs = jobCount,
- placeTorches = placeTorches,
- placeIgnored = placeIgnored,
- haveIgnoreChest = haveIgnoreChest}))
- file.close()
- end
- -- Next up: add this turtle as a worker to the quarry.
- switchTo("add_turtle")
- -- Finish initialization via disk startup. For example, this will install
- -- this program as an autorun script.
- os.reboot()
- end)
- --[[
- Given an existing dig site adds a new turtle to work on it.
- ]]
- :add("add_turtle", function()
- -- Align back to face away from the disk drive.
- lama.turn((diskSide + 2) % 4)
- private.waitForDiskDrive(0.5)
- assert(disk.isPresent(rsDiskSide) and disk.getMountPath(rsDiskSide),
- "Bad setup detected, no disk in disk drive behind me!")
- -- Copy global quarry settings.
- local jobData = private.readJobData()
- placeIgnored = placeIgnored or jobData.placeIgnored
- placeTorches = placeTorches or jobData.placeTorches
- haveIgnoreChest = haveIgnoreChest or jobData.haveIgnoreChest
- -- Ask for the blocks to ignore to be placed in our inventory. Remember how
- -- many block types we're supposed to ignore.
- if haveIgnoreChest then
- ignoreCount = private.setupIgnoredBlocksFromIgnoreChest()
- else
- ignoreCount = private.setupIgnoredBlocks(placeTorches, placeIgnored)
- -- If we got nothing (nil) we're supposed to try again.
- if not ignoreCount then
- return
- end
- end
- -- Drop any duplicates we have of the stuff we're supposed to ignore.
- -- Switching to goto_drop is equivalent to drop, but more uniform flow.
- switchTo("goto_drop")
- end)
- --[[
- Go back to the docking station to drop stuff we picked up.
- ]]
- :add("goto_drop", function()
- log:info("Going home to deliver the goods.")
- private.sendMessage("state", "goto_drop")
- private.select(1)
- local x, y, z = lama.get()
- local path = private.generatePath(x, y, z, 0, 0, 0, "home")
- private.forceMove("goto_drop", function()
- return lama.navigate(path, math.huge, true)
- end)
- switchTo("drop")
- end)
- --[[
- Actually drop anything in our inventory that's surplus.
- ]]
- :add("drop", function()
- log:info("Dropping it!")
- private.sendMessage("state", "drop")
- if not placeIgnored or not job then
- for slot = 1, ignoreCount do
- if turtle.getItemCount(slot) > 1 then
- private.drop(slot, 1)
- end
- end
- end
- local function isIgnoredBlock(slot)
- if not ignoredLookup then
- return false
- end
- for i = 1, #ignoredLookup do
- if ignoredLookup[i] == slot then
- return true
- end
- end
- return false
- end
- for slot = ignoreCount + 1, 16 do
- if not placeTorches or slot ~= torchSlot then
- if not isIgnoredBlock(slot) then
- private.drop(slot)
- end
- end
- end
- -- Continue with our current job, or get a new one if we don't have one.
- if job then
- switchTo("refuel_job")
- else
- switchTo("get_job")
- -- Turn back to original orientation.
- lama.turn((diskSide + 2) % 4)
- -- We reboot before each job because this way the Lua VM gets a proper
- -- cleaning (not sure if that's what caused it, but running multiple
- -- turtles for extended periods made my worlds crash), and because I
- -- had a weird bug where the turtle would otherwise not see the disk
- -- inside the disk drive until either rebooted forcefully, or I opened
- -- the menu, shortly. Yes. I know. Totally insane, but 99% reproducible
- -- in that one world.
- os.reboot()
- end
- end)
- --[[
- Tries to get a job, on success refuels and executes it, otherwise refuels
- to move to retirement position (out of the way of other turtles).
- ]]
- :add("get_job", function()
- log:info("Looking for a new job.")
- private.sendMessage("state", "get_job")
- -- Make sure the disk drive is behind us.
- assert(private.waitForDiskDrive(5),
- "Bad setup detected, no disk in disk drive behind me!")
- -- Returns a timestamp based on the ingame time (in ticks).
- local function timestamp()
- return (os.day() * 24 + os.time()) * 1000
- end
- -- Get the current job information.
- local jobData = private.readJobData()
- -- If we finished a job, clear it from the list of active jobs.
- if finishedJob and jobData.activeJobs and
- jobData.activeJobs[finishedJob]
- then
- -- Remember how long the last couple of jobs took to finish. This is
- -- useful for predicting when an active job has taken an unexpectedly
- -- long time and may require the player's investigation (via some
- -- stationary computer attached to the disk drive for example).
- local started = jobData.activeJobs[finishedJob]
- local finished = timestamp()
- local duration = finished - started
- jobData.jobDurations = jobData.jobDurations or {}
- table.insert(jobData.jobDurations, 1, duration)
- if #jobData.jobDurations > 10 then
- table.remove(jobData.jobDurations)
- end
- jobData.activeJobs[finishedJob] = nil
- finishedJob = nil
- end
- -- Try to get a new job.
- local nextJob = jobData.nextJob or 1
- if nextJob <= jobData.totalJobs then
- job = nextJob
- jobData.activeJobs = jobData.activeJobs or {}
- jobData.activeJobs[job] = timestamp()
- jobData.nextJob = job + 1
- if job == 1 then
- jobData.startTime = timestamp()
- end
- switchTo("refuel_job")
- log:info("Got one! Our new job ticket is #%d.", job)
- private.sendMessage("job", job)
- else
- endSlot = jobData.finished or 0
- jobData.finished = endSlot + 1
- jobData.stopTime = timestamp()
- switchTo("refuel_end")
- end
- -- Write new state back to disk.
- local diskPath = disk.getMountPath(rsDiskSide)
- local jobFileDisk = fs.combine(diskPath, jobFile)
- local file = assert(fs.open(jobFileDisk, "w"),
- "Bad disk, failed to open job file for writing.")
- file.write(textutils.serialize(jobData))
- file.close()
- end)
- --[[
- Gets enough fuel to get to and back from our job.
- ]]
- :add("refuel_job", function()
- log:info("Getting enough fuel to make it to our job and back.")
- private.sendMessage("state", "refuel_job")
- private.refuel(private.computeExpeditionCost(job), ignoreCount)
- switchTo("restock_torches")
- end)
- --[[
- Refills our torch stack.
- ]]
- :add("restock_torches", function()
- if placeTorches then
- log:info("Picking up some torches to bring light into the dark.")
- private.sendMessage("state", "restock_torches")
- private.restockTorches(ignoreCount)
- end
- switchTo("goto_job")
- end)
- --[[
- Moves the turtle to the top of the hole it should dig.
- ]]
- :add("goto_job", function()
- local tx, tz = private.computeCoordinates(job)
- log:info("On my way to my workplace @ (%d, %d)!", tx, tz)
- private.sendMessage("state", "goto_job")
- private.select(1)
- local x, y, z = lama.get()
- local path = private.generatePath(x, y, z, tx, jobLevel, tz, "job")
- private.forceMove("goto_job", function()
- return lama.navigate(path, math.huge, true, true)
- end)
- if resumeDigUp then
- switchTo("dig_up")
- else
- switchTo("dig_down")
- end
- end)
- --[[
- Digs a hole down until we hit bedrock, returns to drop stuff if full and
- returns to where we left off.
- ]]
- :add("dig_down", function()
- log:info("Looking for bedrock, can offer dancing skills.")
- private.sendMessage("state", "dig_down")
- if resumeDigDown then
- log:info("Resuming from where I took a break.")
- private.select(1)
- while lama.getY() > resumeDigDown do
- lama.down(math.huge, true)
- end
- resumeDigDown = nil
- end
- repeat
- local newSamples = private.digLevel(ignoreCount, ignoreSamples, true)
- if newSamples then
- ignoreSamples = newSamples
- save()
- end
- if private.isInventoryFull() then
- -- We may sample the blocks on the level we're on twice (again when
- -- coming back), but that's an acceptable simplification.
- resumeDigDown = lama.getY()
- return switchTo("goto_drop")
- end
- private.select(1)
- local brokeStuff, pickedSomethingUp =
- private.suckAndDig(turtle.suckDown, turtle.digDown)
- if (placeIgnored or placeTorches) and
- pickedSomethingUp and not groundLevel
- then
- groundLevel = lama.getY()
- save()
- end
- -- Only try to move down if we don't break anything while doing so. If
- -- suckAndDig() may return false even though there is something if our
- -- inventory is full and we should go empty it, so check if there's no
- -- block before we move.
- local couldMove = false
- if brokeStuff or not turtle.detectDown() then
- couldMove = lama.down(math.huge, true)
- end
- until not brokeStuff and not couldMove
- switchTo("rebuild_ignore_lookup")
- end)
- --[[
- Sorts the blocks we're supposed to ignore, if any, so that the most
- frequently encountered ones are at the lower inventory positions (for
- faster early exists when comparing).
- ]]
- :add("rebuild_ignore_lookup", function()
- if placeIgnored and groundLevel then
- log:info("Checking for overflow of ignorance.")
- private.sendMessage("state", "rebuild_ignore_lookup")
- -- Swaps two slots in the inventory.
- local function swap(slotA, slotB)
- if slotA == slotB then
- return
- end
- -- Find an empty slot to work with as a temporary buffer.
- local slotTmp
- for slot = ignoreCount + 1, 16 do
- if turtle.getItemCount(slot) == 0 then
- slotTmp = slot
- break
- end
- end
- if not slotTmp then
- return false
- end
- private.select(slotA)
- turtle.transferTo(slotTmp)
- private.select(slotB)
- turtle.transferTo(slotA)
- private.select(slotTmp)
- turtle.transferTo(slotB)
- return true
- end
- -- Build a look-up table of slots that also contain ignored items in
- -- case we dug up so many we had an overflow. This is used for to avoid
- -- having to check the complete inventory when placing ignored blocks
- -- while moving up (which would be horribly slow).
- local count, nextSlot = 0, 16
- local lookup = {}
- for slot = 16, ignoreCount + 1, -1 do
- if turtle.getItemCount(slot) > 0 and
- not placeTorches or slot ~= torchSlot
- then
- -- Got a candidate, compare it to all known ignored blocks.
- private.select(slot)
- for ignoreSlot = 1, ignoreCount do
- if turtle.compareTo(ignoreSlot) then
- -- This is an ignored block. Try to push it to the back
- -- of the inventory, to avoid ripping holes into the
- -- inventory list when we deplete the stack, which can
- -- lead to getting two stacks of the same item type of
- -- dug up blocks (e.g.: coal at 5, additional cobble at
- -- 4, cobble depletes, new coal found, goes to 4 ->
- -- both 4 and 5 contain coal, where if cobble were
- -- behind the coal they'd be merged (assuming what coal
- -- we had wasn't a full stack already, of course).
- count = count + turtle.getItemCount(slot)
- if swap(slot, nextSlot) then
- table.insert(lookup, 1, nextSlot)
- nextSlot = nextSlot - 1
- else
- table.insert(lookup, 1, slot)
- end
- break
- end
- end
- end
- end
- if #lookup > 0 then
- ignoredLookup = lookup
- end
- -- Add the counts for the excess items in the stacks of the reference
- -- slots (we only need to keep one of these).
- for slot = 1, ignoreCount do
- count = count + (turtle.getItemCount(slot) - 1)
- end
- -- Remember the level at which we can start placing ignored blocks so
- -- that we have enough up to the surface.
- placeIgnoredLevel = groundLevel - count
- end
- switchTo("dig_up")
- end)
- --[[
- Digs back up, picking up whatever isn't ignored. Returns home to drop
- stuff if inventory is full to resume where it we off.
- ]]
- :add("dig_up", function()
- log:info("Moving up in the world.")
- private.sendMessage("state", "dig_up")
- -- Get back to where we stopped if we're resuming our job.
- if resumeDigUp then
- log:info("Resuming from where I took a break.")
- private.select(1)
- while lama.getY() > resumeDigUp do
- lama.down(math.huge, true)
- end
- resumeDigUp = nil
- end
- -- Break after block placement to guarantee we always place as necessary.
- while true do
- -- Try placing before moving, so that we can be sure we placed our
- -- thing if we restarted after finishing a move, but before placing.
- if groundLevel and lama.getY() <= groundLevel then
- -- Note that placeIgnored and placeTorches are mutually exclusive.
- if placeIgnored and not placeIgnoredLevel or
- lama.getY() > placeIgnoredLevel
- then
- -- Once we're above the level we can start placing our ignored
- -- blocks from we unset the variable to reduce state file size.
- if placeIgnoredLevel then
- placeIgnoredLevel = nil
- save()
- end
- -- Figure out which slot to place from, starting with overflow
- -- slots (stored in ignoreLookup, determined in sort state).
- local slot
- if ignoredLookup then
- slot = ignoredLookup[#ignoredLookup]
- else
- -- No more lookup-blocks. Draw from our reference stacks.
- for i = 1, ignoreCount do
- if turtle.getItemCount(i) > 1 then
- slot = i
- break
- end
- end
- end
- -- If no-one stole items from our inventory and we didn't
- -- miscalculate we should always have a slot by now.
- if slot then
- private.select(slot)
- turtle.placeDown()
- if ignoredLookup then
- if turtle.getItemCount(slot) == 0 then
- table.remove(ignoredLookup)
- end
- if #ignoredLookup == 0 then
- ignoredLookup = nil
- end
- save()
- end
- else
- groundLevel = nil
- end
- elseif placeTorches and lama.getY() % torchFrequency == 0 then
- private.select(torchSlot)
- turtle.placeDown()
- end
- else
- -- Reset after coming above ground level, to make the check fail
- -- at the first part and to reduce state file size.
- groundLevel = nil
- end
- -- Dig out the level.
- local newSamples = private.digLevel(ignoreCount, ignoreSamples, false)
- if newSamples then
- ignoreSamples = newSamples
- save()
- end
- -- Move up until we're back at our starting location.
- if lama.getY() >= jobLevel then
- break
- end
- if private.isInventoryFull() then
- -- We may sample the blocks on the level we're on twice (again when
- -- coming back), but that's an acceptable simplification.
- resumeDigUp = lama.getY()
- return switchTo("goto_drop")
- end
- private.select(1)
- private.forceMove("dig_up", function()
- return lama.up(math.huge, true)
- end)
- end
- finishedJob = job
- groundLevel = nil
- placeIgnoredLevel = nil
- ignoredLookup = nil
- job = nil
- switchTo("goto_drop")
- end)
- --[[
- Gets enough fuel to move to the graveyard.
- ]]
- :add("refuel_end", function()
- log:info("Getting my last rites. Fuel! I meant fuel.")
- private.sendMessage("state", "refuel_end")
- -- Generate the path to our final resting place: two up, n forward and one
- -- to the side (so future finishing turtles can pass us by).
- local x, y, z = lama.get()
- local path = {
- {x = x, y = y, z = z},
- {x = 0, y = 2, z = 0},
- {x = 0, y = 2, z = endSlot},
- {x = 1, y = 2, z = endSlot}
- }
- private.refuel(private.computeFuelNeeded(path), ignoreCount)
- switchTo("clear_inventory")
- end)
- --[[
- Clear our inventory when we're done.
- ]]
- :add("clear_inventory", function()
- log:info("Clearing out my inventory since I'm done with the world.")
- private.sendMessage("state", "clear_inventory")
- for slot = 1, 16 do
- private.drop(slot)
- end
- private.select(1)
- switchTo("goto_end")
- end)
- --[[
- Moves to *in front of* the slot where we'll shut down.
- ]]
- :add("goto_end", function()
- log:info("Moving out of the way, I'm just useless junk anyway.")
- private.sendMessage("state", "goto_end")
- local x, y, z = lama.get()
- local path = private.generatePath(x, y, z, 0, 2, endSlot, "end")
- private.forceMove("goto_end", function()
- return lama.navigate(path, math.huge, true)
- end)
- switchTo("end")
- end)
- --[[
- Moves into the actual slot where we'll shut down and then die quietly.
- ]]
- :add("end", function()
- log:info("That's it, I'm going to end it right here. It was a nice life.")
- private.sendMessage("state", "end")
- private.forceMove("end", function()
- return lama.moveto(1, 2, endSlot, lama.side.north, math.huge, true)
- end)
- startup.disable("jam")
- switchTo(nil)
- end)
- -------------------------------------------------------------------------------
- -------------------------------------------------------------------------------
- -- Utility functions --
- -------------------------------------------------------------------------------
- -------------------------------------------------------------------------------
- -- Private namespace.
- private = {}
- -------------------------------------------------------------------------------
- -- Quarry and turtle setup --
- -------------------------------------------------------------------------------
- --[[
- Show instructions on how to set up a JAM quarry.
- ]]
- function private.showBlueprint()
- term.clear()
- term.setCursorPos(1, 1)
- write("To use JAM you need to build a small \n" ..
- "docking station. Build it like this: \n" ..
- " \n" ..
- " CF Where \n" ..
- " CD TU CT CD: Chest for dropping \n" ..
- " DD found blocks/items. \n" ..
- " CF: Chest with turtle fuel.\n" ..
- " CT: Chest with torches or blocks to \n" ..
- " ignore (depends on config). \n" ..
- " DD: Disk drive with an empty disk. \n" ..
- " TU: This turtle, back to the drive. \n" ..
- " \n" ..
- "When done, press [Enter] to continue.")
- private.prompt({keys.enter}, {})
- end
- --[[
- Show information about how to configure ignored blocks.
- ]]
- function private.showIgnoreChestInfo(placeIgnored)
- term.clear()
- term.setCursorPos(1, 1)
- print("Please put the block types that you \n" ..
- "want to avoid into the torch chest or \n" ..
- "my inventory. Insert as many each, as \n" ..
- "you wish to employ turtles (normally \n" ..
- "one stack each should be plenty). \n\n" ..
- "When done, press [Enter] to confirm.\n\n" ..
- "Recommended: Stone, Dirt and possibly \n" ..
- "Gravel or Sand (depends on the biome).\n" ..
- (placeIgnored and
- "You may also want to add Cobblestone, \n" ..
- "to allow placing it while moving up."
- or "" ))
- private.prompt({keys.enter}, {})
- end
- --[[
- Asks the user for the size of the dig site and configures the floppy disk.
- This function never returns, it reboots the turtle after it has run.
- ]]
- function private.setupDigSite()
- -- Label the disk if it hasn't been labeled yet.
- if not disk.getLabel(rsDiskSide) or disk.getLabel(rsDiskSide) == "" then
- disk.setLabel(rsDiskSide, "JAM Job State")
- end
- local diskPath = disk.getMountPath(rsDiskSide)
- -- Ask the user how big of a dig site he'd like to create.
- term.clear()
- term.setCursorPos(1, 1)
- print(" Welcome to JAM: Just another Miner!\n\n" ..
- "Please tell me how large an area you \n" ..
- "want to mine. The area will always be \n" ..
- "squared. Use the arrow keys to adjust \n" ..
- "the size, then press [Enter]. \n")
- local _, y = term.getCursorPos()
- local format = {" Size: %d (%d chunk, %d hole)\n",
- " Size: %d (%d chunks, %d holes)\n"}
- print("\n")
- write("[left/right: one, up/down: five steps]\n\n" ..
- "(Note: chunks refers to the number of \n" ..
- "affected ones, not the worked on area)")
- local size, radius, chunks, jobCount = 1
- repeat
- local rx, ry = term.getCursorPos()
- radius = size * 3
- chunks = math.ceil(radius / 16)
- chunks = chunks * chunks
- jobCount = private.computeJobCount(radius)
- term.setCursorPos(1, y)
- term.clearLine()
- write(string.format(size == 1 and format[1] or format[2],
- radius, chunks, jobCount))
- -- For termination not f*cking up the shell.
- term.setCursorPos(rx, ry)
- local _, code = os.pullEvent("key")
- if code == keys.right then
- -- Let's bound the maximum dig site size to the area of loaded
- -- chunks around a single player...
- size = math.min(55, size + 2)
- elseif code == keys.left then
- size = math.max(1, size - 2)
- elseif code == keys.up then
- size = math.min(55, size + 10)
- elseif code == keys.down then
- size = math.max(1, size - 10)
- end
- until code == keys.enter
- local upOption = 0
- repeat
- term.clear()
- term.setCursorPos(1, 1)
- print("While making their way up out of the\n" ..
- "holes they dug, do you want your \n" ..
- "turtles to: \n")
- if upOption == 0 then
- print(" > Fill the hole with ignored blocks!\n\n" ..
- " Place torches every now and then?\n\n" ..
- " Just dig as fast as they can? \n")
- elseif upOption == 1 then
- print(" Fill the hole with ignored blocks?\n\n" ..
- " > Place torches every now and then!\n\n" ..
- " Just dig as fast as they can? \n")
- elseif upOption == 2 then
- print(" Fill the hole with ignored blocks?\n\n" ..
- " Place torches every now and then?\n\n" ..
- " > Just dig as fast as they can! \n")
- end
- print("Please select one with the arrow keys \n" ..
- "and press [Enter] to confirm.")
- local _, code = os.pullEvent("key")
- if code == keys.up then
- upOption = (upOption - 1) % 3
- elseif code == keys.down then
- upOption = (upOption + 1) % 3
- end
- until code == keys.enter
- local placeIgnored = upOption == 0
- local placeTorches = upOption == 1
- local haveIgnoreChest = false
- if not placeTorches then
- term.clear()
- term.setCursorPos(1, 1)
- print("Since the turtles won't need one of \n" ..
- "the chests for torches, you can \n" ..
- "instead place blocks you want your \n" ..
- "turtles to ignore in that chest. \n" ..
- "This way you won't have to add them \n" ..
- "to each one individually. You'll \n" ..
- "have to place exacly one stack of at\n" ..
- "least a size equal to the number of \n" ..
- "turtles you plan to use into the \n" ..
- "torch chest.\n")
- write("Do you want to do this? [Y/n] > ")
- haveIgnoreChest = private.prompt()
- end
- term.clear()
- term.setCursorPos(1, 1)
- print("Please confirm your settings\n\n" ..
- " Radius: " .. radius .. "\n" ..
- " Affected chunks: " .. chunks .. "\n" ..
- " Holes/Jobs: " .. jobCount .. "\n" ..
- " Place blocks: " .. tostring(placeIgnored) .. "\n" ..
- " Place torches: " .. tostring(placeTorches) .. "\n" ..
- " Blocks to ignore in chest: " .. tostring(haveIgnoreChest))
- write("\nIs this correct? [Y/n] > ")
- if not private.prompt() then
- -- nil causes restart.
- return nil
- end
- -- Set up the disk so that it installs the script to any
- -- turtle that starts up while in front of it.
- term.clear()
- term.setCursorPos(1, 1)
- print("Writing initialization data to disk...")
- local install = {
- {from = bapil.resolveAPI("apis/bapil"),
- to = fs.combine(diskPath, "apis/bapil")},
- {from = bapil.resolveAPI("apis/lama"),
- to = fs.combine(diskPath, "apis/lama")},
- {from = bapil.resolveAPI("apis/logger"),
- to = fs.combine(diskPath, "apis/logger")},
- {from = bapil.resolveAPI("apis/stacktrace"),
- to = fs.combine(diskPath, "apis/stacktrace")},
- {from = bapil.resolveAPI("apis/startup"),
- to = fs.combine(diskPath, "apis/startup")},
- {from = bapil.resolveAPI("apis/state"),
- to = fs.combine(diskPath, "apis/state")},
- {from = shell.getRunningProgram(),
- to = fs.combine(diskPath, "programs/jam")},
- {from = "programs/jam-status",
- to = fs.combine(diskPath, "programs/jam-status")},
- -- We write a startup file to the disk drive that will install our
- -- environment to any new turtles started up in front of it. This is
- -- pretty much the same we do here, in reverse.
- {content = string.format(
- [=[-- Ignore anything that isn't a turtle.
- if not turtle then
- return fs.exists("/startup") and dofile("/startup")
- end
- -- Ignore if we're on the wrong side. Otherwise get the mount path of the disk.
- local diskSide = %q
- local diskPath = disk.getMountPath(diskSide)
- if not disk.isPresent(diskSide) then
- return fs.exists("/startup") and dofile("/startup")
- end
- -- Update the environment, copy APIs, create startup file, that kind of stuff.
- print("Updating JAM installation...")
- -- Set label if necessary.
- if os.getComputerLabel() == nil or os.getComputerLabel() == "" then
- os.setComputerLabel("JAM-" .. os.getComputerID())
- end
- -- List of files we put on our turtles.
- local install = {
- {from = fs.combine(diskPath, "apis/bapil"),
- to = "/apis/bapil"},
- {from = fs.combine(diskPath, "apis/lama"),
- to = "/apis/lama"},
- {from = fs.combine(diskPath, "apis/logger"),
- to = "/apis/logger"},
- {from = fs.combine(diskPath, "apis/stacktrace"),
- to = "/apis/stacktrace"},
- {from = fs.combine(diskPath, "apis/startup"),
- to = "/apis/startup"},
- {from = fs.combine(diskPath, "apis/state"),
- to = "/apis/state"},
- {from = fs.combine(diskPath, "programs/jam"),
- to = "/programs/jam"},
- {content =
- [[if startup then return false end
- os.loadAPI("apis/bapil")
- bapil.hijackOSAPI()
- shell.setPath(shell.path() .. ":/programs")
- assert(os.loadAPI("apis/stacktrace"))
- _G.pcall = stacktrace.tpcall
- assert(os.loadAPI("apis/startup"))
- -- Avoid tail-recursion optimization removing our environment from the stack.
- return startup.run() or false]],
- to = "/startup", keep = true}
- }
- local function areDifferent(f1, f2)
- local s1 = fs.getSize(f1)
- local s2 = fs.getSize(f2)
- if s1 == 512 or s2 == 512 then return true end
- s1 = s1 - fs.getName(f1):len()
- s2 = s2 - fs.getName(f2):len()
- return s1 ~= s2
- end
- local requiredDiskSpace, folders = 0, {}
- for _, entry in ipairs(install) do
- assert(not fs.exists(entry.to) or not fs.isDir(entry.to),
- "Bad turtle, folder in location " .. entry.to)
- local folder = entry.to:sub(1, #entry.to - fs.getName(entry.to):len())
- assert(not fs.exists(folder) or fs.isDir(folder),
- "Bad turtle, file in location " .. folder)
- if not fs.exists(folder) and not folders[folder] then
- requiredDiskSpace = requiredDiskSpace + 512
- folders[folder] = true
- end
- if not fs.exists(entry.to) or not entry.keep then
- if entry.from then
- assert(fs.exists(entry.from) and not fs.isDir(entry.from),
- "Bad disk, missing file " .. entry.from)
- if not fs.exists(entry.to) or
- areDifferent(entry.from, entry.to)
- then
- fs.delete(entry.to)
- requiredDiskSpace = requiredDiskSpace + fs.getSize(entry.from)
- end
- elseif entry.content then
- if not fs.exists(entry.to) or
- areDifferent(#entry.content, entry.to)
- then
- fs.delete(entry.to)
- requiredDiskSpace = requiredDiskSpace + #entry.content
- end
- end
- end
- end
- if requiredDiskSpace > fs.getFreeSpace(diskPath) then
- print("Not enough space on turtle. Please make some and try again.")
- return false
- end
- for folder, _ in pairs(folders) do fs.makeDir(folder) end
- for _, entry in ipairs(install) do
- print(" > " .. entry.to)
- if entry.from then
- if not fs.exists(entry.to) then
- fs.copy(entry.from, entry.to)
- end
- elseif entry.content then
- if not fs.exists(entry.to) then
- local file = fs.open(entry.to, "w")
- file.write(entry.content)
- file.close()
- end
- end
- os.sleep(0.1)
- end
- -- Run local startup script.
- if not dofile("/startup") then return false end
- -- The local startup file should have loaded our APIs.
- assert(startup, "Bad installation, startup API not loaded automatically.")
- -- Overwrite this program in case of updates (different file size).
- if not startup.isEnabled("jam") then
- startup.remove("jam")
- startup.addFile("jam", 20, "programs/jam")
- print("Done installing JAM, rebooting...")
- os.sleep(1)
- os.reboot()
- end]=], rsDiskSide),
- to = fs.combine(diskPath, "startup")}
- }
- -- Compute total disk space required and which folders we need to create.
- local requiredDiskSpace, folders = 0, {}
- for _, entry in ipairs(install) do
- assert(not fs.exists(entry.to) or not fs.isDir(entry.to),
- "Bad disk, folder in location " .. entry.to)
- local folder = entry.to:sub(1, #entry.to - fs.getName(entry.to):len())
- assert(not fs.exists(folder) or fs.isDir(folder),
- "Bad disk, file in location " .. folder)
- if not fs.exists(folder) and not folders[folder] then
- requiredDiskSpace = requiredDiskSpace + 512
- folders[folder] = true
- end
- if entry.from then
- assert(fs.exists(entry.from) and not fs.isDir(entry.from),
- "Bad setup, missing file " .. entry.from)
- if not fs.exists(entry.to) or
- private.areDifferent(entry.from, entry.to)
- then
- fs.delete(entry.to)
- requiredDiskSpace = requiredDiskSpace + fs.getSize(entry.from)
- end
- elseif entry.content then
- if not fs.exists(entry.to) or
- private.areDifferent(#entry.content, entry.to)
- then
- fs.delete(entry.to)
- requiredDiskSpace = requiredDiskSpace + #entry.content
- end
- end
- end
- print("Additional disk space required: " ..
- math.ceil(requiredDiskSpace / 1024) .. "KB.")
- if requiredDiskSpace > fs.getFreeSpace(diskPath) then
- print("\nNot enough space on attached disk.\n")
- if not private.formatDisk() then
- -- nil causes restart.
- return nil
- end
- end
- for folder, _ in pairs(folders) do
- fs.makeDir(folder)
- end
- for _, entry in ipairs(install) do
- print(" > " .. entry.to)
- if entry.from then
- if not fs.exists(entry.to) then
- fs.copy(entry.from, entry.to)
- end
- elseif entry.content then
- if not fs.exists(entry.to) then
- local file = fs.open(entry.to, "w")
- file.write(entry.content)
- file.close()
- end
- end
- os.sleep(0.1)
- end
- return jobCount, placeTorches, placeIgnored, haveIgnoreChest
- end
- --[[
- This function asks the user to put blocks that should be ignored into the
- turtle's inventory and does some sanity checks on the found blocks.
- @return the number of block types to ignore.
- ]]
- function private.setupIgnoredBlocks(placeTorches, placeIgnored)
- -- If we come here a dig site has already been set up, so we just need to
- -- know which block types to ignore.
- term.clear()
- term.setCursorPos(1, 1)
- print("Please put one of each block type that\n" ..
- "you want to avoid into my inventory. \n" ..
- "When done, press [Enter] to confirm.\n\n" ..
- "Oh, and you can place the blocks into \n" ..
- "any slot you want, I'll put them into \n" ..
- "the right ones myself \\o/ \n\n" ..
- "Recommended: Stone, Dirt and possibly \n" ..
- "Gravel or Sand (depends on the biome).\n" ..
- (placeIgnored and
- "You may also want to add Cobblestone, \n" ..
- "to allow placing it while moving up."
- or "" ))
- private.prompt({keys.enter}, {})
- print("OK, let me see...")
- -- Move stuff from anywhere in the inventory into the first slots, so that
- -- we have a continuous interval of occupied slots.
- local slot, free = 1
- while slot <= 16 do
- if turtle.getItemCount(slot) > 0 then
- private.select(slot)
- for i = 1, slot - 1 do
- if turtle.compareTo(i) then
- if not turtle.transferTo(i) or
- turtle.getItemCount(slot) > 0
- then
- term.clear()
- term.setCursorPos(1, 1)
- print("Well, I tried to reduce the redundant \n" ..
- "stuff you gave me into one pile, but I \n" ..
- "failed horribly. Cut me some slack. ;) \n" ..
- "Please try only giving one block per \n" ..
- "block type, m'kay? \n")
- print("Press [Enter] to try again m(.,.)m")
- private.prompt({keys.enter}, {})
- -- Restarts the state.
- return nil
- end
- break
- end
- end
- if free and turtle.getItemCount(slot) > 0 then
- private.select(slot)
- turtle.transferTo(free)
- slot = free
- free = nil
- end
- end
- -- Might have become free while merging blocks.
- if turtle.getItemCount(slot) == 0 and free == nil then
- free = slot
- end
- slot = slot + 1
- end
- -- See how many blocks we have.
- local occupied = 0
- for slot = 1, 16 do
- if turtle.getItemCount(slot) > 0 then
- occupied = occupied + 1
- end
- end
- term.clear()
- term.setCursorPos(1, 1)
- if occupied == 0 then
- print("Woah, you want me to fetch, like, \n" ..
- "everything? Even stone? That'll mean a\n" ..
- "lot of additional running back and \n" ..
- "and forth, which is less efficient /o\\\n\n")
- write("Are you sure? [y/N] > ")
- if not private.prompt({keys.y}, {keys.n, keys.enter}) then
- -- Restarts the state.
- return nil
- end
- print("Oh boy... well, here goes nothing!")
- elseif occupied == 16 then
- print("You can't be serious, right? There's \n" ..
- "just no way this could possibly work.\n" ..
- "I need at least one free slot into \n" ..
- "which I can put the stuff I dig up. \n")
- print("Press [Enter] to try again -.-")
- private.prompt({keys.enter}, {})
- -- Restarts the state.
- return nil
- elseif occupied == 15 and placeTorches then
- print("Nice try, but this can't work in my \n" ..
- "current configuration, since I need \n" ..
- "one additional slot for the torches \n" ..
- "I'm supposed to place. So I need some\n" ..
- "more room to store the stuff I dig up\n" ..
- "in. Meaning at least two free slots \n" ..
- "in total. Can you do that for me? \n")
- print("Press [Enter] to try again :/")
- private.prompt({keys.enter}, {})
- -- Restarts the state.
- return nil
- elseif placeTorches and occupied >= torchSlot then
- print("Unless you messed with the code this \n" ..
- "should not happen: the inventory \n" ..
- "range used for blocks to ignore \n" ..
- "intersects with the torch. You'll \n" ..
- "have to specify less than " ..
- (torchSlot - 1) .. " block \n" ..
- "types. Press [Enter] to try again... \n")
- private.prompt({keys.enter}, {})
- -- Restarts the state.
- return nil
- elseif occupied > 6 then
- print("While this is technically a feasible \n" ..
- "configuration, I'd highly recommend \n" ..
- "adding fewer blocks to ignore. The \n" ..
- "more blocks I have to check the more \n" ..
- "often I have to go back to clear my \n" ..
- "inventory, and the longer it takes \n" ..
- "to check if I may dig up a block ^.- \n\n" ..
- "Is this really what you want? ")
- write("[Y/n] > ")
- if not private.prompt() then
- -- Restarts the state.
- return nil
- end
- print("Well, if you say so... ley's do this!")
- else
- local agree = {
- "Sounds reasonable.",
- "Yeah, that seems about right.",
- "As you command, my master!",
- "I humbly obey.",
- "Wow, you've got taste.",
- "Off I go then!",
- "Here I go!",
- "Work, work."
- }
- print(agree[math.random(1, #agree)])
- end
- os.sleep(1.5)
- return occupied
- end
- --[[
- An automated version for getting the blocks we should ignore.
- ]]
- function private.setupIgnoredBlocksFromIgnoreChest()
- lama.turn(torchSide)
- private.select(1)
- assert(turtle.detect(), "Chest with ignored blocks disappeared!")
- -- If we're resuming in here, it means we only have stuff we're supposed
- -- to ignore in our inventory. Dump it back and start over.
- for slot = 1, 16 do
- if turtle.getItemCount(slot) > 0 then
- private.select(slot)
- assert(turtle.drop(), "Chest with blocks to ignore is full!")
- end
- end
- -- Suck as often as we can. If there's more, silently ignore it.
- private.select(1)
- for slot = 1, 15 do
- if not turtle.suck() then
- -- Stop if there's nothing else in there.
- break
- end
- end
- -- Put everything except one item back.
- for slot = 1, 15 do
- if turtle.getItemCount(slot) > 0 then
- private.select(slot)
- turtle.drop(turtle.getItemCount(slot) - 1)
- else
- break
- end
- end
- -- Merge duplicates, compact the range and dump duplicates.
- for slot = 2, 16 do
- -- Ignore empty slots.
- if turtle.getItemCount(slot) > 0 then
- -- Compare to all preceding slots
- for otherSlot = 1, slot - 1 do
- if turtle.compareTo(otherSlot) then
- -- Got one of the same kind, try to merge.
- if not turtle.transferTo(otherSlot) or
- turtle.getItemCount(slot) > 0
- then
- -- Could not merge (other stack already full) or still
- -- have some left (other stack is now full and there's
- -- still some left here). Drop the remainder.
- assert(turtle.drop(),
- "Chest with blocks to ignore is full!")
- end
- end
- end
- end
- -- If there's still something in the slot we didn't find anything of
- -- the same kind in our inventory. Look for an empty slot at a lower
- -- index to get a continuous segment.
- if turtle.getItemCount(slot) > 0 then
- for otherSlot = 1, slot - 1 do
- if turtle.getItemCount(otherSlot) == 0 then
- turtle.transferTo(otherSlot)
- break
- end
- end
- end
- end
- -- Count the remaining number of ignored block types and return that.
- local count = 0
- for slot = 1, 16 do
- if turtle.getItemCount(slot) > 0 then
- count = count + 1
- else
- break
- end
- end
- return count
- end
- --[[
- Reads a single key input from the user and returns whether the prompt was
- confirmed or denied.
- @param yesKeys a single or multiple keys that indicate success.
- @param noKeys a single or multiple keys that indicate denial.
- @return whether the user accepted or denied the prompt.
- ]]
- function private.prompt(yesKeys, noKeys)
- yesKeys = yesKeys or {keys.y, keys.enter}
- noKeys = noKeys or {keys.n}
- if not type(yesKeys) == "table" then
- yesKeys = {yesKeys}
- end
- if not type(noKeys) == "table" then
- noKeys = {noKeys}
- end
- local result
- term.setCursorBlink(true)
- repeat
- local _, code = os.pullEvent("key")
- for _, k in pairs(yesKeys) do
- if code == k then
- result = true
- break
- end
- end
- for _, k in pairs(noKeys) do
- if code == k then
- result = false
- break
- end
- end
- until result ~= nil
- term.setCursorBlink(false)
- return result
- end
- --[[
- Checks if two files are potentially different by comparing their file
- sizes.
- ]]
- function private.areDifferent(file1, file2)
- local s1 = type(file1) == "string" and fs.getSize(file1) or file1
- local s2 = fs.getSize(file2)
- if s1 == 512 or s2 == 512 then
- return true
- end
- s1 = type(file1) == "string" and s1 - fs.getName(file1):len() or s1
- s2 = s2 - fs.getName(file2):len()
- return s1 ~= s2
- end
- --[[
- Formats the attached disk drive.
- ]]
- function private.formatDisk()
- if not disk.isPresent(rsDiskSide) then
- return true
- end
- local diskPath = disk.getMountPath(rsDiskSide)
- if #fs.list(diskPath) < 1 then
- return true
- end
- print("This will format the disk in the \n" ..
- "attached disk drive. All data will be \n" ..
- "irretrievably lost. \n\n" ..
- "Are you sure? [Y/n]")
- if not private.prompt() then
- print("Aborting.")
- -- Eat key event so it doesn't propagate into the shell.
- os.sleep(0.1)
- return false
- end
- print("Formatting disk...")
- for _, file in pairs(fs.list(diskPath)) do
- local path = fs.combine(diskPath, file)
- print(" > " .. file .. (fs.isDir(path) and "/*" or ""))
- fs.delete(path)
- os.sleep(0.1)
- end
- return true
- end
- --[[
- Resets the disk and program and removes our startup script.
- ]]
- function private.reset()
- program:reset()
- startup.remove("jam")
- if not disk.isPresent(rsDiskSide) or not disk.getMountPath(rsDiskSide) then
- lama.turn((diskSide + 2) % 4)
- end
- private.waitForDiskDrive(0.5)
- return private.formatDisk()
- end
- -------------------------------------------------------------------------------
- -- Job position related stuff --
- -------------------------------------------------------------------------------
- --[[
- Utility function to determine how many holes we can dig so that our spiral
- doesn't exceed the specified bounds.
- @param squareSize the size of the bounding square our dig operation has
- to fit into.
- @return the number of holes we will dig.
- @private
- ]]
- function private.computeJobCount(squareSize)
- if squareSize < 0 then
- return 0
- end
- -- Subtract one for the center, then divide by six, because each following
- -- layer adds an additional six blocks.
- local spiralRadius = math.floor((squareSize - 1) / 6)
- -- Since we still have a square and we now know the number of layers, the
- -- number of holes is simply the length of one side squared.
- local sideLength = spiralRadius * 2 + 1
- return sideLength * sideLength
- end
- --[[
- Computes the actual x and y coordinates of the nth hole.
- @param n the number of the hole to compute the coordinates for.
- @return (x, y) being heightless the coordinates of the nth hole.
- @private
- ]]
- function private.computeCoordinates(n)
- -- Adjust to zero indexed system.
- n = n - 1
- -- If we're at the origin we can return right away. In fact, we should,
- -- since we'd get a division by zero in the following...
- if n < 1 then
- return 0, 0
- end
- -- Compute the coordinates on a plain old rectangular spiral, first.
- local shell = math.floor((math.sqrt(n) + 1) / 2)
- local tmp = (2 * shell - 1); tmp = tmp * tmp
- local leg = math.floor((n - tmp) / (2 * shell))
- local element = (n - tmp) - (2 * shell * leg) - shell + 1
- local x, y
- if leg == 0 then
- x, y = shell, element
- elseif leg == 1 then
- x, y = -element, shell
- elseif leg == 2 then
- x, y = -shell, -element
- else
- x, y = element, -shell
- end
- -- Then map it to our knights move grid.
- return x * 2 - y, y * 2 + x
- end
- --[[
- Computes how much fuel we should stock up on for making sure we can safely
- travel to the specified dig site, dig it up, and come back.
- ]]
- function private.computeExpeditionCost(job)
- local x, z = private.computeCoordinates(job)
- -- Start at home, move to the job location (layer enforced by function).
- local path = private.generatePath(0, 0, 0, x, jobLevel, z, "job")
- -- Digging faaar down, worst case scenario.
- table.insert(path, {x = x, y = -255, z = z})
- -- Append our way back home (again, move layer enforced by generatePath()).
- local pathBack = private.generatePath(x, jobLevel, z, 0, 0, 0, "home")
- for _, point in ipairs(pathBack) do
- table.insert(path, point)
- end
- return private.computeFuelNeeded(path)
- end
- --[[
- Computes how much fuel is needed to travel along the specified path.
- @param path the path to compute the fuel requirement for.
- @return the required fuel to travel the path.
- @private
- ]]
- function private.computeFuelNeeded(path)
- local function fuel(from, to)
- local dx = math.abs(to.x - from.x)
- local dy = math.abs(to.y - from.y)
- local dz = math.abs(to.z - from.z)
- return dx + dy + dz
- end
- local result = 0
- local previous = nil
- for _, current in ipairs(path) do
- if previous then
- result = result + fuel(previous, current)
- end
- previous = current
- end
- return result
- end
- --[[
- Computes a path leading from or to the docking station, from and to to the
- specified coordinates.
- When moving away from the docking station we move in the layer above it,
- when returning to it in the layer below it.
- ]]
- function private.generatePath(sx, sy, sz, tx, ty, tz, target)
- if sx == tx and sz == tz then
- if sy == ty then
- return {}
- end
- return {{x = tx, y = ty, z = tz}}
- end
- local layer
- if target == "home" then
- layer = backLevel
- elseif target == "end" then
- layer = 2
- elseif target == "job" then
- layer = awayLevel
- else
- error("'target' is invalid")
- end
- return {
- -- Start at the base station.
- {x = sx, y = sy, z = sz},
- -- Move one up to the layer we use for moving away.
- {x = sx, y = layer, z = sz},
- -- Move to above the actual coordinates but on the same layer.
- {x = tx, y = layer, z = tz},
- -- Move down to where we want to go.
- {x = tx, y = ty, z = tz}
- }
- end
- -------------------------------------------------------------------------------
- -- Working turtle stuff --
- -------------------------------------------------------------------------------
- --[[
- Sends a message via WIFI if available.
- @param the message category, i.e. the type of the message.
- @param the actual message content.
- ]]
- function private.sendMessage(category, message)
- if private.noModem then
- return
- elseif not private.modem then
- if peripheral.isPresent("right") and
- peripheral.getType("right") == "modem"
- then
- private.modem = peripheral.wrap("right")
- elseif peripheral.isPresent("left") and
- peripheral.getType("left") == "modem"
- then
- private.modem = peripheral.wrap("left")
- else
- -- Don't try again.
- private.noModem = true
- return
- end
- end
- local packet = {}
- packet.source = os.getComputerID()
- packet.sourceLabel = os.getComputerLabel()
- packet.category = category
- packet.message = message
- private.modem.transmit(sendChannel, 0, textutils.serialize(packet))
- end
- function private.waitForDiskDrive(secondsToWait)
- -- Wait a bit to finish moving and connect to the disk drive. This is
- -- really weird, but apparently a disk drive can be 'present' before it is
- -- mounted. So we wait until it's actually mounted... damn timining
- -- problems. We limit our number of tries, in case the disk drive was
- -- removed or - by some *very* slim chance - we're not in the right spot.
- -- Try for a total of five seconds, so lag is a non-issue.
- for i = 1, secondsToWait * 10 do
- if disk.isPresent(rsDiskSide) and disk.getMountPath(rsDiskSide) then
- return true
- end
- os.sleep(0.1)
- end
- return false
- end
- function private.readJobData()
- local diskPath = disk.getMountPath(rsDiskSide)
- local jobFileDisk = fs.combine(diskPath, jobFile)
- local file = assert(fs.open(jobFileDisk, "r"),
- "Bad disk, could not find job file.")
- local jobData = textutils.unserialize(file.readAll())
- file.close()
- assert(type(jobData) == "table", "Bad disk, invalid job data.")
- return jobData
- end
- --[[
- Checks if all inventory slots are occupied.
- ]]
- function private.isInventoryFull()
- for slot = 1, 16 do
- if turtle.getItemCount(slot) == 0 then
- return false
- end
- end
- return true
- end
- --[[
- Gets the total number of items in the turtles inventory.
- ]]
- function private.itemCount()
- local count = 0
- for slot = 1, 16 do
- count = count + turtle.getItemCount(slot)
- end
- return count
- end
- --[[
- Replacement for turtle.select() that tracks the currently selected slot so
- selecting the same slot again becomes a no-op.
- ]]
- function private.select(slot)
- if slot ~= private.selectedSlot then
- turtle.select(slot)
- private.selectedSlot = slot
- end
- end
- --[[
- Tries to empty out any inventories and then dig up the block.
- ]]
- function private.suckAndDig(suck, dig)
- suck = suck or turtle.suck
- dig = dig or turtle.dig
- while true do
- if private.isInventoryFull() then
- return nil
- end
- if not suck() then
- local count = private.itemCount()
- local result = dig()
- return result, count ~= private.itemCount()
- end
- end
- end
- --[[
- Sorts the lookup table for ignore slots so that we iterate them in the
- order of their frequencies.
- This takes some measures to avoid rebuilding the sort order
- ]]
- function private.computeIgnoreOrder(ignoreCount, ignoreSamples)
- if not private.orderedIgnoreSlots then
- -- Re-use the table to go easy on the GC.
- private.orderedIgnoreSlots = {}
- for i = 1, ignoreCount do
- table.insert(private.orderedIgnoreSlots, i)
- end
- end
- -- This is done quite often, but since this table will be really small it
- -- doesn't matter.
- if ignoreSamples then
- local function comparator(slotA, slotB)
- return (ignoreSamples[slotA] or 0) > (ignoreSamples[slotB] or 0)
- end
- table.sort(private.orderedIgnoreSlots, comparator)
- end
- return private.orderedIgnoreSlots
- end
- --[[
- Used to repeat a move until it succeeds. This *should* be unnecessary. I'm
- pretty sure I fixed LAMA up so that this it cannot happen, unless there's
- really an unbreakable block in our way all of a sudden (player placed?).
- But you just never know, so let's just keep trying hard...
- ]]
- function private.forceMove(where, move)
- repeat
- local success, reason = move()
- if not success then
- log:warn("%s: unexpectedly couldn't move (%s)", where, reason)
- -- This really shouldn't happen ever anyway, but if it does, don't
- -- spam too hard.
- os.sleep(30)
- end
- until success
- end
- --[[
- -- Digs out a level either on the way up or down.
- ]]
- function private.digLevel(ignoreCount, ignoreSamples, goingDown)
- -- Dig out two sides each while going down and up. Minimize turns by
- -- alternating the order between even and odd levels.
- local sides = ({
- -- Downwards.
- [true] = {
- -- Even layer.
- [true] = {lama.side.north, lama.side.east},
- -- Odd layer.
- [false] = {lama.side.east, lama.side.north}
- },
- -- Upwards.
- [false] = {
- -- Even layer.
- [true] = {lama.side.south, lama.side.west},
- -- Odd layer.
- [false] = {lama.side.west, lama.side.south}
- }
- })[goingDown][lama.getY() % 2 == 0]
- -- Get the order in which to check the ignored items.
- local order = private.computeIgnoreOrder(ignoreCount, ignoreSamples)
- local droppedChanged = false
- for _, side in ipairs(sides) do
- lama.turn(side)
- if turtle.detect() then
- -- There's something there! Check if we should ignore it.
- local ignored = false
- for i = 1, ignoreCount do
- local slot = order[i]
- private.select(slot)
- if turtle.compare() then
- -- Yep, ignore this one. Update our sampling.
- ignored = true
- ignoreSamples = ignoreSamples or {}
- ignoreSamples[slot] = (ignoreSamples[slot] or 0) + 1
- droppedChanged = true
- break
- end
- end
- if not ignored then
- private.select(ignoreCount + 1)
- if private.suckAndDig() == nil then
- -- Only returns nil if the inventory is full. Early exit in
- -- that case to potentially avoid an unnecessary turn.
- break
- end
- end
- end
- end
- return droppedChanged and ignoreSamples or nil
- end
- --[[
- Drop the contents of the specified slot, keeping only a specific amount.
- This keeps trying to drop until it succeeds.
- ]]
- function private.drop(slot, keep)
- keep = keep or 0
- -- Don't spam the log. We DO spam the WIFI, though, since that can fail.
- local didLogFull = false
- while turtle.getItemCount(slot) > keep do
- lama.turn(dropSide)
- assert(turtle.detect(), "The dropbox disappeared!")
- private.select(slot)
- if not turtle.drop(turtle.getItemCount(slot) - keep) then
- private.sendMessage("full", "The drop chest is full!")
- if not didLogFull then
- didLogFull = true
- log:warn("The drop chest is full! Waiting...")
- end
- os.sleep(5)
- end
- end
- end
- --[[
- Refuels the turtle to the specified fuel level. This keeps retrying until
- it succeeds.
- ]]
- function private.refuel(needed, ignoreCount)
- -- Don't spam the log. We DO spam the WIFI, though, since that can fail.
- local didLogNoFuel = false
- local fuelSlot = ignoreCount + 1
- while turtle.getFuelLevel() < needed do
- private.select(fuelSlot)
- if turtle.getItemCount(fuelSlot) == 0 then
- lama.turn(fuelSide)
- assert(turtle.detect(), "The fuel chest disappeared!")
- if not turtle.suck() then
- private.sendMessage("fuel", "We're all out fuel!")
- if not didLogNoFuel then
- didLogNoFuel = true
- log:warn("The fuel chest is empty! Waiting...")
- end
- os.sleep(5)
- end
- else
- while turtle.getItemCount(fuelSlot) > 0 and
- turtle.getFuelLevel() < needed
- do
- if not lama.refuel(1) then
- private.drop(fuelSlot)
- break
- end
- end
- turtle.drop()
- end
- end
- private.drop(fuelSlot)
- end
- --[[
- Restock on torches to get a full stack. This keeps retrying until it
- succeeds.
- ]]
- function private.restockTorches(ignoreCount)
- -- Don't spam the log. We DO spam the WIFI, though, since that can fail.
- local didLogNoTorches = false
- local tempSlot = ignoreCount + 1
- while turtle.getItemCount(torchSlot) < (256 / torchFrequency) do
- -- Select an empty slot because we cannot control how much we pull,
- -- then replenish our actual torch stack from that and drop the rest.
- private.select(tempSlot)
- -- Make sure our temp slot is empty or has torches in it.
- if turtle.getItemCount(tempSlot) > 0 and
- -- If we don't have any torches left we cannot be sure that whatever
- -- we have in our temp slot are torches, so we drop them. Otherwise
- -- we assume whatever is in our torch slot are guaranteed to be
- -- torches.
- (turtle.getItemCount(torchSlot) == 0 or
- not turtle.compareTo(torchSlot))
- then
- private.drop(tempSlot)
- end
- lama.turn(torchSide)
- assert(turtle.detect(), "The torch chest disappeared!")
- -- At this point we can be sure that if there's something in our temp
- -- slot it's torches, so use them up first, before sucking up more.
- if turtle.getItemCount(tempSlot) > 0 or turtle.suck() then
- turtle.transferTo(torchSlot)
- -- Put back any surplus.
- turtle.drop()
- else
- private.sendMessage("torches", "We're all out of torches!")
- if not didLogNoTorches then
- didLogNoTorches = true
- log:warn("The torch chest is empty! Waiting...")
- end
- os.sleep(5)
- end
- end
- private.drop(tempSlot)
- end
- -------------------------------------------------------------------------------
- -- Environment checking --
- -------------------------------------------------------------------------------
- assert(turtle, "JAM only works on turtles.")
- assert(type(stateFile) == "string" and stateFile ~= "",
- "The setting 'stateFile' must be a non-empty string.")
- assert(type(jobFile) == "string" and jobFile ~= "",
- "The setting 'jobFile' must be a non-empty string.")
- assert(rawget(lama.side, dropSide),
- "The setting 'dropSide' must be a valid lama.side.")
- assert(rawget(lama.side, fuelSide),
- "The setting 'fuelSide' must be a valid lama.side.")
- assert(rawget(lama.side, torchSide),
- "The setting 'torchSide' must be a valid lama.side.")
- assert(rawget(lama.side, diskSide),
- "The setting 'diskSide' must be a valid lama.side.")
- assert(dropSide ~= fuelSide and fuelSide ~= diskSide and
- dropSide ~= diskSide and diskSide ~= torchSide and
- dropSide ~= torchSide and fuelSide ~= torchSide,
- "Duplicate side configuration detected. Make sure the side for each " ..
- "docking bay element (disk, drop, fuel and torch chest) is different.")
- assert(type(rsDiskSide) == "string" and rsDiskSide ~= "",
- "'rsDiskSide' is invalid; did you mess with the code?")
- -------------------------------------------------------------------------------
- -- Initialization --
- -------------------------------------------------------------------------------
- -- Command line argument parsing.
- local args = {...}
- if args[1] == "reset" then
- return private.reset()
- elseif args[1] == "install" then
- if not private.reset() then
- return false
- end
- elseif #args > 0 then
- print("Usage: jam [command]")
- print("Commands:")
- print(" reset - resets the program state")
- print(" and formats the disk.")
- print(" install - resets and then runs JAM.")
- print()
- print("If no command is given, JAM is run")
- print("normally, either running the wizard")
- print("or resuming from a previous state.")
- return false
- end
- -- Make sure LAMA is initialized.
- lama.init()
- -- Run our program.
- program:run()
- -- If the program returns normally we've reached our final position, shut down.
- os.shutdown()
Add Comment
Please, Sign In to add comment