cc-libs/programs/goo.lua

833 lines
17 KiB
Lua

local _VERSION = "1.0.0"
local args = table.pack(...)
local command = args[1]
local GOO_BLOCK_PREFIX = "justdirethings:gooblock_tier"
local MIN_START_FUEL = 50
local REFUEL_THRESHOLD = 1000
local WAIT_SECONDS = 2
local MIN_FEEDING_ITEMS = 4
local MAX_SUCK_ATTEMPTS = 16
local FUEL_ITEMS = {
"justdirethings:coal_t4",
"justdirethings:coal_t3",
"justdirethings:coal_t2",
"justdirethings:coal_t1",
"minecraft:coal",
}
local PROCESS_ITEMS = {
["minecraft:iron_block"] = { tier = 1 },
["minecraft:coal_block"] = { tier = 1 },
["mekanism:block_charcoal"] = { tier = 1 },
["justdirethings:coalblock_t1"] = { tier = 1 },
["justdirethings:coalblock_t2"] = { tier = 2 },
["justdirethings:coalblock_t3"] = { tier = 3 },
["justdirethings:coalblock_t4"] = { tier = 4 },
["minecraft:gold_block"] = { tier = 2 },
["minecraft:diamond_block"] = { tier = 3 },
["minecraft:netherite_block"] = { tier = 4 },
}
local FEEDING_ITEMS_BY_TIER = {
[1] = { "minecraft:sugar", "minecraft:rotten_flesh" },
[2] = { "minecraft:nether_wart" },
[3] = { "minecraft:chorus_fruit" },
[4] = { "minecraft:sculk" },
}
local DIR_NORTH = 0
local DIR_EAST = 1
local DIR_SOUTH = 2
local DIR_WEST = 3
local DIRECTION_DELTAS = {
[DIR_NORTH] = { x = 0, z = -1 },
[DIR_EAST] = { x = 1, z = 0 },
[DIR_SOUTH] = { x = 0, z = 1 },
[DIR_WEST] = { x = -1, z = 0 },
}
local HOME = { x = -2, y = 0, z = 0, facing = DIR_EAST }
local GOO_CHECK = { x = -1, y = 0, z = 0, facing = DIR_EAST }
local GOO_CHECKS = {
{ name = "west goo check", x = -1, y = 0, z = 0, facing = DIR_EAST, action = "forward" },
{ name = "east goo check", x = 1, y = 0, z = 0, facing = DIR_WEST, action = "forward" },
{ name = "north goo check", x = 0, y = 0, z = -1, facing = DIR_SOUTH, action = "forward" },
{ name = "south goo check", x = 0, y = 0, z = 1, facing = DIR_NORTH, action = "forward" },
{ name = "top goo check", x = 0, y = 1, z = 0, facing = DIR_EAST, action = "down" },
}
local TARGETS = {
{ name = "west side", x = -2, y = 0, z = 0, facing = DIR_EAST, action = "forward" },
{ name = "east side", x = 2, y = 0, z = 0, facing = DIR_WEST, action = "forward" },
{ name = "north side", x = 0, y = 0, z = -2, facing = DIR_SOUTH, action = "forward" },
{ name = "south side", x = 0, y = 0, z = 2, facing = DIR_NORTH, action = "forward" },
{ name = "top", x = -1, y = 1, z = 0, facing = DIR_EAST, action = "forward" },
}
local position = { x = HOME.x, y = HOME.y, z = HOME.z }
local facing = HOME.facing
local loggedBlockedItems = {}
local placedTargets = {}
local lastGooTier = nil
local function isFlag(name)
return function(arg)
return arg == "-" .. name or arg == "--" .. name
end
end
local isHelpFlag = isFlag("help")
local isVersionFlag = isFlag("version")
local function printUsage()
print("goo usage:")
print()
print("\t\t\tgoo start")
print("\t\t\tgoo version")
print("\t\t\tgoo help")
end
if command == "version" or isVersionFlag(command) then
print("goo v" .. _VERSION)
return
end
if command == nil or command == "" or command == "help" or isHelpFlag(command) then
printUsage()
return
end
if command ~= "start" then
printUsage()
return
end
if not turtle then
error("goo must be run on a turtle")
end
local function hasPickaxeEquipped()
local left = turtle.getEquippedLeft()
local right = turtle.getEquippedRight()
return (left and string.match(left.name or "", "_pickaxe$") ~= nil)
or (right and string.match(right.name or "", "_pickaxe$") ~= nil)
end
local function parseGooTier(blockName)
local tier = string.match(blockName or "", "^" .. GOO_BLOCK_PREFIX .. "(%d+)$")
return tonumber(tier)
end
local function isGooBlock(blockName)
return parseGooTier(blockName) ~= nil
end
local function isProcessBlock(blockName)
return PROCESS_ITEMS[blockName] ~= nil
end
local function isFuelItem(itemName)
for i = 1, #FUEL_ITEMS do
if FUEL_ITEMS[i] == itemName then
return true
end
end
return false
end
local function isFeedingItemForTier(itemName, gooTier)
local feedingItems = FEEDING_ITEMS_BY_TIER[gooTier] or {}
for i = 1, #feedingItems do
if feedingItems[i] == itemName then
return true
end
end
return false
end
local function countItem(itemName)
local count = 0
for slot = 1, 16 do
local item = turtle.getItemDetail(slot)
if item and item.name == itemName then
count = count + item.count
end
end
return count
end
local function countFeedingItems(gooTier)
local count = 0
local feedingItems = FEEDING_ITEMS_BY_TIER[gooTier] or {}
for i = 1, #feedingItems do
count = count + countItem(feedingItems[i])
end
return count
end
local function findItemSlot(itemName)
for slot = 1, 16 do
local item = turtle.getItemDetail(slot)
if item and item.name == itemName then
return slot, item
end
end
return nil
end
local function hasFreeSlot()
for slot = 1, 16 do
if turtle.getItemCount(slot) == 0 then
return true
end
end
return false
end
local function getFuelLevel()
local fuelLevel = turtle.getFuelLevel()
if fuelLevel == "unlimited" then
return nil
end
return fuelLevel
end
local function getRefuelTarget()
local fuelLimit = turtle.getFuelLimit()
if fuelLimit == "unlimited" or fuelLimit >= REFUEL_THRESHOLD then
return REFUEL_THRESHOLD
end
return fuelLimit
end
local function findFuelSlot()
for i = 1, #FUEL_ITEMS do
local slot, item = findItemSlot(FUEL_ITEMS[i])
if slot then
return slot, item
end
end
return nil
end
local function turnRight()
turtle.turnRight()
facing = (facing + 1) % 4
end
local function turnLeft()
turtle.turnLeft()
facing = (facing + 3) % 4
end
local function turnTo(direction)
while facing ~= direction do
local rightTurns = (direction - facing) % 4
if rightTurns == 3 then
turnLeft()
else
turnRight()
end
end
end
local function moveForward()
if not turtle.forward() then
return false
end
local delta = DIRECTION_DELTAS[facing]
position.x = position.x + delta.x
position.z = position.z + delta.z
return true
end
local function moveUp()
if not turtle.up() then
return false
end
position.y = position.y + 1
return true
end
local function moveDown()
if not turtle.down() then
return false
end
position.y = position.y - 1
return true
end
local function moveAxis(target, positiveDirection, negativeDirection, axis)
while position[axis] ~= target do
if position[axis] < target then
turnTo(positiveDirection)
else
turnTo(negativeDirection)
end
if not moveForward() then
return false
end
end
return true
end
local function goTo(target)
while position.y < target.y do
if not moveUp() then
return false
end
end
while position.y > target.y do
if not moveDown() then
return false
end
end
if not moveAxis(target.x, DIR_EAST, DIR_WEST, "x") then
return false
end
if not moveAxis(target.z, DIR_SOUTH, DIR_NORTH, "z") then
return false
end
if target.facing then
turnTo(target.facing)
end
return true
end
local function mustGoTo(target, description)
while not goTo(target) do
print("Cannot reach " .. description .. ". Clear the path and waiting...")
os.sleep(WAIT_SECONDS)
end
end
local function goHome()
mustGoTo(HOME, "home/provider")
end
local function goToGooCheck()
mustGoTo(GOO_CHECK, "goo check position")
end
local function inspectDirection(action)
if action == "forward" then
return turtle.inspect()
elseif action == "up" then
return turtle.inspectUp()
elseif action == "down" then
return turtle.inspectDown()
end
error("unknown inspect action " .. tostring(action))
end
local function placeDirection(action)
if action == "forward" then
return turtle.place()
elseif action == "up" then
return turtle.placeUp()
elseif action == "down" then
return turtle.placeDown()
end
error("unknown place action " .. tostring(action))
end
local function refuelFromInventory()
local fuelLevel = getFuelLevel()
if not fuelLevel or fuelLevel >= getRefuelTarget() then
return false
end
local previousSlot = turtle.getSelectedSlot()
local consumedFuel = false
while fuelLevel < getRefuelTarget() do
local slot, item = findFuelSlot()
if not slot then
break
end
turtle.select(slot)
if turtle.refuel(1) then
consumedFuel = true
print("Refueled with " .. item.name .. ". Fuel: " .. tostring(turtle.getFuelLevel()))
fuelLevel = getFuelLevel()
else
print("Could not refuel with " .. item.name .. ".")
break
end
end
turtle.select(previousSlot)
return consumedFuel
end
local function isProviderBelow()
local ok = turtle.inspectDown()
return ok
end
local function pullFromProvider()
goHome()
local pulled = false
for _ = 1, MAX_SUCK_ATTEMPTS do
if not hasFreeSlot() then
break
end
if turtle.suckDown() then
pulled = true
else
break
end
end
return pulled
end
local function shouldKeepItem(item, gooTier)
if isFuelItem(item.name) then
return true
end
local processItem = PROCESS_ITEMS[item.name]
if processItem and processItem.tier <= gooTier then
return true
end
if isFeedingItemForTier(item.name, gooTier) then
return countFeedingItems(gooTier) <= MIN_FEEDING_ITEMS
end
return false
end
local function depositOutputs(gooTier)
goHome()
local previousSlot = turtle.getSelectedSlot()
for slot = 1, 16 do
local item = turtle.getItemDetail(slot)
if item and not shouldKeepItem(item, gooTier) then
turtle.select(slot)
if turtle.dropDown() then
print("Deposited " .. item.name .. ".")
else
print("Could not deposit " .. item.name .. ". Provider may be full.")
break
end
end
end
turtle.select(previousSlot)
end
local function waitForProvider(message)
if message then
print(message)
end
repeat
os.sleep(WAIT_SECONDS)
until pullFromProvider()
end
local function ensureStartFuel()
while true do
local fuelLevel = getFuelLevel()
if not fuelLevel or fuelLevel >= MIN_START_FUEL then
return
end
refuelFromInventory()
fuelLevel = getFuelLevel()
if not fuelLevel or fuelLevel >= MIN_START_FUEL then
return
end
waitForProvider("Fuel is below " .. tostring(MIN_START_FUEL) .. ". Add fuel to the provider below home.")
end
end
local function ensureRuntimeFuel()
local fuelLevel = getFuelLevel()
if not fuelLevel then
return
end
if fuelLevel < getRefuelTarget() then
refuelFromInventory()
end
fuelLevel = getFuelLevel()
while fuelLevel and fuelLevel < MIN_START_FUEL do
waitForProvider("Fuel is below " .. tostring(MIN_START_FUEL) .. ". Add fuel to the provider below home.")
refuelFromInventory()
fuelLevel = getFuelLevel()
end
end
local function findFeedingSlot(gooTier)
local feedingItems = FEEDING_ITEMS_BY_TIER[gooTier] or {}
for i = 1, #feedingItems do
local slot, item = findItemSlot(feedingItems[i])
if slot then
return slot, item
end
end
return nil
end
local function findEligibleProcessSlot(gooTier)
for slot = 1, 16 do
local item = turtle.getItemDetail(slot)
local processItem = item and PROCESS_ITEMS[item.name]
if processItem then
if processItem.tier <= gooTier then
return slot, item, processItem
end
local logKey = item.name .. ":" .. tostring(gooTier)
if not loggedBlockedItems[logKey] then
print(
item.name
.. " requires goo tier "
.. tostring(processItem.tier)
.. ", current tier is "
.. tostring(gooTier)
)
loggedBlockedItems[logKey] = true
end
end
end
return nil
end
local function inspectGoo()
for i = 1, #GOO_CHECKS do
local check = GOO_CHECKS[i]
if goTo(check) then
local ok, inspected = inspectDirection(check.action)
if ok then
local tier = parseGooTier(inspected.name)
if tier then
lastGooTier = tier
return tier, inspected, check
end
end
end
end
if lastGooTier then
print("Cannot reach the goo to inspect it. Using last known tier " .. tostring(lastGooTier) .. ".")
return lastGooTier, { state = { alive = true } }, nil
end
error("expected a reachable Just Dire Things goo block around the turtle")
end
local function ensureGooAlive()
while true do
local gooTier, inspected, check = inspectGoo()
if not check or inspected.state and inspected.state.alive == true then
return gooTier
end
local slot, item = findFeedingSlot(gooTier)
if not slot then
waitForProvider("Goo tier " .. tostring(gooTier) .. " is not alive. Add feeding items to the provider.")
else
turtle.select(slot)
print("Goo tier " .. tostring(gooTier) .. " is not alive. Feeding with " .. item.name .. "...")
if not placeDirection(check.action) then
print("Could not feed the goo. Waiting before retry...")
os.sleep(WAIT_SECONDS)
else
os.sleep(1)
end
end
end
end
local function selectProcessItem(gooTier)
local slot, item = findEligibleProcessSlot(gooTier)
if not slot then
return nil
end
turtle.select(slot)
return item
end
local function inspectTarget(target)
if target.action == "forward" then
return turtle.inspect()
elseif target.action == "up" then
return turtle.inspectUp()
elseif target.action == "down" then
return turtle.inspectDown()
end
error("unknown target action " .. tostring(target.action))
end
local function digTarget(target)
if target.action == "forward" then
return turtle.dig()
elseif target.action == "up" then
return turtle.digUp()
elseif target.action == "down" then
return turtle.digDown()
end
error("unknown target action " .. tostring(target.action))
end
local function placeTarget(target)
if target.action == "forward" then
return turtle.place()
elseif target.action == "up" then
return turtle.placeUp()
elseif target.action == "down" then
return turtle.placeDown()
end
error("unknown target action " .. tostring(target.action))
end
local function ensureMiningSpace(gooTier)
while not hasFreeSlot() do
depositOutputs(gooTier)
if not hasFreeSlot() then
print("Inventory is full and provider may be full. Waiting...")
os.sleep(WAIT_SECONDS)
end
end
end
local function isTargetQueued(target)
for i = 1, #placedTargets do
if placedTargets[i] == target then
return true
end
end
return false
end
local function removeQueuedTarget(index)
table.remove(placedTargets, index)
end
local function goToTarget(target)
mustGoTo(target, target.name)
end
local function mineQueuedTarget(gooTier)
if #placedTargets == 0 then
return false
end
local target = placedTargets[1]
goToTarget(target)
ensureGooAlive()
goToTarget(target)
local ok, inspected = inspectTarget(target)
if not ok then
print("Queued " .. target.name .. " is empty.")
removeQueuedTarget(1)
return true
end
if isProcessBlock(inspected.name) then
return false
end
if isGooBlock(inspected.name) then
removeQueuedTarget(1)
return true
end
print("Mining processed " .. target.name .. " block: " .. tostring(inspected.name))
ensureMiningSpace(gooTier)
goToTarget(target)
if digTarget(target) then
removeQueuedTarget(1)
return true
end
print("Could not mine " .. target.name .. ".")
return false
end
local function placeAvailableTarget(gooTier)
for i = 1, #TARGETS do
local target = TARGETS[i]
if not isTargetQueued(target) then
goToTarget(target)
ensureGooAlive()
goToTarget(target)
local ok, inspected = inspectTarget(target)
if ok then
if not isProcessBlock(inspected.name) and not isGooBlock(inspected.name) then
print("Mining existing " .. target.name .. " block: " .. tostring(inspected.name))
ensureMiningSpace(gooTier)
goToTarget(target)
if digTarget(target) then
return true
end
end
else
local item = selectProcessItem(gooTier)
if not item then
return false
end
print("Placing " .. item.name .. " on goo " .. target.name)
if placeTarget(target) then
placedTargets[#placedTargets + 1] = target
return true
end
print("Could not place " .. target.name .. ".")
end
end
end
return false
end
local function waitAtOldestTarget()
if #placedTargets == 0 then
goToGooCheck()
else
goToTarget(placedTargets[1])
end
print("Waiting for goo processing...")
os.sleep(WAIT_SECONDS)
end
local function startup()
print("goo started. Layout: provider below turtle, goo two blocks ahead, turtle facing goo.")
if not hasPickaxeEquipped() then
error("goo requires a turtle with a pickaxe equipped")
end
if not isProviderBelow() then
error("goo requires a provider/deposit barrel below the turtle")
end
pullFromProvider()
ensureStartFuel()
end
startup()
while true do
ensureRuntimeFuel()
local gooTier = ensureGooAlive()
depositOutputs(gooTier)
pullFromProvider()
ensureRuntimeFuel()
gooTier = ensureGooAlive()
local changed = mineQueuedTarget(gooTier)
if not changed then
changed = placeAvailableTarget(gooTier)
end
if not changed then
if findEligibleProcessSlot(gooTier) then
waitAtOldestTarget()
else
waitForProvider("No eligible process block found. Add inputs to the provider below home.")
end
end
end