Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --
- -- Desc: A press manager program for FTB StoneBlock 4
- -- Indexes primary inventory and attempts to balance press inputs/outputs
- -- simplifies setup because modem can connect directly to press inventory
- -- outputs are returned to chests that already contain them by default
- -- IMPORTANT!: as you connect modems to presses/chests, update the config to match
- -- their names, or this will not work correctly.
- --
- -- By: hornedcommando
- -- =========================
- -- ========== CONFIG =======
- -- =========================
- local CONFIG = {
- -- Your press peripheral name
- press = "create:depot_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 press recipes
- inputs = {
- ["ftbmaterials:zinc_ingot"] = {
- outputs = {
- "ftbmaterials:zinc_plate"
- }
- },
- ["minecraft:gold_ingot"] = {
- outputs = {
- "ftbmaterials:gold_plate"
- }
- },
- ["minecraft:iron_ingot"] = {
- outputs = {
- "ftbmaterials:iron_plate"
- }
- },
- ["ftbmaterials:aluminum_ingot"] = {
- outputs = {
- "ftbmaterials:aluminum_plate"
- }
- },
- ["minecraft:copper_ingot"] = {
- outputs = {
- "ftbmaterials:copper_plate"
- }
- },
- ["ftbmaterials:lead_ingot"] = {
- outputs = {
- "ftbmaterials:lead_plate"
- }
- },
- ["ftbmaterials:nickel_ingot"] = {
- outputs = {
- "ftbmaterials:nickel_plate"
- }
- },
- ["ftbmaterials:steel_ingot"] = {
- outputs = {
- "ftbmaterials:steel_plate"
- }
- },
- ["ftbmaterials:electrum_ingot"] = {
- outputs = {
- "ftbmaterials:electrum_plate"
- }
- },
- ["ftbmaterials:bronze_ingot"] = {
- outputs = {
- "ftbmaterials:bronze_plate"
- }
- },
- ["minecraft:sugar_cane"] = {
- outputs = {
- "minecraft:paper"
- }
- },
- ["create:pulp"] = {
- outputs = {
- "create:cardboard "
- }
- },
- ["ftbmaterials:brass_ingot"] = {
- outputs = {
- "ftbmaterials:brass_plate"
- }
- },
- ["create_enchantment_industry:super_experience_block"] = {
- outputs = {
- "create_enchantment_industry:super_enchanting_template"
- }
- },
- ["ftbmaterials:constantan_ingot"] = {
- outputs = {
- "ftbmaterials:constantan_plate"
- }
- },
- ["create:experience_block"] = {
- outputs = {
- "create_enchantment_industry:enchanting_template"
- }
- },
- ["ftbmaterials:invar_ingot"] = {
- outputs = {
- "ftbmaterials:invar_plate"
- }
- },
- },
- -- 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, press, fallback)
- local best, bestCount = nil, -1
- for _, n in ipairs(invs) do
- if n ~= press then
- local c = countInInv(n, itemId)
- if c > 0 and canReach(press, n) then
- if c > bestCount then best, bestCount = n, c end
- end
- end
- end
- if best then return best end
- if fallback and fallback ~= press and canReach(press, fallback) then return fallback end
- -- else pick most free slots
- local mfName, mf = nil, -1
- for _, n in ipairs(invs) do
- if n ~= press and canReach(press, 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 pushToPress(fromStacks, press, 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 press route to valid input
- return peripheral.call(st.periph, "pushItems", press, st.slot, move)
- end)
- if ok and type(movedOrErr) == "number" and movedOrErr > 0 then
- remain = remain - movedOrErr
- movedTotal = movedTotal + movedOrErr
- log("[PRESS] Pushed ", movedOrErr, " from ", st.periph, " -> ", press)
- else
- log("[WARN] push to press failed from ", st.periph, ": ", tostring(movedOrErr))
- end
- end
- return movedTotal
- end
- local function pullOutputsFromPress(press, invs, inputsSet)
- local lst = list(press)
- 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, press, CONFIG.fallbackOutputChest)
- if dest then
- local moved = peripheral.call(press, "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 press for now)")
- end
- end
- end
- end
- return pulled, pulledItems
- end
- local function disposeExcessInput(inputId, overAmount, invs, press, 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, press)
- 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 press = CONFIG.press
- if not peripheral.isPresent(press) then
- log("[WARN] Press not present: ", press)
- 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 = 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 (< 9), not if they're already available (9-64)
- local per, allAbundant, anyBelowAbundant, anyScarce = outputsStatus(outputs, invs, classify)
- if anyScarce then
- local stacks = stacksAcrossNetwork(inputId, invs, press)
- -- 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 = pushToPress(stacks, press, pushBudget)
- if moved > 0 then didSomething = true end
- else
- log("[OK] Reserve intact; nothing to push for ", inputId)
- end
- else
- log("[OK] All outputs already at available level or higher; skipping processing for ", inputId)
- end
- elseif inputBand == "abundant" then
- -- Process if ANY output is below abundant; otherwise skip
- if anyBelowAbundant then
- local stacks = stacksAcrossNetwork(inputId, invs, press)
- local moved = pushToPress(stacks, press, 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, press)
- local moved = pushToPress(stacks, press, 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, press, 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 = pullOutputsFromPress(press, 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("Press Manager starting...")
- while true do
- tick()
- end
- end
- main()
Add Comment
Please, Sign In to add comment