local Position = require("lib.position") local Direction = require("lib.direction") local Stack = require("lib.stack") ---@alias TurtleControllerConfig {fuel_direction: Direction, fuel_name: string, refuel_amount: number, refuel_safety_margin: number, storage_direction: Direction} ---@class TurtleController ---@field config TurtleControllerConfig ---@field position Position ---@field last_positions Stack ---@field mine_forward boolean ---@field mine_above boolean ---@field mine_below boolean local TurtleController = {} TurtleController.__index = TurtleController ---@return TurtleController function TurtleController:Create() local t = {} setmetatable(t, TurtleController) ----------------------------------------------------------------------------------------------- -- Fields ----------------------------------------------------------------------------------------------- t.config = { fuel_direction = Direction.EAST, fuel_name = turtle.getItemDetail(1) == nil and "" or turtle.getItemDetail(1).name, refuel_amount = 500, refuel_safety_margin = 160, storage_direction = Direction.WEST, } t.position = Position:Empty() t.last_positions = Stack:Create() t.mine_forward = false t.mine_above = false t.mine_below = false return t end ----------------------------------------------------------------------------------------------- -- Movement Methods ----------------------------------------------------------------------------------------------- ---Positive numbers turn clockwise, negative numbers turn counterclockwise ---@param number_of_turns number function TurtleController:TurnRelative(number_of_turns) if number_of_turns % 4 == 0 then return end -- Turn turtle local turns = number_of_turns % 4 if turns == 3 then -- print(("Turtle is turning by %d (shortened to %d)..."):format(number_of_turns, -1)) -- If we're rotating by 3, we could turn faster by rotating by 1 in the opposite direction turtle.turnLeft() else -- print(("Turtle is turning by %d (shortened to %d)..."):format(number_of_turns, turns)) for _ = 1, turns do turtle.turnRight() end end -- Update current_position self.position.dir = (self.position.dir + number_of_turns) % 4 end ---@param direction Direction function TurtleController:TurnToDirection(direction) if self.position.dir == direction then return end self:TurnRelative(direction - self.position.dir) end ---Move forward by a number of blocks depending on the current rotation ---@param number_of_blocks number ---@param skip_unstocking boolean | nil ---@param skip_refueling boolean | nil function TurtleController:MoveForward(number_of_blocks, skip_unstocking, skip_refueling) skip_unstocking = skip_unstocking or false skip_refueling = skip_refueling or false if number_of_blocks == 0 then return end -- print(("Turtle is moving forward by %d blocks..."):format(number_of_blocks)) for _ = 1, math.abs(number_of_blocks) do if not skip_refueling then self:RefuelIfEmpty() end if not skip_unstocking then self:UnstockIfFull() end -- Mine/Move if self.mine_forward then while not turtle.forward() do if self:TestForBlock("minecraft:chest") then error("Won't mine because a chest is in the way!") end turtle.dig() end elseif not turtle.forward() then error("Turtle failed to move forward!") end if self.mine_above then if self:TestForBlock("minecraft:chest", true, false) then error("Won't mine because a chest is in the way!") end turtle.digUp() end if self.mine_below then if self:TestForBlock("minecraft:chest", false, true) then error("Won't mine because a chest is in the way!") end turtle.digDown() end -- Update current position if self.position.dir == Direction.NORTH then self.position.z = self.position.z + 1 elseif self.position.dir == Direction.SOUTH then self.position.z = self.position.z - 1 elseif self.position.dir == Direction.EAST then self.position.x = self.position.x + 1 elseif self.position.dir == Direction.WEST then self.position.x = self.position.x - 1 end end end ---Positive numbers move up, negative numbers move down. ---@param number_of_blocks number ---@param skip_unstocking boolean | nil ---@param skip_refueling boolean | nil function TurtleController:MoveVertical(number_of_blocks, skip_unstocking, skip_refueling) skip_unstocking = skip_unstocking or false skip_refueling = skip_refueling or false if number_of_blocks == 0 then return end -- print(("Turtle is moving vertically by %d blocks..."):format(number_of_blocks)) local move_function = number_of_blocks > 0 and turtle.up or turtle.down local mine_function = number_of_blocks > 0 and turtle.digUp or turtle.digDown local mine_enabled = number_of_blocks > 0 and self.mine_above or self.mine_below for _ = 1, math.abs(number_of_blocks) do if not skip_refueling then self:RefuelIfEmpty() end if not skip_unstocking then self:UnstockIfFull() end -- Mine/Move if mine_enabled then while not move_function() do if self:TestForBlock("minecraft:chest", number_of_blocks > 0, number_of_blocks < 0) then error("Won't mine because a chest is in the way!") end mine_function() end elseif not move_function() then error("Turtle failed to move vertically!") end -- Update current position if number_of_blocks > 0 then self.position.y = self.position.y + 1 elseif number_of_blocks < 0 then self.position.y = self.position.y - 1 end end end ---Stores the current position on the stack so we can return using TurtleController:MoveBack() function TurtleController:StorePosition() self.last_positions:Push(Position:Copy(self.position)) end ---Move to an absolute position. Stores the current position on the stack so we can return using TurtleController:MoveBack() ---@param x number The EAST/WEST axis (grows from WEST to EAST) ---@param y number The UP/DOWN axis (grows from DOWN to UP) ---@param z number The NORTH/SOUTH axis (grows from SOUTH to NORTH) ---@param dir Direction ---@param skip_unstocking boolean | nil ---@param skip_refueling boolean | nil function TurtleController:MoveToPosition(x, y, z, dir, skip_unstocking, skip_refueling) -- print(("Turtle is moving to (%d, %d, %d)..."):format(x, y, z)) -- Store the current position on the stack, so we can return using TurtleController:MoveBack() self.last_positions:Push(Position:Copy(self.position)) -- Store mining config so we can restore it later local mine_forward = self.mine_forward local mine_above = self.mine_above local mine_below = self.mine_below -- EAST/WEST axis (do first to not interfere with chests) self:EnableMiningForward() self:DisableMiningAbove() self:DisableMiningBelow() if self.position.x > x then self:TurnToDirection(Direction.WEST) elseif self.position.x < x then self:TurnToDirection(Direction.EAST) end self:MoveForward(math.abs(x - self.position.x), skip_unstocking, skip_refueling) -- NORTH/SOUTH axis if self.position.z > z then self:TurnToDirection(Direction.SOUTH) elseif self.position.z < z then self:TurnToDirection(Direction.NORTH) end self:MoveForward(math.abs(z - self.position.z), skip_unstocking, skip_refueling) -- UP/DOWN axis self:DisableMiningForward() if y < self.position.y then self:EnableMiningBelow() elseif y > self.position.y then self:EnableMiningAbove() end self:MoveVertical(y - self.position.y, skip_unstocking, skip_refueling) -- Direction self:TurnToDirection(dir or Direction.NORTH) -- Restore mining config if mine_forward then self:EnableMiningForward() else self:DisableMiningForward() end if mine_above then self:EnableMiningAbove() else self:DisableMiningAbove() end if mine_below then self:EnableMiningBelow() else self:DisableMiningBelow() end -- Sanity check if not (self.position.x == x and self.position.y == y and self.position.z == z and self.position.dir == dir) then error("TurtleController:MoveToPosition failed to move to target position!") end end ---Move by a vector. Stores the current position on the stack so we can return using TurtleController:MoveBack() ---@param x number The EAST/WEST axis (grows from WEST to EAST) ---@param y number The UP/DOWN axis (grows from DOWN to UP) ---@param z number The NORTH/SOUTH axis (grows from SOUTH to NORTH) ---@param dir Direction ---@param skip_unstocking boolean | nil ---@param skip_refueling boolean | nil function TurtleController:MoveByVector(x, y, z, dir, skip_unstocking, skip_refueling) self:MoveToPosition( self.position.x + x, self.position.y + y, self.position.z + z, dir, skip_unstocking, skip_refueling ) end ---Move to the previously stored position and pop this position from the stack. ---@param skip_unstocking boolean | nil ---@param skip_refueling boolean | nil function TurtleController:MoveBack(skip_unstocking, skip_refueling) local last_position = self.last_positions:Pop() if last_position == nil then error("Failed to obtain last_position to move back!") else self:MoveToPosition( last_position.x, last_position.y, last_position.z, last_position.dir, skip_unstocking, skip_refueling ) -- Pop the stack because MoveToPosition pushes our current position self.last_positions:Pop() end end ----------------------------------------------------------------------------------------------- -- Testing Methods ----------------------------------------------------------------------------------------------- ---@param block_name string ---@param above boolean | nil ---@param below boolean | nil ---@return boolean function TurtleController:TestForBlock(block_name, above, below) above = above or false below = below or false if above and below then error("Can only test for blocks in one direction!") end local inspect_function = turtle.inspect if above then inspect_function = turtle.inspectUp elseif below then inspect_function = turtle.inspectDown end local has_block, block_data = inspect_function() if not has_block then return false end return block_data.name == block_name end ----------------------------------------------------------------------------------------------- -- Inventory Methods ----------------------------------------------------------------------------------------------- ---@return boolean function TurtleController:HasFuel() local level = turtle.getFuelLevel() local distance_home = self.position.x + self.position.y + self.position.z return level > distance_home + self.config.refuel_safety_margin end ---@return boolean function TurtleController:HasInventorySpace() for slot = 1, 16 do if turtle.getItemDetail(slot) == nil then return true end end return false end ---@param slot number ---@param count number ---@return boolean function TurtleController:SuckItemFromChest(slot, count) if not self:TestForBlock("minecraft:chest") then error("Can't suck item: no chest!") end local previous_slot = turtle.getSelectedSlot() turtle.select(slot or 1) local sucked = turtle.suck(count) turtle.select(previous_slot) return sucked end ---@param slot number ---@param count number ---@return boolean function TurtleController:DropItemIntoChest(slot, count) if not self:TestForBlock("minecraft:chest") then error("Can't drop item: no chest!") end local previous_slot = turtle.getSelectedSlot() turtle.select(slot or 1) local dropped = turtle.drop(count) turtle.select(previous_slot) return dropped end function TurtleController:DropInventoryIntoChest() if not self:TestForBlock("minecraft:chest") then error("Can't drop item: no chest!") end print("Dropping inventory into chest...") for slot = 1, 16 do turtle.select(slot) turtle.drop() end turtle.select(1) end ---@param skip_inventory_check boolean | nil function TurtleController:UnstockIfFull(skip_inventory_check) skip_inventory_check = skip_inventory_check or false if not skip_inventory_check and self:HasInventorySpace() then return end if skip_inventory_check then print(("HasInventorySpace() returned %s"):format(self:HasInventorySpace())) end print("Turtle is unstocking...") self:MoveToPosition(0, 0, 0, self.config.storage_direction, true, true) self:DropInventoryIntoChest() self:RefuelIfEmpty() self:TurnToDirection(Direction.NORTH) self:MoveForward(1) self:MoveBack(true, true) end ---@param skip_fuel_check boolean | nil function TurtleController:RefuelIfEmpty(skip_fuel_check) skip_fuel_check = skip_fuel_check or false if not skip_fuel_check and self:HasFuel() then return end if skip_fuel_check then print(("HasFuel() returned %s"):format(self:HasFuel())) end print("Turtle is refueling...") -- Clear our inventory into the storage chest self:MoveToPosition(0, 0, 0, self.config.storage_direction, true, true) self:DropInventoryIntoChest() -- Prepare refueling self:TurnToDirection(self.config.fuel_direction) turtle.select(1) -- Include the distance to the last position when refueling -- to keep the amount of work done between refuelings constant local target_fuel_level = self.config.refuel_amount local last_position = self.last_positions:Peek() if last_position == nil then error("Failed to obtain last_position while refueling!") else target_fuel_level = target_fuel_level + last_position.x + last_position.y + last_position.z end -- Refuel until we hit the refuel_amount local before_level = turtle.getFuelLevel() repeat if not self:SuckItemFromChest(1, 1) then error("Failed to suck fuel out of fuel chest!") end turtle.refuel() until turtle.getFuelLevel() >= target_fuel_level local after_level = turtle.getFuelLevel() self:TurnToDirection(Direction.NORTH) self:MoveForward(1) self:MoveBack(true, true) print( ("Refuelled %d units, current level is %d (old level was %d)"):format( after_level - before_level, after_level, before_level ) ) end ----------------------------------------------------------------------------------------------- -- Management Methods ----------------------------------------------------------------------------------------------- function TurtleController:Configure() local config_complete = false while not config_complete do -- Check for valid fuel while self.config.fuel_name == "" do print("Please put valid fuel into the first slot and press Enter!") local _ = io.read() self.config.fuel_name = turtle.getItemDetail(1) == nil and "" or turtle.getItemDetail(1).name end print("A chest with fuel has to be placed beside the turtle's starting position. Is this chest...") print(("...right of the turtle (enter %d)?"):format(Direction.EAST)) print(("...behind the turtle (enter %d)?"):format(Direction.SOUTH)) print(("...left of the turtle (enter %d)?"):format(Direction.WEST)) print("By default, the chest is assumed on the left.") self.config.fuel_direction = tonumber(io.read()) or Direction.WEST print("A chest to store mined blocks has to be placed beside the turtle's starting position. Is this chest...") print(("...right of the turtle (enter %d)?"):format(Direction.EAST)) print(("...behind the turtle (enter %d)?"):format(Direction.SOUTH)) print(("...left of the turtle (enter %d)?"):format(Direction.WEST)) print("By default, the chest is assumed on the right.") self.config.storage_direction = tonumber(io.read()) or Direction.EAST print("Configuration complete!") print( ('Turtle will consume "%s" for fuel. Put the desired fuel into the first slot to change.'):format( self.config.fuel_name ) ) print("Do you want to accept the configuration (enter 1, otherwise 0)?") config_complete = tonumber(io.read()) == 1 end end function TurtleController:EnableMiningForward() self.mine_forward = true end function TurtleController:DisableMiningForward() self.mine_forward = false end function TurtleController:EnableMiningAbove() self.mine_above = true end function TurtleController:DisableMiningAbove() self.mine_above = false end function TurtleController:EnableMiningBelow() self.mine_below = true end function TurtleController:DisableMiningBelow() self.mine_below = false end return TurtleController