415 lines
13 KiB
Lua
415 lines
13 KiB
Lua
-- 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
|