hornedcommando

Minecraft ComputerCraft SifterManager

Nov 6th, 2025
77
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 15.60 KB | Gaming | 0 0
  1.  
  2. --
  3. -- Desc: A sifter manager program for FTB Skies 2
  4. -- Indexes primary inventory and attempts to balance sifter inputs/outputs
  5. -- simplifies setup because modem can connect directly to sifter inventory
  6. -- outputs are returned to chests that already contain them by default
  7. -- IMPORTANT!: as you connect modems to sifters/chests, update the config to match
  8. -- their names, or this will not work correctly.
  9. --
  10.  
  11. -- By: hornedcommando
  12.  
  13. -- =========================
  14. -- ========== CONFIG =======
  15. -- =========================
  16. local CONFIG = {
  17.   -- Your sifter peripheral name
  18.   sifter = "createsifter:sifter_0",
  19.  
  20.   -- by default inputs where their outputs > 128 are trashed here, leave as nil to disable
  21.   trashPeripheral = "trashcans:item_trash_can_tile_1",
  22.  
  23.   -- Optional: a fallback output chest if no merge target exists
  24.   fallbackOutputChest = "sophisticatedstorage:chest_7",
  25.  
  26.   -- Inputs and their outputs (used for balance comparisons).
  27.   -- change this if your modpack has different sifter recipes
  28.   inputs = {
  29.     ["ftbstuff:crushed_netherrack"] = {
  30.       outputs = {
  31.         "mysticalagriculture:soulium_dust",
  32.         "minecraft:glowstone_dust",
  33.         "minecraft:quartz",
  34.         "ftbmaterials:quartz_dust",
  35.         "ftbmaterials:dust_sulfur",
  36.         "actuallyadditions:black_quartz",
  37.         "minecraft:blaze_powder",
  38.       }
  39.     },
  40.  
  41.     ["minecraft:gravel"] = {
  42.       outputs = {
  43.         "ftbmaterials:iron_chunk",
  44.         "ftbmaterials:copper_chunk",
  45.         "minecraft:coal",
  46.         "ftbmaterials:lapis_lazuli_chunk",
  47.         "ftbmaterials:lead_chunk",
  48.         "ftbmaterials:aluminum_chunk",
  49.         "ftbmaterials:tin_chunk",
  50.         "ftbmaterials:osmium_chunk",
  51.         "ftbmaterials:emerald_chunk",
  52.         "ftbmaterials:diamond_chunk",
  53.       }
  54.     },
  55.  
  56.     ["minecraft:sand"] = {
  57.       outputs = {
  58.         "ftbmaterials:copper_chunk",
  59.         "ftbmaterials:iron_chunk",
  60.         "ftbmaterials:aluminum_chunk",
  61.         "ftbmaterials:gold_chunk",
  62.         "ftbmaterials:silver_chunk",
  63.         "ftbmaterials:nickel_chunk",
  64.         "ftbmaterials:osmium_chunk",
  65.         "ftbmaterials:lead_chunk",
  66.         "ftbmaterials:tin_chunk",
  67.         "mysticalagriculture:prosperity_shard",
  68.       }
  69.     },
  70.  
  71.     ["minecraft:red_sand"] = {
  72.       outputs = {
  73.         "ftbmaterials:copper_chunk",
  74.         "ftbmaterials:iron_chunk",
  75.         "ftbmaterials:aluminum_chunk",
  76.         "ftbmaterials:gold_chunk",
  77.         "ftbmaterials:silver_chunk",
  78.         "ftbmaterials:nickel_chunk",
  79.         "ftbmaterials:osmium_chunk",
  80.         "ftbmaterials:lead_chunk",
  81.         "ftbmaterials:tin_chunk",
  82.         "mysticalagriculture:prosperity_shard",
  83.       }
  84.     },
  85.  
  86.     ["ftb:crushed_kivi"] = {
  87.       outputs = {
  88.         "xycraft_world:xychorium_gem_light",
  89.         "xycraft_world:xychorium_gem_dark",
  90.         "xycraft_world:xychorium_gem_blue",
  91.         "xycraft_world:xychorium_gem_green",
  92.         "xycraft_world:xychorium_gem_red",
  93.       }
  94.     },
  95.  
  96.     ["ftbstuff:crushed_basalt"] = {
  97.       outputs = {
  98.         "minecraft:netherrack",
  99.         "ftbmaterials:dust_sulfur",
  100.         "mekanism:dirty_netherite_scrap",
  101.       }
  102.     },
  103.  
  104.     ["ftbstuff:dust"] = {
  105.       outputs = {
  106.         "minecraft:bone_meal",
  107.         "ftbmaterials:niter_dust",
  108.         "minecraft:gunpowder",
  109.         "ftbmaterials:redstone_chunk",
  110.         "ftbmaterials:gold_chunk",
  111.         "ftbmaterials:bauxite_dust",
  112.         "ftbmaterials:silver_chunk",
  113.         "ftbmaterials:nickel_chunk",
  114.         "minecraft:redstone",
  115.         "ftbmaterials:aluminum_dust",
  116.         "minecraft:gold_ingot",
  117.         "ftbmaterials:silver_ingot",
  118.         "ftbmaterials:nickel_ingot",
  119.       }
  120.     },
  121.   },
  122.  
  123.   -- Balancer thresholds
  124.   scarceLT = 9,      -- <9 scarce
  125.   availLE = 64,      -- 9..64 available
  126.   abundLE = 128,     -- 64..128 abundant
  127.   reserveCount = 8,  -- keep at least this many of the INPUT unprocessed in Case #2
  128.  
  129.   -- Push/pull parameters
  130.   maxPushPerCycle = 64,        -- don't overfill in one tick
  131.   maxOutputPullPerCycle = 512, -- cap pulls per loop to avoid huge bursts
  132.  
  133.   -- Sleep timings
  134.   sleepShort = 2,   -- after any action
  135.   sleepIdle = 5,    -- when nothing to do
  136.   sleepError = 5,   -- on warnings
  137.  
  138.   -- Logging
  139.   verbose = true,
  140. }
  141.  
  142. -- =========================
  143. -- ====== UTILITIES ========
  144. -- =========================
  145. local function log(...)
  146.   if CONFIG.verbose then
  147.     local t = {}
  148.     for i = 1, select("#", ...) do t[#t+1] = tostring(select(i, ...)) end
  149.     print(table.concat(t))
  150.   end
  151. end
  152.  
  153. local function isInv(name)
  154.   if not peripheral.isPresent(name) then return false end
  155.   local m = peripheral.getMethods(name) or {}
  156.   local has = {}
  157.   for _,x in ipairs(m) do has[x] = true end
  158.   return has.list and (has.pushItems or has.pullItems)
  159. end
  160.  
  161. local function allInventories()
  162.   local out = {}
  163.   for _,n in ipairs(peripheral.getNames()) do
  164.     if isInv(n) then table.insert(out, n) end
  165.   end
  166.   table.sort(out)
  167.   return out
  168. end
  169.  
  170. local function list(name)
  171.   local ok, res = pcall(function() return peripheral.call(name, "list") end)
  172.   if ok and type(res) == "table" then return res end
  173.   return {}
  174. end
  175. local function getItemDetail(name, slot)
  176.   local ok, d = pcall(function() return peripheral.call(name, "getItemDetail", slot) end)
  177.   if ok then return d end
  178.   return nil
  179. end
  180.  
  181. local function maxCountFor(name, slot)
  182.   local d = getItemDetail(name, slot)
  183.   if d and d.maxCount then return d.maxCount end
  184.   return 64
  185. end
  186.  
  187. local function countInInv(name, id)
  188.   local total = 0
  189.   for _, it in pairs(list(name)) do
  190.     if it.name == id then total = total + (it.count or 0) end
  191.   end
  192.   return total
  193. end
  194.  
  195. local function countAcrossNetwork(id, invs)
  196.   local total = 0
  197.   for _, n in ipairs(invs) do
  198.     total = total + countInInv(n, id)
  199.   end
  200.   return total
  201. end
  202. local function stacksAcrossNetwork(id, invs, exclude)
  203.   local out = {}
  204.   for _, n in ipairs(invs) do
  205.     if n ~= exclude then
  206.       for slot, it in pairs(list(n)) do
  207.         if it.name == id and (it.count or 0) > 0 then
  208.           table.insert(out, { periph = n, slot = slot, count = it.count })
  209.         end
  210.       end
  211.     end
  212.   end
  213.   return out
  214. end
  215.  
  216. local function invFreeSlots(name)
  217.   local okS, size = pcall(function() return peripheral.call(name, "size") end)
  218.   if not okS or type(size) ~= "number" then return 0 end
  219.   local used = 0
  220.   for _ in pairs(list(name)) do used = used + 1 end
  221.   return math.max(0, size - used)
  222. end
  223.  
  224. local function canReach(src, dst)
  225.   local ok, err = pcall(function() peripheral.call(src, "pushItems", dst, 1, 0) end)
  226.   if ok then return true end
  227.   local msg = tostring(err or "")
  228.   if msg:find("does not exist") or msg:find("No such peripheral") then return false end
  229.   return true
  230. end
  231.  
  232. -- Prefer an inventory that already has the item, else fallback chest, else max free slots
  233. local function chooseOutputTarget(itemId, invs, sifter, fallback)
  234.   local best, bestCount = nil, -1
  235.   for _, n in ipairs(invs) do
  236.     if n ~= sifter then
  237.       local c = countInInv(n, itemId)
  238.       if c > 0 and canReach(sifter, n) then
  239.         if c > bestCount then best, bestCount = n, c end
  240.       end
  241.     end
  242.   end
  243.   if best then return best end
  244.   if fallback and fallback ~= sifter and canReach(sifter, fallback) then return fallback end
  245.   -- else pick most free slots
  246.   local mfName, mf = nil, -1
  247.   for _, n in ipairs(invs) do
  248.     if n ~= sifter and canReach(sifter, n) then
  249.       local free = invFreeSlots(n)
  250.       if free > mf then mfName, mf = n, free end
  251.     end
  252.   end
  253.   return mfName
  254. end
  255.  
  256. local function classify(amount)
  257.   if amount < CONFIG.scarceLT then return "scarce"
  258.   elseif amount <= CONFIG.availLE then return "available"
  259.   elseif amount <= CONFIG.abundLE then return "abundant"
  260.   else return "trash" end
  261. end
  262.  
  263. local function sumCounts(ids, invs)
  264.   local t = 0
  265.   for _, id in ipairs(ids) do
  266.     t = t + countAcrossNetwork(id, invs)
  267.   end
  268.   return t
  269. end
  270.  
  271. local function outputsStatus(outputs, invs, classifyFn)
  272.   local per = {}
  273.   local allAbundant = true
  274.   local anyBelowAbundant = false
  275.   local anyScarce = false
  276.  
  277.   for _, oid in ipairs(outputs or {}) do
  278.     local c = countAcrossNetwork(oid, invs)
  279.     local b = classifyFn(c) -- uses CONFIG thresholds
  280.     per[oid] = { count = c, band = b }
  281.  
  282.     if b == "scarce" or b == "available" then
  283.       anyBelowAbundant = true
  284.     end
  285.     if b == "scarce" then
  286.       anyScarce = true
  287.     end
  288.     if b ~= "abundant" and b ~= "trash" then
  289.       allAbundant = false
  290.     end
  291.   end
  292.  
  293.   -- If there are no outputs mapped, treat as "unknown" => do not block processing
  294.   if not outputs or #outputs == 0 then
  295.     allAbundant = false
  296.     anyBelowAbundant = true
  297.   end
  298.  
  299.   return per, allAbundant, anyBelowAbundant, anyScarce
  300. end
  301.  
  302. -- =========================
  303. -- ===== CORE ACTIONS ======
  304. -- =========================
  305. local function pushToSifter(fromStacks, sifter, maxToPush)
  306.   local remain = maxToPush
  307.   local movedTotal = 0
  308.   for _, st in ipairs(fromStacks) do
  309.     if remain <= 0 then break end
  310.     local move = math.min(remain, st.count)
  311.     local ok, movedOrErr = pcall(function()
  312.       -- No toSlot: let the sifter route to valid input
  313.       return peripheral.call(st.periph, "pushItems", sifter, st.slot, move)
  314.     end)
  315.     if ok and type(movedOrErr) == "number" and movedOrErr > 0 then
  316.       remain = remain - movedOrErr
  317.       movedTotal = movedTotal + movedOrErr
  318.       log("[SIFTER] Pushed ", movedOrErr, " from ", st.periph, " -> ", sifter)
  319.     else
  320.       log("[WARN] push to sifter failed from ", st.periph, ": ", tostring(movedOrErr))
  321.     end
  322.   end
  323.   return movedTotal
  324. end
  325.  
  326. local function pullOutputsFromSifter(sifter, invs, inputsSet)
  327.   local lst = list(sifter)
  328.   local pulled = 0
  329.   local pulledItems = {}
  330.  
  331.   for slot, it in pairs(lst) do
  332.     if it and it.name then
  333.       local id = it.name
  334.       local isInput = inputsSet[id] == true
  335.       local looksLikeMesh = string.find(id, "mesh", 1, true) ~= nil
  336.  
  337.       if (not isInput) and (not looksLikeMesh) and it.count > 0 then
  338.         local dest = chooseOutputTarget(id, invs, sifter, CONFIG.fallbackOutputChest)
  339.         if dest then
  340.           local moved = peripheral.call(sifter, "pushItems", dest, slot, it.count)
  341.           if moved and moved > 0 then
  342.             pulled = pulled + moved
  343.             pulledItems[id] = (pulledItems[id] or 0) + moved
  344.             log("[ROUTE] ", id, " x", moved, " -> ", dest)
  345.           end
  346.         else
  347.           log("[WARN] No reachable output destination for ", id, " (holding in sifter for now)")
  348.         end
  349.       end
  350.     end
  351.   end
  352.  
  353.   return pulled, pulledItems
  354. end
  355.  
  356. local function disposeExcessInput(inputId, overAmount, invs, sifter, trash)
  357.   if not trash or not peripheral.isPresent(trash) then return 0 end
  358.   if overAmount <= 0 then return 0 end
  359.   local stacks = stacksAcrossNetwork(inputId, invs, sifter)
  360.   local remaining = overAmount
  361.   local disposed = 0
  362.   for _, st in ipairs(stacks) do
  363.     if remaining <= 0 then break end
  364.     local move = math.min(remaining, st.count)
  365.     local ok, movedOrErr = pcall(function()
  366.       return peripheral.call(st.periph, "pushItems", trash, st.slot, move)
  367.     end)
  368.     if ok and type(movedOrErr) == "number" and movedOrErr > 0 then
  369.       remaining = remaining - movedOrErr
  370.       disposed = disposed + movedOrErr
  371.       log("[TRASH] ", inputId, " x", movedOrErr, " from ", st.periph, " -> ", trash)
  372.     else
  373.       log("[WARN] Dispose failed from ", st.periph, " -> ", trash, ": ", tostring(movedOrErr))
  374.     end
  375.   end
  376.   return disposed
  377. end
  378.  
  379. -- =========================
  380. -- ====== MAIN LOOP ========
  381. -- =========================
  382. local function tick()
  383.   local sifter = CONFIG.sifter
  384.   if not peripheral.isPresent(sifter) then
  385.     log("[WARN] Sifter not present: ", sifter)
  386.     sleep(CONFIG.sleepError)
  387.     return
  388.   end
  389.  
  390.   local invs = allInventories()
  391.   if #invs == 0 then
  392.     log("[INFO] No inventories on network.")
  393.     sleep(CONFIG.sleepIdle)
  394.     return
  395.   end
  396.  
  397.   -- Build a set of input IDs for quick checks
  398.   local inputsSet, recipeOrder = {}, {}
  399.   for inputId, _ in pairs(CONFIG.inputs) do
  400.     inputsSet[inputId] = true
  401.     table.insert(recipeOrder, inputId)
  402.   end
  403.   table.sort(recipeOrder)
  404.  
  405.   local didSomething = false
  406.  
  407.   for _, inputId in ipairs(recipeOrder) do
  408.     local recipe = CONFIG.inputs[inputId]
  409.     local outputs = recipe.outputs or {}
  410.     local inputCount = countAcrossNetwork(inputId, invs)
  411.     if inputCount <= 0 then
  412.       log("[SKIP] No input found: ", inputId)
  413.       goto continue_input
  414.     end
  415.  
  416.     local inputBand = classify(inputCount)
  417.     local per, allAbundant, anyBelowAbundant = outputsStatus(outputs, invs, classify)
  418.  
  419.     -- Debug snapshot
  420.     do
  421.       local dbg = {}
  422.       for oid, st in pairs(per) do
  423.         table.insert(dbg, (oid .. "=" .. st.count .. "(" .. st.band .. ")"))
  424.       end
  425.       table.sort(dbg)
  426.       log(string.format("[EVAL] %s input=%d(%s) | outputs: %s",
  427.         inputId, inputCount, inputBand, table.concat(dbg, ", ")))
  428.     end
  429.  
  430.     -- Decision rules:
  431.  
  432.     if inputBand == "scarce" then
  433.       -- Rule: scarce inputs are preserved (don't process)
  434.       log("[HOLD] Scarce input, skipping: ", inputId)
  435.  
  436.     elseif inputBand == "available" then
  437.       -- Process only if ANY output is below abundant
  438.       if anyBelowAbundant then
  439.         local stacks = stacksAcrossNetwork(inputId, invs, sifter)
  440.         -- Reserve a floor of input; process only what's above reserve
  441.         local pushBudget = math.max(0, inputCount - CONFIG.reserveCount)
  442.         pushBudget = math.min(pushBudget, CONFIG.maxPushPerCycle)
  443.         if pushBudget > 0 then
  444.           local moved = pushToSifter(stacks, sifter, pushBudget)
  445.           if moved > 0 then didSomething = true end
  446.         else
  447.           log("[OK] Reserve intact; nothing to push for ", inputId)
  448.         end
  449.       else
  450.         log("[OK] All outputs already abundant; skipping processing for ", inputId)
  451.       end
  452.     elseif inputBand == "abundant" then
  453.       -- Process if ANY output is below abundant; otherwise skip
  454.       if anyBelowAbundant then
  455.         local stacks = stacksAcrossNetwork(inputId, invs, sifter)
  456.         local moved = pushToSifter(stacks, sifter, CONFIG.maxPushPerCycle)
  457.         if moved > 0 then didSomething = true end
  458.       else
  459.         log("[OK] All outputs abundant; nothing to process for ", inputId)
  460.       end
  461.  
  462.     else -- inputBand == "trash" (> 128)
  463.       if anyBelowAbundant then
  464.         -- Still need outputs: process rather than dispose
  465.         local stacks = stacksAcrossNetwork(inputId, invs, sifter)
  466.         local moved = pushToSifter(stacks, sifter, CONFIG.maxPushPerCycle)
  467.         if moved > 0 then didSomething = true end
  468.       else
  469.         -- All outputs abundant: safe to dispose input above 128 if trash is configured
  470.         if CONFIG.trashPeripheral then
  471.           local over = inputCount - CONFIG.abundLE
  472.           if over > 0 then
  473.             local disposed = disposeExcessInput(inputId, over, invs, sifter, CONFIG.trashPeripheral)
  474.             if disposed > 0 then didSomething = true end
  475.           else
  476.             log("[OK] No excess to dispose for ", inputId)
  477.           end
  478.         else
  479.           log("[OK] Would dispose excess of ", inputId, " but no trash configured")
  480.         end
  481.       end
  482.     end
  483.  
  484.     ::continue_input::
  485.   end
  486.  
  487.   -- After any pushes, pull outputs once to clear machine buffers
  488.   local pulled, pulledItems = pullOutputsFromSifter(sifter, invs, inputsSet)
  489.   if pulled > 0 then
  490.     didSomething = true
  491.   end
  492.  
  493.   if didSomething then
  494.     sleep(CONFIG.sleepShort)
  495.   else
  496.     sleep(CONFIG.sleepIdle)
  497.   end
  498. end
  499.  
  500. local function main()
  501.   print("Sifter Manager starting...")
  502.   while true do
  503.     tick()
  504.   end
  505. end
  506.  
  507. main()
Add Comment
Please, Sign In to add comment