From 9dd4455203298c91e2d64482a5e1593226484d92 Mon Sep 17 00:00:00 2001 From: Guillaume ARM Date: Mon, 1 Jun 2026 20:50:05 +0200 Subject: [PATCH] feat(goo): rewrite automation flow --- programs/goo.lua | 717 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 485 insertions(+), 232 deletions(-) diff --git a/programs/goo.lua b/programs/goo.lua index 1b046f6..eb718a4 100644 --- a/programs/goo.lua +++ b/programs/goo.lua @@ -1,4 +1,4 @@ -local _VERSION = "0.2.1" +local _VERSION = "1.0.0" local args = table.pack(...) local command = args[1] @@ -7,6 +7,8 @@ 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", @@ -36,7 +38,42 @@ local FEEDING_ITEMS_BY_TIER = { [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) @@ -82,11 +119,6 @@ local function hasPickaxeEquipped() or (right and string.match(right.name or "", "_pickaxe$") ~= nil) end -local function turnAround() - turtle.turnRight() - turtle.turnRight() -end - local function parseGooTier(blockName) local tier = string.match(blockName or "", "^" .. GOO_BLOCK_PREFIX .. "(%d+)$") @@ -101,20 +133,51 @@ local function isProcessBlock(blockName) return PROCESS_ITEMS[blockName] ~= nil end -local function inspectGoo() - local ok, inspected = turtle.inspectUp() - - if not ok then - error("expected a Just Dire Things goo block above the turtle") +local function isFuelItem(itemName) + for i = 1, #FUEL_ITEMS do + if FUEL_ITEMS[i] == itemName then + return true + end end - local tier = parseGooTier(inspected.name) + return false +end - if not tier then - error("expected a Just Dire Things goo block above the turtle, got " .. tostring(inspected.name)) +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 tier, inspected + 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) @@ -129,14 +192,6 @@ local function findItemSlot(itemName) return nil end -local function waitForInventory(message) - if message then - print(message) - end - - os.pullEvent("turtle_inventory") -end - local function hasFreeSlot() for slot = 1, 16 do if turtle.getItemCount(slot) == 0 then @@ -147,12 +202,6 @@ local function hasFreeSlot() return false end -local function ensureMiningSpace() - while not hasFreeSlot() do - waitForInventory("Inventory is full. Remove items to keep mining.") - end -end - local function getFuelLevel() local fuelLevel = turtle.getFuelLevel() @@ -185,6 +234,143 @@ local function findFuelSlot() 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() @@ -219,6 +405,83 @@ local function refuelFromInventory() 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() @@ -234,7 +497,7 @@ local function ensureStartFuel() return end - waitForInventory("Fuel is below " .. tostring(MIN_START_FUEL) .. ". Insert fuel to start.") + waitForProvider("Fuel is below " .. tostring(MIN_START_FUEL) .. ". Add fuel to the provider below home.") end end @@ -252,7 +515,7 @@ local function ensureRuntimeFuel() fuelLevel = getFuelLevel() while fuelLevel and fuelLevel < MIN_START_FUEL do - waitForInventory("Fuel is below " .. tostring(MIN_START_FUEL) .. ". Insert fuel to continue.") + waitForProvider("Fuel is below " .. tostring(MIN_START_FUEL) .. ". Add fuel to the provider below home.") refuelFromInventory() fuelLevel = getFuelLevel() end @@ -300,23 +563,51 @@ local function findEligibleProcessSlot(gooTier) 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 = inspectGoo() + local gooTier, inspected, check = inspectGoo() - if inspected.state and inspected.state.alive == true then + if not check or inspected.state and inspected.state.alive == true then return gooTier end local slot, item = findFeedingSlot(gooTier) if not slot then - waitForInventory("Goo tier " .. tostring(gooTier) .. " is not alive. Waiting for feeding item...") + 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 turtle.placeUp() then + if not placeDirection(check.action) then print("Could not feed the goo. Waiting before retry...") os.sleep(WAIT_SECONDS) else @@ -327,7 +618,7 @@ local function ensureGooAlive() end local function selectProcessItem(gooTier) - local slot, item, processItem = findEligibleProcessSlot(gooTier) + local slot, item = findEligibleProcessSlot(gooTier) if not slot then return nil @@ -335,245 +626,207 @@ local function selectProcessItem(gooTier) turtle.select(slot) - return item, processItem + return item end -local function workTarget(inspectFn, digFn, placeFn, gooTier, targetName) - local ok, inspected = inspectFn() - - if ok then - if isGooBlock(inspected.name) then - return false - end - - if isProcessBlock(inspected.name) then - return false - end - - print("Mining processed " .. targetName .. " block: " .. tostring(inspected.name)) - ensureMiningSpace() - return digFn() +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 - local item = selectProcessItem(gooTier) + error("unknown target action " .. tostring(target.action)) +end - if not item then +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 - print("Placing " .. item.name .. " on goo " .. targetName) - return placeFn() -end + local target = placedTargets[1] -local function workHorizontalSide(gooTier, sideIndex) - if not turtle.forward() then - print("Cannot move to horizontal side " .. tostring(sideIndex) .. ". Waiting...") + 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 - local changed = workTarget(turtle.inspectUp, turtle.digUp, turtle.placeUp, gooTier, "side") - - if not turtle.back() then - error("could not return below the goo") + if isGooBlock(inspected.name) then + removeQueuedTarget(1) + return true end - return changed + 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 workHorizontalSides(gooTier) - local changed = false +local function placeAvailableTarget(gooTier) + for i = 1, #TARGETS do + local target = TARGETS[i] - for sideIndex = 1, 4 do - changed = workHorizontalSide(gooTier, sideIndex) or changed - turtle.turnRight() - end + if not isTargetQueued(target) then + goToTarget(target) + ensureGooAlive() + goToTarget(target) - return changed -end + local ok, inspected = inspectTarget(target) --- Returns `changed, reachedTop`. `reachedTop` is true only once we have actually --- climbed above the goo and inspected its top face, so the caller can stop trying --- the remaining sides; it stays false when this side is blocked or unclimbable. -local function workTopFromCurrentSide(gooTier) - if not turtle.forward() then - return false, false - end + 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) - -- Only the presence of a block above matters here; ignore the inspect metadata. - local sideOccupied = turtle.inspectUp() - local changed = false - local reachedTop = false - - if not sideOccupied then - if turtle.up() then - if turtle.up() then - turnAround() - changed = workTarget(turtle.inspect, turtle.dig, turtle.place, gooTier, "top") - reachedTop = true - turnAround() - - if not turtle.down() then - error("could not descend from top placement position") + if digTarget(target) then + return true + end end - else - print("Cannot climb high enough to work on the top side.") - end - - if not turtle.down() then - error("could not return to side floor position") - end - else - print("Cannot climb to work on the top side.") - end - end - - if not turtle.back() then - error("could not return below the goo after top side check") - end - - return changed, reachedTop -end - -local function workTop(gooTier) - local changed = false - local done = false - - -- Iterate all four sides so the four turnRight calls net to zero (facing is - -- restored), but skip the climb once the single top face has been handled. - for _ = 1, 4 do - if not done then - local sideChanged, reachedTop = workTopFromCurrentSide(gooTier) - changed = sideChanged or changed - done = sideChanged or reachedTop - end - - turtle.turnRight() - end - - return changed -end - -local function turnLeft(times) - for _ = 1, times do - turtle.turnLeft() - end -end - -local function moveToSideFacingCenter() - for turns = 0, 3 do - if turtle.forward() then - turnAround() - return turns - end - - turtle.turnRight() - end - - return nil -end - -local function returnFromBottomSide(turns) - if not turtle.forward() then - return false - end - - turnAround() - turnLeft(turns) - - return true -end - -local function workBottom() - local gooTier = ensureGooAlive() - - local turns = moveToSideFacingCenter() - - if not turns then - print("Cannot move to a side position for bottom placement. Waiting...") - return false - end - - local changed = false - - -- The bottom face is worked as one atomic round-trip (place -> wait -> mine -> - -- return) rather than fire-and-forget like the other faces, because the turtle - -- must vacate and then reclaim its home cell within a single pass. - while true do - local ok, inspected = turtle.inspect() - - if not ok then - if changed then - if returnFromBottomSide(turns) then - return changed - end - - print("Center position is clear, but the turtle could not return. Waiting...") - os.sleep(WAIT_SECONDS) else local item = selectProcessItem(gooTier) - if item then - print("Placing " .. item.name .. " on goo bottom") - - if turtle.place() then - changed = true - else - print("Could not place bottom block.") - end - elseif returnFromBottomSide(turns) then - return changed - else - print("Center position is blocked. Waiting...") - os.sleep(WAIT_SECONDS) + if not item then + return false end - end - elseif isProcessBlock(inspected.name) then - print("Bottom block is still processing. Waiting outside...") - os.sleep(WAIT_SECONDS) - elseif isGooBlock(inspected.name) then - error("unexpected goo block in the turtle center position") - else - print("Mining processed bottom block: " .. tostring(inspected.name)) - ensureMiningSpace() - if turtle.dig() then - changed = true - else - print("Could not mine bottom block. Waiting...") - os.sleep(WAIT_SECONDS) + 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 -print("goo started. Place the turtle directly below the goo block.") +local function waitAtOldestTarget() + if #placedTargets == 0 then + goToGooCheck() + else + goToTarget(placedTargets[1]) + end -if not hasPickaxeEquipped() then - error("goo requires a turtle with a pickaxe equipped") + print("Waiting for goo processing...") + os.sleep(WAIT_SECONDS) end -ensureStartFuel() +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() - -- Also relied on for its side effect: emits the "requires goo tier X" warnings - -- for blocks the current tier cannot process yet. - local hasEligibleBlocks = findEligibleProcessSlot(gooTier) ~= nil + depositOutputs(gooTier) + pullFromProvider() + ensureRuntimeFuel() - local changed = false - - changed = workTop(gooTier) or changed - changed = workHorizontalSides(gooTier) or changed - changed = workBottom() or changed + gooTier = ensureGooAlive() + local changed = mineQueuedTarget(gooTier) if not changed then - if hasEligibleBlocks then - print("No free goo side found. Waiting for processing...") - os.sleep(WAIT_SECONDS) + changed = placeAvailableTarget(gooTier) + end + + if not changed then + if findEligibleProcessSlot(gooTier) then + waitAtOldestTarget() else - waitForInventory("No eligible process block found. Waiting for inventory...") + waitForProvider("No eligible process block found. Add inputs to the provider below home.") end end end