462 lines
14 KiB
Lua
462 lines
14 KiB
Lua
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
|
|
turtle.dig()
|
|
end
|
|
elseif not turtle.forward() then
|
|
error("Turtle failed to move forward!")
|
|
end
|
|
if self.mine_above then
|
|
turtle.digUp()
|
|
end
|
|
if self.mine_below then
|
|
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
|
|
if mine_enabled then
|
|
mine_function()
|
|
end
|
|
|
|
-- Move
|
|
if 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))
|
|
|
|
-- EAST/WEST axis (do first to not interfere with chests)
|
|
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:MoveVertical(y - self.position.y, skip_unstocking, skip_refueling)
|
|
|
|
-- Direction
|
|
self:TurnToDirection(dir or Direction.NORTH)
|
|
|
|
-- 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
|
|
|
|
|
|
-----------------------------------------------------------------------------------------------
|
|
-- 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:SuckItem(slot, count)
|
|
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:DropItem(slot, count)
|
|
local previous_slot = turtle.getSelectedSlot()
|
|
|
|
turtle.select(slot or 1)
|
|
local dropped = turtle.drop(count)
|
|
|
|
turtle.select(previous_slot)
|
|
|
|
return dropped
|
|
end
|
|
|
|
|
|
function TurtleController:DropInventory()
|
|
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:DropInventory()
|
|
self:RefuelIfEmpty()
|
|
|
|
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:DropInventory()
|
|
|
|
-- 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:SuckItem(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: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 |