breedAuto_cows.lua
619 lines · 16.8 KB
Cow Auto-Breeder (Perimeter Walker)
Copy & run
wget https://perlytiara.github.io/turtles.tips/raw/programs/perlytiara/husbandry/breedAuto_cows.lua
| 1 | -- Cow Auto-Breeder (Perimeter Walker) |
| 2 | -- Requirements: CC:Tweaked turtle with Advanced Peripherals Husbandry Automata upgrade |
| 3 | -- Place a chest directly behind the turtle at start for refuel/restock/deposit |
| 4 | |
| 5 | -- ===================== UI HELPERS ===================== |
| 6 | local function supportsColor() |
| 7 | return term.isColor and term.isColor() |
| 8 | end |
| 9 | |
| 10 | local function clearScreen() |
| 11 | term.clear() |
| 12 | term.setCursorPos(1, 1) |
| 13 | end |
| 14 | |
| 15 | local function banner() |
| 16 | if supportsColor() then term.setTextColor(colors.lime) end |
| 17 | print("==============================") |
| 18 | print(" Cow Auto-Breeder - Perimeter ") |
| 19 | print("==============================") |
| 20 | if supportsColor() then term.setTextColor(colors.white) end |
| 21 | end |
| 22 | |
| 23 | local function prompt(text, default) |
| 24 | io.write(text) |
| 25 | if default ~= nil then io.write(" [" .. tostring(default) .. "]") end |
| 26 | io.write(": ") |
| 27 | local ans = read() |
| 28 | ans = ans and ans:gsub("^%s+", ""):gsub("%s+$", "") or "" |
| 29 | if ans == "" and default ~= nil then return default end |
| 30 | return ans |
| 31 | end |
| 32 | |
| 33 | local function waitEnter(msg) |
| 34 | io.write(msg or "Press Enter to continue...") |
| 35 | read() |
| 36 | end |
| 37 | |
| 38 | -- ===================== PERIPHERAL ===================== |
| 39 | local function findHusbandryPeripheral() |
| 40 | local candidates = {"husbandry_automata", "husbandryAutomata", "weak_automata", "automata"} |
| 41 | for _, name in ipairs(candidates) do |
| 42 | local p = peripheral.find(name) |
| 43 | if p then return p, name end |
| 44 | end |
| 45 | return nil, nil |
| 46 | end |
| 47 | |
| 48 | -- ===================== CONFIG ===================== |
| 49 | local Config = { |
| 50 | fenceSampleSlot = 1, |
| 51 | cooldownSeconds = 300, |
| 52 | maxCows = 24, |
| 53 | cullingEnabled = true, |
| 54 | targetWheat = 64, |
| 55 | minFuel = 1000, |
| 56 | } |
| 57 | |
| 58 | local function runSetup() |
| 59 | clearScreen() |
| 60 | banner() |
| 61 | print("Setup:") |
| 62 | print("- Ensure a chest is placed DIRECTLY BEHIND the turtle.") |
| 63 | print("- Put wheat and coal/charcoal into that chest for restocking/refuel.") |
| 64 | print("- Place the turtle INSIDE the pen, facing along the fence so the fence is on its RIGHT.") |
| 65 | print("") |
| 66 | print("Provide a sample of the pen's fence block in an inventory slot.") |
| 67 | Config.fenceSampleSlot = tonumber(prompt("Which slot holds the fence sample?", tostring(Config.fenceSampleSlot))) or Config.fenceSampleSlot |
| 68 | Config.maxCows = tonumber(prompt("Max adult cows to keep (cull if above)", tostring(Config.maxCows))) or Config.maxCows |
| 69 | Config.cullingEnabled = string.lower(prompt("Enable culling if above max? (yes/no)", Config.cullingEnabled and "yes" or "no")) == "yes" |
| 70 | local mins = tonumber(prompt("Minutes between breeding cycles", tostring(math.floor(Config.cooldownSeconds / 60)))) or (Config.cooldownSeconds / 60) |
| 71 | Config.cooldownSeconds = math.floor(mins * 60) |
| 72 | Config.targetWheat = tonumber(prompt("Target wheat count to keep in inventory", tostring(Config.targetWheat))) or Config.targetWheat |
| 73 | Config.minFuel = tonumber(prompt("Minimum fuel level to maintain", tostring(Config.minFuel))) or Config.minFuel |
| 74 | print("") |
| 75 | print("Setup complete. Starting...") |
| 76 | os.sleep(1) |
| 77 | end |
| 78 | |
| 79 | -- ===================== INVENTORY ===================== |
| 80 | local function getItemDetail(slot) |
| 81 | local d = turtle.getItemDetail(slot) |
| 82 | if d then |
| 83 | return { |
| 84 | slot = slot, |
| 85 | name = d.name, |
| 86 | displayName = d.displayName or d.name, |
| 87 | count = d.count or turtle.getItemCount(slot), |
| 88 | } |
| 89 | end |
| 90 | return nil |
| 91 | end |
| 92 | |
| 93 | local function eachItem() |
| 94 | local i = 0 |
| 95 | return function() |
| 96 | i = i + 1 |
| 97 | while i <= 16 do |
| 98 | local d = getItemDetail(i) |
| 99 | if d then return d end |
| 100 | i = i + 1 |
| 101 | end |
| 102 | return nil |
| 103 | end |
| 104 | end |
| 105 | |
| 106 | local function itemMatches(detail, keywords) |
| 107 | local lname = string.lower((detail.name or "") .. " " .. (detail.displayName or "")) |
| 108 | for _, key in ipairs(keywords) do |
| 109 | if string.find(lname, string.lower(key), 1, true) then return true end |
| 110 | end |
| 111 | return false |
| 112 | end |
| 113 | |
| 114 | local function selectItem(keywords) |
| 115 | for item in eachItem() do |
| 116 | if itemMatches(item, keywords) then |
| 117 | turtle.select(item.slot) |
| 118 | return true, item |
| 119 | end |
| 120 | end |
| 121 | return false |
| 122 | end |
| 123 | |
| 124 | local function countItems(keywords) |
| 125 | local total = 0 |
| 126 | for item in eachItem() do |
| 127 | if itemMatches(item, keywords) then total = total + item.count end |
| 128 | end |
| 129 | return total |
| 130 | end |
| 131 | |
| 132 | local function dropAllExcept(keepKeywords) |
| 133 | -- Face chest assumed in front |
| 134 | for slot = 1, 16 do |
| 135 | local d = getItemDetail(slot) |
| 136 | if d then |
| 137 | local keep = itemMatches(d, keepKeywords) |
| 138 | if not keep then |
| 139 | turtle.select(slot) |
| 140 | turtle.drop() |
| 141 | end |
| 142 | end |
| 143 | end |
| 144 | end |
| 145 | |
| 146 | local function suckUntil(predicate, maxActions) |
| 147 | maxActions = maxActions or 64 |
| 148 | local actions = 0 |
| 149 | while actions < maxActions do |
| 150 | if predicate() then return true end |
| 151 | if not turtle.suck() then return predicate() end |
| 152 | actions = actions + 1 |
| 153 | end |
| 154 | return predicate() |
| 155 | end |
| 156 | |
| 157 | local COAL_KEYS = {"coal", "charcoal"} |
| 158 | local WHEAT_KEYS = {"wheat"} |
| 159 | local SWORD_KEYS = {"sword"} |
| 160 | |
| 161 | local function faceChest(fn) |
| 162 | -- Turn around, do fn(), turn back |
| 163 | turtle.turnLeft(); turtle.turnLeft() |
| 164 | local ok, err = pcall(fn) |
| 165 | turtle.turnLeft(); turtle.turnLeft() |
| 166 | if not ok then error(err) end |
| 167 | end |
| 168 | |
| 169 | local function ensureFuel(minFuel) |
| 170 | local lvl = turtle.getFuelLevel() |
| 171 | if lvl == "unlimited" or lvl == math.huge then return true end |
| 172 | if lvl >= minFuel then return true end |
| 173 | faceChest(function() |
| 174 | -- Try to refuel using existing coal first |
| 175 | if not selectItem(COAL_KEYS) then |
| 176 | -- Pull from chest |
| 177 | suckUntil(function() return selectItem(COAL_KEYS) end, 128) |
| 178 | end |
| 179 | if selectItem(COAL_KEYS) then |
| 180 | -- Refuel until target |
| 181 | for _ = 1, 16 do |
| 182 | local before = turtle.getFuelLevel() |
| 183 | turtle.refuel(1) |
| 184 | if turtle.getFuelLevel() >= minFuel then break end |
| 185 | if turtle.getFuelLevel() == before then break end |
| 186 | end |
| 187 | end |
| 188 | -- Drop back non-keep |
| 189 | dropAllExcept({"wheat", "coal", "charcoal"}) |
| 190 | end) |
| 191 | return turtle.getFuelLevel() == "unlimited" or turtle.getFuelLevel() >= minFuel |
| 192 | end |
| 193 | |
| 194 | local function ensureWheat(targetCount) |
| 195 | if countItems(WHEAT_KEYS) >= targetCount then return true end |
| 196 | faceChest(function() |
| 197 | -- pull until we reach target or chest empty |
| 198 | suckUntil(function() return countItems(WHEAT_KEYS) >= targetCount end, 256) |
| 199 | -- drop back everything except wheat and coal |
| 200 | dropAllExcept({"wheat", "coal", "charcoal"}) |
| 201 | end) |
| 202 | return countItems(WHEAT_KEYS) >= math.min(1, targetCount) |
| 203 | end |
| 204 | |
| 205 | local function depositNonKeep() |
| 206 | faceChest(function() |
| 207 | dropAllExcept({"wheat", "coal", "charcoal"}) |
| 208 | end) |
| 209 | end |
| 210 | |
| 211 | -- ===================== MOVEMENT / PERIMETER ===================== |
| 212 | local heading = 0 -- 0=N,1=E,2=S,3=W |
| 213 | local startHeading = 0 |
| 214 | local posX, posZ = 0, 0 |
| 215 | local moveHistory = {} |
| 216 | |
| 217 | local function setHeading(h) |
| 218 | heading = (h % 4 + 4) % 4 |
| 219 | end |
| 220 | |
| 221 | local function turnRight() |
| 222 | turtle.turnRight() |
| 223 | setHeading(heading + 1) |
| 224 | moveHistory[#moveHistory + 1] = "R" |
| 225 | end |
| 226 | |
| 227 | local function turnLeft() |
| 228 | turtle.turnLeft() |
| 229 | setHeading(heading - 1) |
| 230 | moveHistory[#moveHistory + 1] = "L" |
| 231 | end |
| 232 | |
| 233 | local function turnRightNoRecord() |
| 234 | turtle.turnRight() |
| 235 | setHeading(heading + 1) |
| 236 | end |
| 237 | |
| 238 | local function turnLeftNoRecord() |
| 239 | turtle.turnLeft() |
| 240 | setHeading(heading - 1) |
| 241 | end |
| 242 | |
| 243 | local function forward() |
| 244 | if turtle.forward() then |
| 245 | if heading == 0 then posZ = posZ - 1 |
| 246 | elseif heading == 1 then posX = posX + 1 |
| 247 | elseif heading == 2 then posZ = posZ + 1 |
| 248 | else posX = posX - 1 end |
| 249 | moveHistory[#moveHistory + 1] = "F" |
| 250 | return true |
| 251 | end |
| 252 | return false |
| 253 | end |
| 254 | |
| 255 | local function moveBack() |
| 256 | if turtle.back() then |
| 257 | if heading == 0 then posZ = posZ + 1 |
| 258 | elseif heading == 1 then posX = posX - 1 |
| 259 | elseif heading == 2 then posZ = posZ - 1 |
| 260 | else posX = posX + 1 end |
| 261 | moveHistory[#moveHistory + 1] = "B" |
| 262 | return true |
| 263 | end |
| 264 | return false |
| 265 | end |
| 266 | |
| 267 | local function resetPose() |
| 268 | posX, posZ = 0, 0 |
| 269 | startHeading = heading |
| 270 | moveHistory = {} |
| 271 | end |
| 272 | |
| 273 | local fenceName = nil |
| 274 | local function readFenceNameFromSample() |
| 275 | local d = getItemDetail(Config.fenceSampleSlot) |
| 276 | if not d then return nil end |
| 277 | return d.name |
| 278 | end |
| 279 | |
| 280 | local function inspectFrontName() |
| 281 | local ok, data = turtle.inspect() |
| 282 | if ok and data and data.name then return data.name end |
| 283 | return nil |
| 284 | end |
| 285 | |
| 286 | local function hasFenceRight() |
| 287 | turnRightNoRecord() |
| 288 | local name = inspectFrontName() |
| 289 | turnLeftNoRecord() |
| 290 | return name == fenceName |
| 291 | end |
| 292 | |
| 293 | local function frontBlockedByBlock() |
| 294 | return turtle.detect() |
| 295 | end |
| 296 | |
| 297 | -- Forward declarations for functions referenced before definition |
| 298 | local tryFeedFrontCow |
| 299 | local tryCullFrontCow |
| 300 | |
| 301 | local function isFenceAhead() |
| 302 | if not frontBlockedByBlock() then return false end |
| 303 | return inspectFrontName() == fenceName |
| 304 | end |
| 305 | |
| 306 | local function turnToHeading(target) |
| 307 | target = (target % 4 + 4) % 4 |
| 308 | local diff = (target - heading) % 4 |
| 309 | if diff == 1 then |
| 310 | turnRight() |
| 311 | elseif diff == 2 then |
| 312 | turnRight(); turnRight() |
| 313 | elseif diff == 3 then |
| 314 | turnLeft() |
| 315 | end |
| 316 | end |
| 317 | |
| 318 | local function advanceToWallCurrentHeading(mode) |
| 319 | local guard = 0 |
| 320 | while not isFenceAhead() and guard < 8192 do |
| 321 | if mode == "breed" then tryFeedFrontCow() else tryCullFrontCow() end |
| 322 | local ok, reason = forceForward(mode, 30) |
| 323 | if not ok and reason == "fence" then break end |
| 324 | guard = guard + 1 |
| 325 | end |
| 326 | end |
| 327 | |
| 328 | -- ===================== ANIMAL INTERACTION ===================== |
| 329 | local automata, automataName = nil, nil |
| 330 | |
| 331 | local function isCowEntityInfo(info) |
| 332 | if not info then return false end |
| 333 | local vals = { |
| 334 | string.lower(tostring(info.id or "")), |
| 335 | string.lower(tostring(info.name or "")), |
| 336 | string.lower(tostring(info.species or "")), |
| 337 | } |
| 338 | for _, v in ipairs(vals) do |
| 339 | if v ~= "" and (v == "minecraft:cow" or v == "cow" or v:find("cow", 1, true)) then return true end |
| 340 | end |
| 341 | return false |
| 342 | end |
| 343 | |
| 344 | local function isBabyFromInfo(info) |
| 345 | if not info then return false end |
| 346 | if info.isBaby == true then return true end |
| 347 | if type(info.age) == "number" and info.age < 0 then return true end |
| 348 | if type(info.growingAge) == "number" and info.growingAge < 0 then return true end |
| 349 | return false |
| 350 | end |
| 351 | |
| 352 | tryFeedFrontCow = function() |
| 353 | local info = select(1, automata.inspectAnimal()) |
| 354 | if not info or not isCowEntityInfo(info) then return false, "no_cow" end |
| 355 | if isBabyFromInfo(info) then return false, "baby" end |
| 356 | if not selectItem(WHEAT_KEYS) then return false, "no_wheat" end |
| 357 | local ok, msg = automata.useOnAnimal() |
| 358 | return ok or false, msg |
| 359 | end |
| 360 | |
| 361 | tryCullFrontCow = function() |
| 362 | local info = select(1, automata.inspectAnimal()) |
| 363 | if not info or not isCowEntityInfo(info) then return false, "no_cow" end |
| 364 | if isBabyFromInfo(info) then return false, "baby" end |
| 365 | -- Attempt a few attacks |
| 366 | selectItem(SWORD_KEYS) |
| 367 | local attacked = false |
| 368 | for _ = 1, 5 do |
| 369 | if turtle.attack() then attacked = true end |
| 370 | os.sleep(0.1) |
| 371 | -- if path clears, break |
| 372 | if forward() then |
| 373 | -- stepped into spot; step back to maintain path |
| 374 | moveBack() |
| 375 | break |
| 376 | end |
| 377 | end |
| 378 | return attacked, attacked and "attacked" or "blocked" |
| 379 | end |
| 380 | |
| 381 | -- Robust movement that forces progress through entities (not blocks) |
| 382 | local function forceForward(mode, maxTries) |
| 383 | maxTries = maxTries or 30 |
| 384 | if frontBlockedByBlock() and inspectFrontName() == fenceName then |
| 385 | return false, "fence" |
| 386 | end |
| 387 | for i = 1, maxTries do |
| 388 | if forward() then return true end |
| 389 | -- entity in front; interact per mode |
| 390 | if mode == "breed" then |
| 391 | tryFeedFrontCow() |
| 392 | else |
| 393 | tryCullFrontCow() |
| 394 | end |
| 395 | os.sleep(0.1) |
| 396 | end |
| 397 | return false, "entity_blocked" |
| 398 | end |
| 399 | |
| 400 | local function forceBack(maxTries) |
| 401 | maxTries = maxTries or 20 |
| 402 | for i = 1, maxTries do |
| 403 | if moveBack() then return true end |
| 404 | -- fallback: 180 and forward |
| 405 | turnLeftNoRecord(); turnLeftNoRecord() |
| 406 | local ok = forward() |
| 407 | turnLeftNoRecord(); turnLeftNoRecord() |
| 408 | if ok then return true end |
| 409 | os.sleep(0.05) |
| 410 | end |
| 411 | return false |
| 412 | end |
| 413 | |
| 414 | local function countNearbyCows() |
| 415 | local list = select(1, automata.searchAnimals()) or {} |
| 416 | local countAdult = 0 |
| 417 | for _, a in ipairs(list) do |
| 418 | if isCowEntityInfo(a) then |
| 419 | if not isBabyFromInfo(a) then countAdult = countAdult + 1 end |
| 420 | end |
| 421 | end |
| 422 | return countAdult |
| 423 | end |
| 424 | |
| 425 | -- ===================== PERIMETER WALK ===================== |
| 426 | local function orientToFenceRight(maxTries) |
| 427 | maxTries = maxTries or 4 |
| 428 | for _ = 1, maxTries do |
| 429 | if hasFenceRight() then return true end |
| 430 | turnRight() |
| 431 | end |
| 432 | return false |
| 433 | end |
| 434 | |
| 435 | local function handleEntityBlock(mode) |
| 436 | -- try to feed/attack then attempt to move forward a few times |
| 437 | if mode == "breed" then tryFeedFrontCow() else tryCullFrontCow() end |
| 438 | for _ = 1, 6 do |
| 439 | if forward() then return true end |
| 440 | os.sleep(0.1) |
| 441 | end |
| 442 | return false |
| 443 | end |
| 444 | |
| 445 | -- Stable right-hand wall follower around the specified fence |
| 446 | local function perimeterOnce(mode) |
| 447 | mode = mode or "breed" -- or "cull" |
| 448 | -- Ensure we start hugging fence on right |
| 449 | if not orientToFenceRight(8) then |
| 450 | print("Could not find fence on the right. Adjust turtle position/orientation and press Enter.") |
| 451 | waitEnter() |
| 452 | if not orientToFenceRight(8) then return false, "no_fence" end |
| 453 | end |
| 454 | resetPose() |
| 455 | local steps = 0 |
| 456 | local safety = 16000 |
| 457 | while safety > 0 do |
| 458 | safety = safety - 1 |
| 459 | -- Interact opportunistically if a cow is in front |
| 460 | if mode == "breed" then tryFeedFrontCow() else tryCullFrontCow() end |
| 461 | |
| 462 | local rightIsFence = hasFenceRight() |
| 463 | local frontIsBlock = frontBlockedByBlock() |
| 464 | if not rightIsFence then |
| 465 | -- no fence on right: turn right and try to go forward |
| 466 | turnRight() |
| 467 | if frontBlockedByBlock() then |
| 468 | -- corner or obstacle, turn left back to continue search |
| 469 | turnLeft() |
| 470 | else |
| 471 | if not forward() then |
| 472 | -- entity blocking |
| 473 | handleEntityBlock(mode) |
| 474 | end |
| 475 | end |
| 476 | else |
| 477 | -- fence is on the right; follow along it |
| 478 | if frontIsBlock then |
| 479 | -- turn left around corner |
| 480 | turnLeft() |
| 481 | else |
| 482 | if not forward() then |
| 483 | -- entity blocking |
| 484 | handleEntityBlock(mode) |
| 485 | end |
| 486 | end |
| 487 | end |
| 488 | steps = steps + 1 |
| 489 | -- Loop complete if back to start pose after some movement |
| 490 | if posX == 0 and posZ == 0 and heading == startHeading and steps > 8 then |
| 491 | return true |
| 492 | end |
| 493 | -- light delay to be friendly |
| 494 | os.sleep(0.05) |
| 495 | end |
| 496 | return false, "safety_exceeded" |
| 497 | end |
| 498 | |
| 499 | -- Serpentine traversal within the pen. Returns to home using move history. |
| 500 | local function serpentineOnce(mode) |
| 501 | mode = mode or "breed" |
| 502 | resetPose() |
| 503 | |
| 504 | -- Step 1: go straight until first wall in current facing |
| 505 | local dirFirst = heading |
| 506 | advanceToWallCurrentHeading(mode) |
| 507 | |
| 508 | -- Step 2: turn right and go to the next wall (corner) |
| 509 | turnRight() |
| 510 | local dirSecond = heading |
| 511 | advanceToWallCurrentHeading(mode) |
| 512 | |
| 513 | -- Define run and lateral directions relative to first/second legs |
| 514 | local runDir = (dirSecond + 2) % 4 -- opposite of second leg (go away from that wall) |
| 515 | local lateralDir = (dirFirst + 2) % 4 -- move one block opposite of first leg between runs |
| 516 | |
| 517 | -- Serpentine columns: run to wall, shift 1 laterally, reverse run, repeat |
| 518 | local flips = 0 |
| 519 | local safety = 20000 |
| 520 | while safety > 0 do |
| 521 | safety = safety - 1 |
| 522 | -- Run to wall along runDir |
| 523 | turnToHeading(runDir) |
| 524 | advanceToWallCurrentHeading(mode) |
| 525 | |
| 526 | -- Shift one block laterally; stop if fence blocks shift |
| 527 | turnToHeading(lateralDir) |
| 528 | if isFenceAhead() then break end |
| 529 | forceForward(mode, 30) |
| 530 | |
| 531 | -- Reverse run direction for next column |
| 532 | runDir = (runDir + 2) % 4 |
| 533 | flips = flips + 1 |
| 534 | if flips > 8192 then break end |
| 535 | end |
| 536 | |
| 537 | -- Go home by reversing history |
| 538 | for i = #moveHistory, 1, -1 do |
| 539 | local op = moveHistory[i] |
| 540 | if op == "F" then |
| 541 | forceBack(15) |
| 542 | elseif op == "B" then |
| 543 | forceForward("breed", 15) |
| 544 | elseif op == "L" then |
| 545 | turnRightNoRecord() |
| 546 | elseif op == "R" then |
| 547 | turnLeftNoRecord() |
| 548 | end |
| 549 | end |
| 550 | moveHistory = {} |
| 551 | return true |
| 552 | end |
| 553 | |
| 554 | -- ===================== MAIN LOOP ===================== |
| 555 | local function main() |
| 556 | if not turtle then |
| 557 | error("Must run on a turtle") |
| 558 | end |
| 559 | automata, automataName = findHusbandryPeripheral() |
| 560 | if not automata then |
| 561 | error("Husbandry Automata peripheral not found") |
| 562 | end |
| 563 | runSetup() |
| 564 | fenceName = readFenceNameFromSample() |
| 565 | if not fenceName then error("Fence sample slot is empty or invalid") end |
| 566 | |
| 567 | -- Initialize pose tracking on demand (perimeterOnce resets pose) |
| 568 | |
| 569 | while true do |
| 570 | clearScreen(); banner() |
| 571 | print("Servicing chest and preparing...") |
| 572 | depositNonKeep() |
| 573 | ensureWheat(Config.targetWheat) |
| 574 | ensureFuel(Config.minFuel) |
| 575 | |
| 576 | print("Breeding serpentine pass...") |
| 577 | local ok, err = serpentineOnce("breed") |
| 578 | if not ok then print("Traversal error: " .. tostring(err)) end |
| 579 | |
| 580 | -- Wait cooldown BEFORE culling |
| 581 | local waitSec = Config.cooldownSeconds |
| 582 | print("Waiting " .. tostring(waitSec) .. "s before next cycle...") |
| 583 | for s = waitSec, 1, -1 do |
| 584 | term.setCursorPos(1, 10) |
| 585 | print(string.format("Next in %ds ", s)) |
| 586 | os.sleep(1) |
| 587 | end |
| 588 | |
| 589 | -- Culling pass after waiting |
| 590 | local count = countNearbyCows() |
| 591 | print("Detected adult cows: " .. tostring(count)) |
| 592 | if Config.cullingEnabled and count > Config.maxCows then |
| 593 | print("Culling serpentine pass to target <= " .. tostring(Config.maxCows)) |
| 594 | local safety = 12 |
| 595 | while count > Config.maxCows and safety > 0 do |
| 596 | local okCull = select(1, serpentineOnce("cull")) |
| 597 | count = countNearbyCows() |
| 598 | print("Cows remaining: " .. tostring(count)) |
| 599 | safety = safety - 1 |
| 600 | if not okCull then break end |
| 601 | end |
| 602 | end |
| 603 | |
| 604 | print("Depositing drops and refueling/restocking...") |
| 605 | depositNonKeep() |
| 606 | ensureFuel(Config.minFuel) |
| 607 | ensureWheat(Config.targetWheat) |
| 608 | end |
| 609 | end |
| 610 | |
| 611 | local ok, err = pcall(main) |
| 612 | if not ok then |
| 613 | print("Error: " .. tostring(err)) |
| 614 | warn = warn or print |
| 615 | warn("Program terminated.") |
| 616 | end |
| 617 | |
| 618 | |
| 619 |