-- Mining Turtle Script -- Sits on top of an infinite cobblestone generator. -- Continuously mines the block below and pushes items into -- networked storage via wired modem. -- Requires a wired modem attached to the turtle. local shell = _ENV.shell local function fatal(msg) printError(msg) print("\nPress any key to exit...") os.pullEvent("key") return end ------------------------------------------------- -- Default configuration (overridden by .miner_config) ------------------------------------------------- local MINE_INTERVAL = 0.5 -- seconds between dig attempts local DUMP_THRESHOLD = 14 -- dump when this many slots are occupied local DUMP_INTERVAL = 30 -- force dump every N seconds even if not full local FUEL_SLOT = 16 -- slot used for pulling/burning fuel local FUEL_THRESHOLD = 200 -- pull fuel when below this level local SYSTEM_CHANNEL = 4205 -- remote reboot channel local ORDER_CHANNEL = 4201 -- channel to talk to manager local FUEL_REQUEST_TIMEOUT = 5 -- seconds to wait for manager reply -- Fuel items the turtle will look for in storage (best first) local FUEL_ITEMS = { "minecraft:coal", "minecraft:charcoal", "minecraft:coal_block", "minecraft:blaze_rod", "minecraft:dried_kelp_block", } ------------------------------------------------- -- Load config from file if present ------------------------------------------------- local _baseDir = fs.getDir(shell.getRunningProgram()) local function _path(rel) return fs.combine(_baseDir, rel) end -- Persistent config path (survives Opus package updates) local _PERSIST_DIR = "usr/config/inventory-manager" local function _configPath(rel) if fs.isDir(_PERSIST_DIR) or fs.isDir("packages/inventory-manager") then if not fs.isDir(_PERSIST_DIR) then fs.makeDir(_PERSIST_DIR) end return fs.combine(_PERSIST_DIR, rel) end return _path(rel) end local CONFIG_FILE = _configPath(".miner_config") local function loadConfig() if not fs.exists(CONFIG_FILE) then return end local f = fs.open(CONFIG_FILE, "r") local data = f.readAll() f.close() local ok, cfg = pcall(textutils.unserialiseJSON, data) if not ok or not cfg then print("[WARN] Failed to parse " .. CONFIG_FILE) return end if cfg.mineInterval then MINE_INTERVAL = cfg.mineInterval end if cfg.dumpThreshold then DUMP_THRESHOLD = cfg.dumpThreshold end if cfg.dumpInterval then DUMP_INTERVAL = cfg.dumpInterval end if cfg.fuelSlot then FUEL_SLOT = cfg.fuelSlot end print("[CONFIG] Loaded from " .. CONFIG_FILE) end loadConfig() ------------------------------------------------- -- Setup ------------------------------------------------- print("=================================") print(" Mining Turtle (Cobble Miner)") print("=================================") print("") if not turtle then return fatal("[ERR] Not a turtle!") end -- Find wired modem and get our network name local modem = nil local modemSide = nil local selfName = nil for _, side in ipairs({"top", "bottom", "left", "right", "front", "back"}) do if peripheral.getType(side) == "modem" then local m = peripheral.wrap(side) if m.getNameLocal then local name = m.getNameLocal() if name then modem = m modemSide = side selfName = name break end end end end if not modem or not selfName then return fatal("[ERR] No wired modem found!\n Attach a wired modem to the turtle\n and connect it to the network.") end print("[OK] Modem: " .. modemSide) print("[OK] Network name: " .. selfName) print("[OK] Mine interval: " .. MINE_INTERVAL .. "s") print("[OK] Dump threshold: " .. DUMP_THRESHOLD .. " slots") print("") ------------------------------------------------- -- Find chests on the network to dump into ------------------------------------------------- local function getChests() local chests = {} for _, name in ipairs(peripheral.getNames()) do local ptype = peripheral.getType(name) if ptype == "minecraft:chest" or ptype == "minecraft:barrel" or ptype == "minecraft:shulker_box" then table.insert(chests, name) end end return chests end ------------------------------------------------- -- Dump inventory into networked storage -- Uses pull approach: wrap the remote chest and call -- chest.pullItems(selfName, slot) — avoids needing -- to wrap the turtle's own peripheral. ------------------------------------------------- local function dumpInventory() local chests = getChests() if #chests == 0 then print("[DUMP] No chests on network!") return 0 end local totalPushed = 0 for slot = 1, 16 do if slot ~= FUEL_SLOT and turtle.getItemCount(slot) > 0 then for _, chestName in ipairs(chests) do local chest = peripheral.wrap(chestName) if chest and chest.pullItems then local ok, n = pcall(chest.pullItems, selfName, slot) if ok and n and n > 0 then totalPushed = totalPushed + n if turtle.getItemCount(slot) == 0 then break -- slot empty, move to next end end end end end end -- Notify manager to refresh its cache so counts update immediately if totalPushed > 0 then pcall(function() modem.transmit(ORDER_CHANNEL, ORDER_CHANNEL, { type = "scan" }) end) end return totalPushed end ------------------------------------------------- -- Count occupied inventory slots ------------------------------------------------- local function occupiedSlots() local count = 0 for slot = 1, 16 do if slot ~= FUEL_SLOT and turtle.getItemCount(slot) > 0 then count = count + 1 end end return count end ------------------------------------------------- -- Auto-refuel: ask manager for fuel, pull directly ------------------------------------------------- --- Try to burn whatever is already in FUEL_SLOT local function burnFuelSlot() if FUEL_SLOT <= 0 then return false end local prev = turtle.getSelectedSlot() turtle.select(FUEL_SLOT) if turtle.getItemCount() > 0 then local ok = turtle.refuel() turtle.select(prev) return ok end turtle.select(prev) return false end --- Ask the inventory manager where fuel items are local function askManagerForFuel() local replyChannel = ORDER_CHANNEL + 100 + os.getComputerID() modem.open(replyChannel) modem.transmit(ORDER_CHANNEL, replyChannel, { type = "find_item", items = FUEL_ITEMS, limit = 1, }) local deadline = os.clock() + FUEL_REQUEST_TIMEOUT local result = nil while os.clock() < deadline do local timerId = os.startTimer(math.max(0.1, deadline - os.clock())) local event, p1, p2, p3, p4 = os.pullEvent() if event == "modem_message" and p2 == replyChannel then if type(p4) == "table" and p4.type == "find_item_result" then result = p4.results break end elseif event == "timer" and p1 == timerId then break end end modem.close(replyChannel) return result end --- Pull fuel from a specific chest+slot (told by manager) local function pullFuelFromSource(source) if FUEL_SLOT <= 0 then return false end local chest = peripheral.wrap(source.chest) if not chest then return false end local ok, n = pcall(chest.pushItems, selfName, source.slot, 64, FUEL_SLOT) if ok and n and n > 0 then print(string.format("[FUEL] Pulled %s x%d from %s", source.name, n, source.chest)) return true end return false end local function pullFuelFromStorage() -- Ask manager first (it knows exactly where fuel is) local sources = askManagerForFuel() if sources and #sources > 0 then for _, source in ipairs(sources) do if pullFuelFromSource(source) then return true end end end -- Fallback: scan chests directly (in case manager is offline) local fuelSet = {} for _, name in ipairs(FUEL_ITEMS) do fuelSet[name] = true end local chests = getChests() for _, chestName in ipairs(chests) do local chest = peripheral.wrap(chestName) if chest and chest.list then for slot, item in pairs(chest.list() or {}) do if fuelSet[item.name] then local ok, n = pcall(chest.pushItems, selfName, slot, 64, FUEL_SLOT) if ok and n and n > 0 then print(string.format("[FUEL] Pulled %s x%d (fallback)", item.name, n)) return true end end end end end return false end local function autoRefuel() if turtle.getFuelLevel() == "unlimited" then return end if turtle.getFuelLevel() >= FUEL_THRESHOLD then return end -- First try burning whatever is already in the fuel slot if burnFuelSlot() then print("[FUEL] Burned local fuel. Level: " .. turtle.getFuelLevel()) return end -- Pull fresh fuel from networked storage if pullFuelFromStorage() then burnFuelSlot() print("[FUEL] Refueled from storage. Level: " .. turtle.getFuelLevel()) end end ------------------------------------------------- -- Stats ------------------------------------------------- local totalMined = 0 local totalDumped = 0 local startTime = os.clock() local function printStats() local elapsed = os.clock() - startTime local mins = math.floor(elapsed / 60) local secs = math.floor(elapsed % 60) term.clear() term.setCursorPos(1, 1) print("=================================") print(" Mining Turtle - Running") print("=================================") print(string.format(" Mined: %d blocks", totalMined)) print(string.format(" Dumped: %d items", totalDumped)) print(string.format(" Uptime: %dm %ds", mins, secs)) if turtle.getFuelLevel() ~= "unlimited" then print(string.format(" Fuel: %d", turtle.getFuelLevel())) end print(string.format(" Inv used: %d/16 slots", occupiedSlots())) print("=================================") end ------------------------------------------------- -- Main mining loop ------------------------------------------------- local function mineLoop() local lastDump = os.clock() while true do -- Auto-refuel if low autoRefuel() -- Check fuel — keep trying to pull from storage if turtle.getFuelLevel() ~= "unlimited" and turtle.getFuelLevel() < 1 then print("[WARN] Out of fuel! Searching storage...") while turtle.getFuelLevel() < 1 do if pullFuelFromStorage() then burnFuelSlot() end if turtle.getFuelLevel() < 1 then printStats() sleep(10) end end end -- Mine the block below if turtle.detectDown() then local ok, err = turtle.digDown() if ok then totalMined = totalMined + 1 end end -- Dump inventory if threshold reached or timer expired local now = os.clock() if occupiedSlots() >= DUMP_THRESHOLD or (now - lastDump) >= DUMP_INTERVAL then if occupiedSlots() > 0 then local pushed = dumpInventory() totalDumped = totalDumped + pushed if pushed > 0 then print(string.format("[DUMP] Pushed %d items to storage", pushed)) end end lastDump = os.clock() end -- Refresh display periodically printStats() sleep(MINE_INTERVAL) end end ------------------------------------------------- -- Reboot listener (remote reboot support) ------------------------------------------------- local function rebootListener() modem.open(SYSTEM_CHANNEL) while true do local _, _, channel, _, message = os.pullEvent("modem_message") if channel == SYSTEM_CHANNEL and type(message) == "table" and message.type == "reboot" then local target = message.target or "all" if target == "all" or target == "miner" or target == tostring(os.getComputerID()) then print("[SYSTEM] Reboot command received.") sleep(0.5) os.reboot() end end end end ------------------------------------------------- -- Entry point ------------------------------------------------- print("Starting mining loop...") print("Press Ctrl+T to stop.") print("") sleep(1) local ok, err = pcall(function() parallel.waitForAny(mineLoop, rebootListener) end) if not ok then fatal("[ERR] Mining turtle crashed:\n" .. tostring(err)) end