diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89fae0d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.log +.treefarm.conf diff --git a/dig.lua b/dig.lua new file mode 100644 index 0000000..79c39c3 --- /dev/null +++ b/dig.lua @@ -0,0 +1,85 @@ +-- Created by Jef Roosens +-- +-- This program lets you dig out a rectangle +-- TODO return to surface option +-- TODO arbitrary height instead of multiple of 3 +-- TODO item storage solution + +local logger = require("jlib.log").Logger:new{level = 3, filename = "dig.log"} +local input = require("jlib.input") +input.logger = logger +local move = require("jlib.move").move +move.logger = logger +local fuel = require("jlib.fuel") +fuel.logger = logger + + +local function layer(width, depth, offset) + for i = 1, width do + -- Mine slice + move('f', depth - 1, { turtle.digUp, turtle.digDown }) + + -- Move to next slice + if i < width then + dir = (i + offset) % 2 + + if dir == 0 then turtle.turnLeft() else turtle.turnRight() end + move('f', 1, { turtle.digUp, turtle.digDown }) + if dir == 0 then turtle.turnLeft() else turtle.turnRight() end + end + end +end + + +local function dig(width, depth, height, dir) + -- Dig initial blocks + turtle.digUp() + turtle.digDown() + + local layers = height / 3 + for j = 1, layers do + local offset = 0 + + if j % 2 == 0 and width % 2 == 0 then offset = 1 end + layer(width, depth, offset) + -- Move down a layer + + if j < layers then + move(dir, 3) + if dir == 'd' then turtle.digDown() else turtle.digUp() end + turtle.turnLeft() + turtle.turnLeft() + end + end +end + + +local function main(args) + -- Take inputs + local width = tonumber(args[1]) or input.read_int("Width: ") + local depth = tonumber(args[2]) or input.read_int("Depth: ") + local height = tonumber(args[3]) or input.read_int("Height: ") + -- TODO add check for dir + local dir = args[4] + + -- Make sure height is valid + -- TODO make program work with any height + if height % 3 ~= 0 then + return logger:log(3, "Height is not a multiple of 3.") + end + + -- Check fuel level + -- The + 2 accounts for the moving between layers + local fuel_needed = (depth * width + 3) * (height / 3) + if not fuel.check(fuel_needed) then + return logger:log(1, "Insufficient fuel.") + end + + -- Do the dig + dig(width, depth, height, dir) + + logger:close() +end + + +main({...}) diff --git a/jlib/fuel.lua b/jlib/fuel.lua new file mode 100644 index 0000000..1787958 --- /dev/null +++ b/jlib/fuel.lua @@ -0,0 +1,115 @@ +-- Created by Jef Roosens +-- +-- This module contains functions which ease fuel management in a turtle + + +-- local turtle = require('turtle') +local module = {} +module.logger = require("jlib.log").Logger:new{level = -1} + + +-- Refuel the turtle using the given slot; tries to optimize refuel speed by +-- calculating needed refuel count +-- +-- @param slot slot to refuel from +-- @param goal refuel goal to reach +-- @return wether the refuel was enough +function module.refuel_from(slot, goal) + -- First, we get the current level + local start = turtle.getFuelLevel() + + -- If we've already reached the goal, just return true + if start >= goal then return true end + + -- Select the slot and refuel once + turtle.select(slot) + turtle.refuel(1) + + -- Get the difference + local level = turtle.getFuelLevel() + local diff = level - start + + -- If the diff is 0 the slot is empty or can't refuel, so return false + if diff == 0 then return false end + + -- Calculate needed refuels + local needed = math.ceil((goal - level) / diff) + + -- Do the refuel + turtle.refuel(needed) + + -- Return wether or not we reached the goal + return turtle.getFuelLevel() >= goal + +end + + +-- Check if the turtle has enough fuel; takes a table of preferred items +-- +-- @param goal fuel goal to reach +-- @param preferred table containing names of preferred items +-- @return wether or not the fuel level was reached +function module.check(goal, preferred) + -- Convert table to set for faster lookup + local set = {} + + if preferred then + for _, v in ipairs(preferred) do + set[v] = true + end + end + + -- Turtle needs to be able to get down at all times + local i, done = 1, turtle.getFuelLevel() >= goal + + -- Instantly quit if it's already good + -- This check is mostly useful voor the logging aspect + if done then + module.logger:info("No refuel needed.") + return true + end + + -- Store all non-preferred options + local options = {} + + -- Iterate over all 16 slots, or stop earlier if possible + while not done and i <= 16 do + local data = turtle.getItemDetail(i) + + if data and set[data.name] then + done = module.refuel_from(i, goal) + + else + table.insert(options, i) + + end + + i = i + 1 + end + + -- Exit if goal has been reached + if done then + module.logger:info("Refueled using preferred.") + + return true + end + + -- Process non-preferred options + for _, j in pairs(options) do + done = module.refuel_from(j, goal) + + if done then + module.logger:info("Refueled using non-preferred.") + + return true + end + end + + module.logger:warning( + "Refuel failed. Missing: " .. goal - turtle.getFuelLevel()) + + return false +end + + +return module diff --git a/jlib/input.lua b/jlib/input.lua new file mode 100644 index 0000000..ce1ca92 --- /dev/null +++ b/jlib/input.lua @@ -0,0 +1,28 @@ +-- Created by Jef Roosens +-- +-- Useful functions for receiving input from the user + +local module = {} +module.logger = require("jlib.log").Logger:new{level = -1} + + +-- Read an integer +-- +-- @param prompt prompt string to show +-- @param negative wether or not the number can be negative; defaults to false +function module.read_int(prompt, negative) + while true do + write(prompt) + + local num = tonumber(read()) + if num and num >= 0 or not negative then + module.logger:info("Input: " .. num) + return num + end + + print("Please enter a valid number.") + end +end + + +return module diff --git a/jlib/log.lua b/jlib/log.lua new file mode 100644 index 0000000..ab69b9d --- /dev/null +++ b/jlib/log.lua @@ -0,0 +1,86 @@ +-- Created by Jef Roosens +-- +-- Module to add logging to a program. This module is integrated in all jlib +-- functions. + + +local module = {} + + +-- Define class with default values +local Logger = { + level = 2, + level_colors = { + colors.red, colors.orange, colors.yellow, colors.blue, colors.white}, + prefixes = {"CRIT", "ERROR", "WARN", "INFO", "DEBUG"}, + -- Explicit is better than implicit + filename = nil +} + + +-- Return a new logger 'object'. +-- +-- @param level log level: +-- (0) critical, (1) errors, (2) warnings, (3) info,(4) debug +-- @param level_colors table containing color names for messages; order is +-- { critical, errors, warnings, info, debug }. Only colors for the needed log +-- level need to be provided: e.g. if the log level is (2) warnings, then only +-- two colors need to be provided. If not provided, defaults to +-- { red, orange, yellow, blue, white }. +-- @param prefixes prefix to show for each log level +-- @param filename log file to write to. If not supplied, log will only be +-- outputted to stdout +-- @return a logger 'object' +function Logger:new(o) + o = o or {} + setmetatable(o, self) + self.__index = self + + -- Open file handler + if o.filename then + o._file = fs.open(o.filename, "a") + end + + return o +end + +-- Log a message with the given level +-- +-- @param level log level; levels higher than logger level get discarded +-- @param message message to log +-- @return wether or not the message was actually logged +function Logger:log(level, message) + -- Exit if the level is too high + if level > self.level then return false end + + -- Log to screen + local cur_color = term.getTextColor() + term.setTextColor(self.level_colors[level + 1]) + term.write("[" .. self.prefixes[level + 1] .. "] ") + term.setTextColor(cur_color) + print(message) + + -- Log to file + if self._file then + local line = "[" .. self.prefixes[level + 1] .. "] " .. message .. '\n' + self._file.write(line) + self._file.flush() + + end +end + +-- Just some convenience functions; wrappers around Logger:log +function Logger:critical(message) self:log(0, message) end +function Logger:error(message) self:log(1, message) end +function Logger:warning(message) self:log(2, message) end +function Logger:info(message) self:log(3, message) end +function Logger:debug(message) self:log(4, message) end + +-- Close the logger a.k.a. close the log file handler +function Logger:close() + if self._file then self._file.close() end +end + + +module.Logger = Logger +return module diff --git a/jlib/mine.lua b/jlib/mine.lua new file mode 100644 index 0000000..d029d19 --- /dev/null +++ b/jlib/mine.lua @@ -0,0 +1,32 @@ +-- Created by Jef Roosens + + +local module = {} +module.logger = require("jlib.log").Logger:new{level = -1} + + +-- Safely dig in given direction (aka account for falling blocks) +-- +-- @param dir direction to dig (f, u, d) +-- @return amount of blocks it broke +function module.dig(dir) + local funcs = { + f = { turtle.detect, turtle.dig }, + u = { turtle.detectUp, turtle.digUp }, + d = { turtle.detectDown, turtle.digDown } + } + + local i = 0 + + while funcs[dir][1]() do + funcs[dir][2]() + i = i + 1 + + end + + module.logger:info("Mined " .. i .. " blocks") + return i +end + + +return module diff --git a/jlib/move.lua b/jlib/move.lua new file mode 100644 index 0000000..f4ccfd0 --- /dev/null +++ b/jlib/move.lua @@ -0,0 +1,50 @@ +-- Created by Jef Roosens +-- +-- Utilities to ease movement using a turtle + + +local dig = require("jlib.mine").dig +local module = {} +module.logger = require("jlib.log").Logger:new{level = -1} + + +-- Safely move n blocks. This means accounting for gravel, trees growing... +-- +-- @param dir direction to move (u, d, f) +-- @param n number of blocks to move. If nil (or not supplied), 1 is taken as +-- @param side_effect a function/table of functions which gets called after the +-- turtle has moved 1 block. This can be used to extend the functionality of +-- the function without having to re-write it. +function module.move(dir, n, side_effect) + -- Default value + n = n or 1 + + local funcs = { + u = turtle.up, + d = turtle.down, + f = turtle.forward + } + + for _ = 1, n do + dig(dir) + funcs[dir]() + + -- Run the side-effect if given + if side_effect then + if type(side_effect) == "table" then + for _, f in pairs(side_effect) do + f() + end + + else + side_effect() + + end + end + end + + module.logger:info("Moved " .. n .. " blocks") +end + + +return module diff --git a/tree.lua b/tree.lua new file mode 100644 index 0000000..2f058e3 --- /dev/null +++ b/tree.lua @@ -0,0 +1,100 @@ +-- Created by Jef Roosens +-- TODO: fix bug where if you start a 2x2 tree in the middle, the turtle will still end up on the bottom + + +local fuel = require('jlib.fuel') +local move = require('jlib.move').move + + +-- Chop a tree in the given direction +local function chop(tree_type, two_deep, go_down, return_to_start) + local i, data = 0 + if go_down then _, data = turtle.inspectDown() else _, data = turtle.inspectUp() end + + while data.name == tree_type do + -- Make sure we have enough fuel to get back up + fuel.check(i + 1) + + if two_deep then turtle.dig() end + -- This also mines the block + if go_down then move('d') else move('u') end + + i = i + 1 + if go_down then _, data = turtle.inspectDown() else _, data = turtle.inspectUp() end + end + + -- Dig one higher. This allows us to chop jungle trees, which often leave one block higher + if i > 0 and not go_down then + fuel.check(i + 1) + if two_deep then turtle.dig() end + move('u') + + i = i + 1 + end + + -- Dig the final block + if two_deep then turtle.dig() end + + -- Because we check the fuel level every time, we know for certain we can get back up/down + if return_to_start then + if go_down then move('u', i) else move('d', i) end + end +end + + +-- Determine tree type +local _, data = turtle.inspect() +local tree_type = data.name + +if not fuel.check(1) then + print("I need 1 fuel to start.") + return +end + +-- Get into position +turtle.dig() +turtle.forward() + +-- Check if it's a two-deep tree +local _, data = turtle.inspect() +local two_deep, two_by_two, right = data.name == tree_type, false, false + +-- Check if it's a 2x2 tree +if two_deep then + turtle.turnRight() + local _, data = turtle.inspect() + turtle.turnLeft() + + two_by_two = data.name == tree_type + + -- Check if we're on the right side instead + if not two_by_two then + turtle.turnLeft() + _, data = turtle.inspect() + turtle.turnRight() + + two_by_two = data.name == tree_type + right = two_by_two + end +end + +-- Chop the actual tree +-- Chop left half +chop(tree_type, two_deep, true, true) +-- Come back down if it's not a two_by_two, else don't +chop(tree_type, two_deep, false, not two_by_two) + +-- Get into position if it's a two-by-two +if two_by_two then + if right then turtle.turnLeft() else turtle.turnRight() end + turtle.dig() + turtle.forward() + if right then turtle.turnRight() else turtle.turnLeft() end + + -- Go back down, or dig second half + chop(tree_type, two_deep, true, false) + + if right then turtle.turnRight() else turtle.turnLeft() end + turtle.forward() + if right then turtle.turnLeft() else turtle.turnRight() end +end diff --git a/treefarm.lua b/treefarm.lua new file mode 100644 index 0000000..e71e079 --- /dev/null +++ b/treefarm.lua @@ -0,0 +1,211 @@ +-- Created by Jef Roosens +local logger = require('jlib.log').Logger:new{level=3, filename="treefarm.log"} +local fuel = require('jlib.fuel') +fuel.logger = logger +local move = require('jlib.move').move + + +-- Chop a 1x1 tree +-- +-- @param max_height height of tallest known tree +-- +-- @return height of chopped tree if greater than known highest, otherwise +-- known highest +local function chop(max_height, log_name) + logger:log(3, "Chopping tree") + + -- Make sure we have enough fuel for the largest known tree + fuel.check(2 * max_height + 1, { log_name }) + + -- Move one forward + move('f') + + turtle.digDown() + + -- Chop up until we no longer find logs + local _, data = turtle.inspectUp() + + local height = 0 + while data.name == log_name do + -- These checks most likely always succeed instantly thanks to the + -- tallest tree system + fuel.check(height, { log_name }) + + move('u', 1) + _, data = turtle.inspectUp() + + height = height + 1 + end + + -- Move back down + move('d', height) + + -- Return the new height if it's greater than the old + + max_height = math.max(max_height, height) + logger:debug("Max height: " .. max_height) + + return max_height +end + + +-- Select given item +-- +-- @param item_name name of item to select +-- +-- @return wether or not the item could be selected +local function select_item(item_name) + for i = 1, 16 do + local data = turtle.getItemDetail(i) + + if data and data.name == item_name then + turtle.select(i) + return true + end + end + + return false +end + + +-- Do one cycle through the farm +-- +-- @param max_height height of tallest known tree +-- +-- @return new height of tallest known tree +local function cycle(width, depth, max_height, sapling_name, log_name, + spacing) + logger:info("Starting cycle") + + for i = 1, width do + -- Check for fuel to go through entire row + fuel.check((spacing + 1) * (depth - 1) + 2, { log_name }) + + for j = 1, depth do + local _, data = turtle.inspect() + + -- Chop down the tree, or move forward + if data.name == log_name then + -- Gotta re-do the check from the beginning of this row + fuel.check( + 2 * max_height + 1 + (spacing + 1) * (depth - j) + 2, + { log_name }) + + max_height = chop(max_height, log_name) + + else + move('f') + end + + -- Place down a sapling, if possible + if select_item(sapling_name) then + turtle.placeDown() + end + + -- Pick up any dropped saplings + turtle.suckDown() + + -- Move forward + + if j < depth then + move('f', spacing) + else + move('f') + end + end + + -- Turn + if i < width then + if i % 2 == 0 then turtle.turnLeft() else turtle.turnRight() end + fuel.check(spacing + 1, { log_name }) + move('f', spacing + 1) + if i % 2 == 0 then turtle.turnLeft() else turtle.turnRight() end + end + end + + return max_height +end + + +-- Do maintenance stuff: drop off items, and restock on saplings +local function maintenance(width, depth, sapling_name, log_name) + logger:info("Doing maintenance") + fuel.check(6, { log_name }) + + -- Move towards sapling chest + move('f') + turtle.turnLeft() + move('f') + + -- Deposite all saplings + while select_item(sapling_name) do turtle.drop(64) end + + -- Get enough saplings + turtle.select(1) + turtle.suck(width * depth) + + -- Move towards log chest + turtle.turnRight() + move('f') + turtle.turnLeft() + + -- Deposite all logs + while select_item(log_name) do turtle.drop(64) end + + -- Keep 10 logs for refueling + turtle.select(2) + turtle.suck(10) + + -- Get back to start block + turtle.turnRight() + turtle.turnRight() + move('f') + turtle.turnRight() + move('f', 2) + + logger:info("Maintenance finished") +end + + +local function main() + -- Load the settings, or quit if it fails + if not settings.load(".treefarm.conf") then + return logger:critical("Couldn't load configuration file. Please make sure it exists and doesn't contain syntax errors.") + end + + -- Get all variables (API calls are slow) + local width, depth, spacing, border_width, log_name, sapling_name, delay = + settings.get("width"), settings.get("depth"), settings.get("spacing"), + settings.get("border_width"), settings.get("log_name"), + settings.get("sapling_name"), settings.get("delay") + local max_height = 0 + + logger:info("Settings loaded") + + -- Start the main loop + logger:info("Starting main loop") + while true do + -- Get into start position for cycle + fuel.check(2, { log_name }) + move('f', 2) + + -- Run through two cycles + max_height = cycle(width, depth, max_height, sapling_name, log_name, + spacing) + turtle.turnRight() + turtle.turnRight() + sleep(delay) + max_height = cycle(width, depth, max_height, sapling_name, log_name, + spacing) + + -- Do maintenance and subtract maintenance time from delay + fuel.check(border_width, { log_name }) + move('f', border_width) + + local seconds = os.clock() + maintenance(width, depth, sapling_name, log_name) + sleep(math.max(0, delay - os.clock() + seconds)) + end +end + +main() diff --git a/tunnel.lua b/tunnel.lua new file mode 100644 index 0000000..35fb3f6 --- /dev/null +++ b/tunnel.lua @@ -0,0 +1,26 @@ +-- Created by Jef Roosens +-- +-- This script digs a 1x2 tunnel of a given length + +local logger = require("jlib.log").init_logger(3) +local fuel = require("jlib.fuel") +fuel.logger = logger +local move = require("jlib.move").move +local input = require("jlib.input") + + +local function main(args) + -- Get tunnel length + local length = tonumber(args[1]) or input.read_int("Tunnel length: ") + + -- Check if fuel is sufficient + if not fuel.check(length) then + return logger:log(1, "Insufficient fuel.") + end + + -- Do the digging + move('f', length, { turtle.digDown, turtle.digUp }) +end + + +main({...})