Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --
- -- Desc: A mill manager program for FTB StoneBlock 4
- -- Indexes primary inventory and attempts to balance mill inputs/outputs
- -- simplifies setup because modem can connect directly to mill inventory
- -- outputs are returned to chests that already contain them by default
- -- IMPORTANT!: as you connect modems to mills/chests, update the config to match
- -- their names, or this will not work correctly.
- --
- -- By: hornedcommando
- -- =========================
- -- ========== CONFIG =======
- -- =========================
- local CONFIG = {
- -- Your mill peripheral name
- mill = "create:millstone_0",
- -- by default inputs where their outputs > 128 are trashed here, leave as nil to disable
- trashPeripheral = "trashcans:item_trash_can_tile_1",
- -- Optional: a fallback output chest if no merge target exists
- fallbackOutputChest = "ironchest:crystal_chest_0",
- -- Inputs and their outputs (used for balance comparisons).
- -- change this if your modpack has different mill recipes
- inputs = {
- ["minecraft:azure_bluet"] = {
- outputs = {
- "minecraft:light_blue_dye",
- "minecraft:white_dye"
- }
- },
- ["minecraft:lilac"] = {
- outputs = {
- "minecraft:magenta_dye",
- "minecraft:purple_dye"
- }
- },
- ["minecraft:beetroot"] = {
- outputs = {
- "minecraft:red_dye",
- "minecraft:beetroot_seeds"
- }
- },
- ["minecraft:sunflower"] = {
- outputs = {
- "minecraft:yellow_dye",
- "minecraft:orange_dye"
- }
- },
- ["minecraft:sea_pickle"] = {
- outputs = {
- "minecraft:lime_dye",
- "minecraft:green_dye"
- }
- },
- ["minecraft:tall_grass"] = {
- outputs = {
- "minecraft:wheat_seeds"
- }
- },
- ["supplementaries:flax"] = {
- outputs = {
- "minecraft:string",
- "supplementaries:flax_seeds"
- }
- },
- ["minecraft:sugar_cane"] = {
- outputs = {
- "minecraft:sugar"
- }
- },
- ["ae2:certus_quartz_crystal"] = {
- outputs = {
- "ae2:certus_quartz_dust"
- }
- },
- ["minecraft:fern"] = {
- outputs = {
- "minecraft:green_dye",
- "minecraft:wheat_seeds"
- }
- },
- ["malum:blazing_quartz"] = {
- outputs = {
- "minecraft:orange_dye",
- "minecraft:yellow_dye"
- }
- },
- ["minecraft:orange_tulip"] = {
- outputs = {
- "minecraft:orange_dye",
- "minecraft:lime_dye"
- }
- },
- ["minecraft:white_tulip"] = {
- outputs = {
- "minecraft:white_dye",
- "minecraft:lime_dye"
- }
- },
- ["minecraft:granite"] = {
- outputs = {
- "minecraft:red_sand"
- }
- },
- ["minecraft:terracotta"] = {
- outputs = {
- "minecraft:red_sand"
- }
- },
- ["minecraft:lapis_lazuli"] = {
- outputs = {
- "minecraft:blue_dye"
- }
- },
- ["minecraft:ender_pearl"] = {
- outputs = {
- "ae2:ender_dust"
- }
- },
- ["minecraft:cobblestone"] = {
- outputs = {
- "minecraft:gravel"
- }
- },
- ["minecraft:bone"] = {
- outputs = {
- "minecraft:bone_meal",
- "minecraft:white_dye"
- }
- },
- ["minecraft:charcoal"] = {
- outputs = {
- "minecraft:black_dye",
- "minecraft:grey_dye"
- }
- },
- ["minecraft:wheat"] = {
- outputs = {
- "create:wheat_flour",
- "minecraft:wheat_seeds",
- }
- },
- ["minecraft:clay_block"] = {
- outputs = {
- "minecraft:clay",
- }
- },
- ["ae2:sky_stone_block"] = {
- outputs = {
- "ae2:sky_dust",
- }
- },
- ["minecraft:short_grass"] = {
- outputs = {
- "minecraft:wheat_seeds",
- }
- },
- ["minecraft:calcite"] = {
- outputs = {
- "minecraft:bone_meal",
- }
- },
- ["minecraft:sandstone"] = {
- outputs = {
- "minecraft:sand",
- }
- },
- ["minecraft:cocoa_beans"] = {
- outputs = {
- "minecraft:brown_dye",
- }
- },
- ["minecraft:poppy"] = {
- outputs = {
- "minecraft:red_dye"
- }
- },
- ["minecraft:dandelion"] = {
- outputs = {
- "minecraft:yellow_dye"
- }
- },
- ["minecraft:rose_bush"] = {
- outputs = {
- "minecraft:red_dye",
- "minecraft:green_dye"
- }
- },
- ["minecraft:peony"] = {
- outputs = {
- "minecraft:pink_dye",
- "minecraft:magenta_dye"
- }
- },
- ["minecraft:cornflower"] = {
- outputs = {
- "minecraft:blue_dye"
- }
- },
- ["minecraft:lily_of_the_valley"] = {
- outputs = {
- "minecraft:white_dye",
- "minecraft:lime_dye"
- }
- },
- ["ae2:fluix_crystal"] = {
- outputs = {
- "ae2:fluix_dust"
- }
- },
- ["minecraft:wool"] = {
- outputs = {
- "minecraft:string"
- }
- },
- ["minecraft:gravel"] = {
- outputs = {
- "minecraft:flint"
- }
- },
- },
- -- Balancer thresholds
- scarceLT = 9, -- <9 scarce
- availLE = 64, -- 9..64 available
- abundLE = 128, -- 64..128 abundant
- reserveCount = 8, -- keep at least this many of the INPUT unprocessed in Case #2
- -- Push/pull parameters
- maxPushPerCycle = 9, -- conservative push to prevent depleting scarce resources
- maxOutputPullPerCycle = 512, -- cap pulls per loop to avoid huge bursts
- -- Sleep timings
- sleepShort = 2, -- after any action
- sleepIdle = 5, -- when nothing to do
- sleepError = 5, -- on warnings
- -- Logging
- verbose = true,
- }
- -- =========================
- -- ====== UTILITIES ========
- -- =========================
- local function log(...)
- if CONFIG.verbose then
- local t = {}
- for i = 1, select("#", ...) do t[#t+1] = tostring(select(i, ...)) end
- print(table.concat(t))
- end
- end
- local function isInv(name)
- if not peripheral.isPresent(name) then return false end
- local m = peripheral.getMethods(name) or {}
- local has = {}
- for _,x in ipairs(m) do has[x] = true end
- return has.list and (has.pushItems or has.pullItems)
- end
- local function allInventories()
- local out = {}
- for _,n in ipairs(peripheral.getNames()) do
- if isInv(n) then table.insert(out, n) end
- end
- table.sort(out)
- return out
- end
- local function list(name)
- local ok, res = pcall(function() return peripheral.call(name, "list") end)
- if ok and type(res) == "table" then return res end
- return {}
- end
- local function getItemDetail(name, slot)
- local ok, d = pcall(function() return peripheral.call(name, "getItemDetail", slot) end)
- if ok then return d end
- return nil
- end
- local function maxCountFor(name, slot)
- local d = getItemDetail(name, slot)
- if d and d.maxCount then return d.maxCount end
- return 64
- end
- local function countInInv(name, id)
- local total = 0
- for _, it in pairs(list(name)) do
- if it.name == id then total = total + (it.count or 0) end
- end
- return total
- end
- local function countAcrossNetwork(id, invs)
- local total = 0
- for _, n in ipairs(invs) do
- total = total + countInInv(n, id)
- end
- return total
- end
- local function stacksAcrossNetwork(id, invs, exclude)
- local out = {}
- for _, n in ipairs(invs) do
- if n ~= exclude then
- for slot, it in pairs(list(n)) do
- if it.name == id and (it.count or 0) > 0 then
- table.insert(out, { periph = n, slot = slot, count = it.count })
- end
- end
- end
- end
- return out
- end
- local function invFreeSlots(name)
- local okS, size = pcall(function() return peripheral.call(name, "size") end)
- if not okS or type(size) ~= "number" then return 0 end
- local used = 0
- for _ in pairs(list(name)) do used = used + 1 end
- return math.max(0, size - used)
- end
- local function canReach(src, dst)
- local ok, err = pcall(function() peripheral.call(src, "pushItems", dst, 1, 0) end)
- if ok then return true end
- local msg = tostring(err or "")
- if msg:find("does not exist") or msg:find("No such peripheral") then return false end
- return true
- end
- -- Prefer an inventory that already has the item, else fallback chest, else max free slots
- local function chooseOutputTarget(itemId, invs, mill, fallback)
- local best, bestCount = nil, -1
- for _, n in ipairs(invs) do
- if n ~= mill then
- local c = countInInv(n, itemId)
- if c > 0 and canReach(mill, n) then
- if c > bestCount then best, bestCount = n, c end
- end
- end
- end
- if best then return best end
- if fallback and fallback ~= mill and canReach(mill, fallback) then return fallback end
- -- else pick most free slots
- local mfName, mf = nil, -1
- for _, n in ipairs(invs) do
- if n ~= mill and canReach(mill, n) then
- local free = invFreeSlots(n)
- if free > mf then mfName, mf = n, free end
- end
- end
- return mfName
- end
- local function classify(amount)
- if amount < CONFIG.scarceLT then return "scarce"
- elseif amount <= CONFIG.availLE then return "available"
- elseif amount <= CONFIG.abundLE then return "abundant"
- else return "trash" end
- end
- local function sumCounts(ids, invs)
- local t = 0
- for _, id in ipairs(ids) do
- t = t + countAcrossNetwork(id, invs)
- end
- return t
- end
- local function outputsStatus(outputs, invs, classifyFn)
- local per = {}
- local allAbundant = true
- local anyBelowAbundant = false
- local anyScarce = false
- for _, oid in ipairs(outputs or {}) do
- local c = countAcrossNetwork(oid, invs)
- local b = classifyFn(c) -- uses CONFIG thresholds
- per[oid] = { count = c, band = b }
- if b == "scarce" or b == "available" then
- anyBelowAbundant = true
- end
- if b == "scarce" then
- anyScarce = true
- end
- if b ~= "abundant" and b ~= "trash" then
- allAbundant = false
- end
- end
- -- If there are no outputs mapped, treat as "unknown" => do not block processing
- if not outputs or #outputs == 0 then
- allAbundant = false
- anyBelowAbundant = true
- end
- return per, allAbundant, anyBelowAbundant, anyScarce
- end
- -- =========================
- -- ===== CORE ACTIONS ======
- -- =========================
- local function pushToMill(fromStacks, mill, maxToPush)
- local remain = maxToPush
- local movedTotal = 0
- for _, st in ipairs(fromStacks) do
- if remain <= 0 then break end
- local move = math.min(remain, st.count)
- local ok, movedOrErr = pcall(function()
- -- No toSlot: let the mill route to valid input
- return peripheral.call(st.periph, "pushItems", mill, st.slot, move)
- end)
- if ok and type(movedOrErr) == "number" and movedOrErr > 0 then
- remain = remain - movedOrErr
- movedTotal = movedTotal + movedOrErr
- log("[MILL] Pushed ", movedOrErr, " from ", st.periph, " -> ", mill)
- else
- log("[WARN] push to mill failed from ", st.periph, ": ", tostring(movedOrErr))
- end
- end
- return movedTotal
- end
- local function pullOutputsFromMill(mill, invs, inputsSet)
- local lst = list(mill)
- local pulled = 0
- local pulledItems = {}
- for slot, it in pairs(lst) do
- if it and it.name then
- local id = it.name
- local isInput = inputsSet[id] == true
- if (not isInput) and it.count > 0 then
- local dest = chooseOutputTarget(id, invs, mill, CONFIG.fallbackOutputChest)
- if dest then
- local moved = peripheral.call(mill, "pushItems", dest, slot, it.count)
- if moved and moved > 0 then
- pulled = pulled + moved
- pulledItems[id] = (pulledItems[id] or 0) + moved
- log("[ROUTE] ", id, " x", moved, " -> ", dest)
- end
- else
- log("[WARN] No reachable output destination for ", id, " (holding in mill for now)")
- end
- end
- end
- end
- return pulled, pulledItems
- end
- local function disposeExcessInput(inputId, overAmount, invs, mill, trash)
- if not trash or not peripheral.isPresent(trash) then return 0 end
- if overAmount <= 0 then return 0 end
- local stacks = stacksAcrossNetwork(inputId, invs, mill)
- local remaining = overAmount
- local disposed = 0
- for _, st in ipairs(stacks) do
- if remaining <= 0 then break end
- local move = math.min(remaining, st.count)
- local ok, movedOrErr = pcall(function()
- return peripheral.call(st.periph, "pushItems", trash, st.slot, move)
- end)
- if ok and type(movedOrErr) == "number" and movedOrErr > 0 then
- remaining = remaining - movedOrErr
- disposed = disposed + movedOrErr
- log("[TRASH] ", inputId, " x", movedOrErr, " from ", st.periph, " -> ", trash)
- else
- log("[WARN] Dispose failed from ", st.periph, " -> ", trash, ": ", tostring(movedOrErr))
- end
- end
- return disposed
- end
- -- =========================
- -- ====== MAIN LOOP ========
- -- =========================
- local function tick()
- local mill = CONFIG.mill
- if not peripheral.isPresent(mill) then
- log("[WARN] Mill not present: ", mill)
- sleep(CONFIG.sleepError)
- return
- end
- local invs = allInventories()
- if #invs == 0 then
- log("[INFO] No inventories on network.")
- sleep(CONFIG.sleepIdle)
- return
- end
- -- Build a set of input IDs for quick checks
- local inputsSet, recipeOrder = {}, {}
- for inputId, _ in pairs(CONFIG.inputs) do
- inputsSet[inputId] = true
- table.insert(recipeOrder, inputId)
- end
- table.sort(recipeOrder)
- local didSomething = false
- for _, inputId in ipairs(recipeOrder) do
- local recipe = CONFIG.inputs[inputId]
- local outputs = recipe.outputs or {}
- local inputCount = countAcrossNetwork(inputId, invs)
- if inputCount <= 0 then
- log("[SKIP] No input found: ", inputId)
- goto continue_input
- end
- local inputBand = classify(inputCount)
- local per, allAbundant, anyBelowAbundant, anyScarce = outputsStatus(outputs, invs, classify)
- -- Debug snapshot
- do
- local dbg = {}
- for oid, st in pairs(per) do
- table.insert(dbg, (oid .. "=" .. st.count .. "(" .. st.band .. ")"))
- end
- table.sort(dbg)
- log(string.format("[EVAL] %s input=%d(%s) | outputs: %s",
- inputId, inputCount, inputBand, table.concat(dbg, ", ")))
- end
- -- Decision rules:
- if inputBand == "scarce" then
- -- Rule: scarce inputs are preserved (don't process)
- log("[HOLD] Scarce input, skipping: ", inputId)
- elseif inputBand == "available" then
- -- Process only if ANY output is SCARCE (not just below abundant)
- if anyScarce then
- local stacks = stacksAcrossNetwork(inputId, invs, mill)
- -- Reserve a floor of input; process only what's above reserve
- local pushBudget = math.max(0, inputCount - CONFIG.reserveCount)
- pushBudget = math.min(pushBudget, CONFIG.maxPushPerCycle)
- if pushBudget > 0 then
- local moved = pushToMill(stacks, mill, pushBudget)
- if moved > 0 then didSomething = true end
- else
- log("[OK] Reserve intact; nothing to push for ", inputId)
- end
- else
- log("[OK] No scarce outputs; skipping processing for available input ", inputId)
- end
- elseif inputBand == "abundant" then
- -- Process if ANY output is below abundant; otherwise skip
- if anyBelowAbundant then
- local stacks = stacksAcrossNetwork(inputId, invs, mill)
- local moved = pushToMill(stacks, mill, CONFIG.maxPushPerCycle)
- if moved > 0 then didSomething = true end
- else
- log("[OK] All outputs abundant; nothing to process for ", inputId)
- end
- else -- inputBand == "trash" (> 128)
- if anyBelowAbundant then
- -- Still need outputs: process rather than dispose
- local stacks = stacksAcrossNetwork(inputId, invs, mill)
- local moved = pushToMill(stacks, mill, CONFIG.maxPushPerCycle)
- if moved > 0 then didSomething = true end
- else
- -- All outputs abundant: safe to dispose input above 128 if trash is configured
- if CONFIG.trashPeripheral then
- local over = inputCount - CONFIG.abundLE
- if over > 0 then
- local disposed = disposeExcessInput(inputId, over, invs, mill, CONFIG.trashPeripheral)
- if disposed > 0 then didSomething = true end
- else
- log("[OK] No excess to dispose for ", inputId)
- end
- else
- log("[OK] Would dispose excess of ", inputId, " but no trash configured")
- end
- end
- end
- ::continue_input::
- end
- -- After any pushes, pull outputs once to clear machine buffers
- local pulled, pulledItems = pullOutputsFromMill(mill, invs, inputsSet)
- if pulled > 0 then
- didSomething = true
- end
- if didSomething then
- sleep(CONFIG.sleepShort)
- else
- sleep(CONFIG.sleepIdle)
- end
- end
- local function main()
- print("Mill Manager starting...")
- while true do
- tick()
- end
- end
- main()
Add Comment
Please, Sign In to add comment