hornedcommando

Minecraft ComputerCraft Modem MillManager

Nov 21st, 2025
61
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 16.90 KB | Gaming | 0 0
  1. --
  2. -- Desc: A mill manager program for FTB StoneBlock 4
  3. -- Indexes primary inventory and attempts to balance mill inputs/outputs
  4. -- simplifies setup because modem can connect directly to mill inventory
  5. -- outputs are returned to chests that already contain them by default
  6. -- IMPORTANT!: as you connect modems to mills/chests, update the config to match
  7. -- their names, or this will not work correctly.
  8. --
  9.  
  10. -- By: hornedcommando
  11.  
  12. -- =========================
  13. -- ========== CONFIG =======
  14. -- =========================
  15. local CONFIG = {
  16.   -- Your mill peripheral name
  17.   mill = "create:millstone_0",
  18.  
  19.   -- by default inputs where their outputs > 128 are trashed here, leave as nil to disable
  20.   trashPeripheral = "trashcans:item_trash_can_tile_1",
  21.  
  22.   -- Optional: a fallback output chest if no merge target exists
  23.   fallbackOutputChest = "ironchest:crystal_chest_0",
  24.  
  25.   -- Inputs and their outputs (used for balance comparisons).
  26.   -- change this if your modpack has different mill recipes
  27.   inputs = {
  28.     ["minecraft:azure_bluet"] = {
  29.       outputs = {
  30.         "minecraft:light_blue_dye",
  31.         "minecraft:white_dye"
  32.       }
  33.     },
  34.  
  35.     ["minecraft:lilac"] = {
  36.       outputs = {
  37.         "minecraft:magenta_dye",
  38.         "minecraft:purple_dye"
  39.       }
  40.     },
  41.  
  42.     ["minecraft:beetroot"] = {
  43.       outputs = {
  44.         "minecraft:red_dye",
  45.         "minecraft:beetroot_seeds"
  46.       }
  47.     },
  48.  
  49.     ["minecraft:sunflower"] = {
  50.       outputs = {
  51.         "minecraft:yellow_dye",
  52.         "minecraft:orange_dye"
  53.       }
  54.     },
  55.  
  56.     ["minecraft:sea_pickle"] = {
  57.       outputs = {
  58.         "minecraft:lime_dye",
  59.         "minecraft:green_dye"
  60.       }
  61.     },
  62.  
  63.     ["minecraft:tall_grass"] = {
  64.       outputs = {
  65.         "minecraft:wheat_seeds"
  66.       }
  67.     },
  68.  
  69.     ["supplementaries:flax"] = {
  70.       outputs = {
  71.         "minecraft:string",
  72.         "supplementaries:flax_seeds"
  73.       }
  74.     },
  75.  
  76.     ["minecraft:sugar_cane"] = {
  77.       outputs = {
  78.         "minecraft:sugar"
  79.       }
  80.     },
  81.  
  82.     ["ae2:certus_quartz_crystal"] = {
  83.       outputs = {
  84.         "ae2:certus_quartz_dust"
  85.       }
  86.     },
  87.  
  88.     ["minecraft:fern"] = {
  89.       outputs = {
  90.         "minecraft:green_dye",
  91.         "minecraft:wheat_seeds"
  92.       }
  93.     },
  94.  
  95.     ["malum:blazing_quartz"] = {
  96.       outputs = {
  97.         "minecraft:orange_dye",
  98.         "minecraft:yellow_dye"
  99.       }
  100.     },
  101.  
  102.     ["minecraft:orange_tulip"] = {
  103.       outputs = {
  104.         "minecraft:orange_dye",
  105.         "minecraft:lime_dye"
  106.       }
  107.     },
  108.  
  109.     ["minecraft:white_tulip"] = {
  110.       outputs = {
  111.         "minecraft:white_dye",
  112.         "minecraft:lime_dye"
  113.       }
  114.     },
  115.  
  116.     ["minecraft:granite"] = {
  117.       outputs = {
  118.         "minecraft:red_sand"
  119.       }
  120.     },
  121.  
  122.     ["minecraft:terracotta"] = {
  123.       outputs = {
  124.         "minecraft:red_sand"
  125.       }
  126.     },
  127.  
  128.     ["minecraft:lapis_lazuli"] = {
  129.       outputs = {
  130.         "minecraft:blue_dye"
  131.       }
  132.     },
  133.  
  134.     ["minecraft:ender_pearl"] = {
  135.       outputs = {
  136.         "ae2:ender_dust"
  137.       }
  138.     },
  139.  
  140.     ["minecraft:cobblestone"] = {
  141.       outputs = {
  142.         "minecraft:gravel"
  143.       }
  144.     },
  145.  
  146.     ["minecraft:bone"] = {
  147.       outputs = {
  148.         "minecraft:bone_meal",
  149.         "minecraft:white_dye"
  150.       }
  151.     },
  152.  
  153.     ["minecraft:charcoal"] = {
  154.       outputs = {
  155.         "minecraft:black_dye",
  156.         "minecraft:grey_dye"
  157.       }
  158.     },
  159.  
  160.     ["minecraft:wheat"] = {
  161.       outputs = {
  162.         "create:wheat_flour",
  163.         "minecraft:wheat_seeds",
  164.       }
  165.     },
  166.  
  167.     ["minecraft:clay_block"] = {
  168.       outputs = {
  169.         "minecraft:clay",
  170.       }
  171.     },
  172.  
  173.     ["ae2:sky_stone_block"] = {
  174.       outputs = {
  175.         "ae2:sky_dust",
  176.       }
  177.     },
  178.  
  179.     ["minecraft:short_grass"] = {
  180.       outputs = {
  181.         "minecraft:wheat_seeds",
  182.       }
  183.     },
  184.  
  185.     ["minecraft:calcite"] = {
  186.       outputs = {
  187.         "minecraft:bone_meal",
  188.       }
  189.     },
  190.  
  191.     ["minecraft:sandstone"] = {
  192.       outputs = {
  193.         "minecraft:sand",
  194.       }
  195.     },
  196.  
  197.     ["minecraft:cocoa_beans"] = {
  198.       outputs = {
  199.         "minecraft:brown_dye",
  200.       }
  201.     },
  202.  
  203.     ["minecraft:poppy"] = {
  204.       outputs = {
  205.         "minecraft:red_dye"
  206.       }
  207.     },
  208.  
  209.     ["minecraft:dandelion"] = {
  210.       outputs = {
  211.         "minecraft:yellow_dye"
  212.       }
  213.     },
  214.  
  215.     ["minecraft:rose_bush"] = {
  216.       outputs = {
  217.         "minecraft:red_dye",
  218.         "minecraft:green_dye"
  219.       }
  220.     },
  221.  
  222.     ["minecraft:peony"] = {
  223.       outputs = {
  224.         "minecraft:pink_dye",
  225.         "minecraft:magenta_dye"
  226.       }
  227.     },
  228.  
  229.     ["minecraft:cornflower"] = {
  230.       outputs = {
  231.         "minecraft:blue_dye"
  232.       }
  233.     },
  234.  
  235.     ["minecraft:lily_of_the_valley"] = {
  236.       outputs = {
  237.         "minecraft:white_dye",
  238.         "minecraft:lime_dye"
  239.       }
  240.     },
  241.  
  242.     ["ae2:fluix_crystal"] = {
  243.       outputs = {
  244.         "ae2:fluix_dust"
  245.       }
  246.     },
  247.  
  248.     ["minecraft:wool"] = {
  249.       outputs = {
  250.         "minecraft:string"
  251.       }
  252.     },
  253.  
  254.     ["minecraft:gravel"] = {
  255.       outputs = {
  256.         "minecraft:flint"
  257.       }
  258.     },
  259.  
  260.   },
  261.  
  262.   -- Balancer thresholds
  263.   scarceLT = 9,      -- <9 scarce
  264.   availLE = 64,      -- 9..64 available
  265.   abundLE = 128,     -- 64..128 abundant
  266.   reserveCount = 8,  -- keep at least this many of the INPUT unprocessed in Case #2
  267.  
  268.   -- Push/pull parameters
  269.   maxPushPerCycle = 9,         -- conservative push to prevent depleting scarce resources
  270.   maxOutputPullPerCycle = 512, -- cap pulls per loop to avoid huge bursts
  271.  
  272.   -- Sleep timings
  273.   sleepShort = 2,   -- after any action
  274.   sleepIdle = 5,    -- when nothing to do
  275.   sleepError = 5,   -- on warnings
  276.  
  277.   -- Logging
  278.   verbose = true,
  279. }
  280.  
  281. -- =========================
  282. -- ====== UTILITIES ========
  283. -- =========================
  284. local function log(...)
  285.   if CONFIG.verbose then
  286.     local t = {}
  287.     for i = 1, select("#", ...) do t[#t+1] = tostring(select(i, ...)) end
  288.     print(table.concat(t))
  289.   end
  290. end
  291.  
  292. local function isInv(name)
  293.   if not peripheral.isPresent(name) then return false end
  294.   local m = peripheral.getMethods(name) or {}
  295.   local has = {}
  296.   for _,x in ipairs(m) do has[x] = true end
  297.   return has.list and (has.pushItems or has.pullItems)
  298. end
  299.  
  300. local function allInventories()
  301.   local out = {}
  302.   for _,n in ipairs(peripheral.getNames()) do
  303.     if isInv(n) then table.insert(out, n) end
  304.   end
  305.   table.sort(out)
  306.   return out
  307. end
  308.  
  309. local function list(name)
  310.   local ok, res = pcall(function() return peripheral.call(name, "list") end)
  311.   if ok and type(res) == "table" then return res end
  312.   return {}
  313. end
  314. local function getItemDetail(name, slot)
  315.   local ok, d = pcall(function() return peripheral.call(name, "getItemDetail", slot) end)
  316.   if ok then return d end
  317.   return nil
  318. end
  319.  
  320. local function maxCountFor(name, slot)
  321.   local d = getItemDetail(name, slot)
  322.   if d and d.maxCount then return d.maxCount end
  323.   return 64
  324. end
  325.  
  326. local function countInInv(name, id)
  327.   local total = 0
  328.   for _, it in pairs(list(name)) do
  329.     if it.name == id then total = total + (it.count or 0) end
  330.   end
  331.   return total
  332. end
  333.  
  334. local function countAcrossNetwork(id, invs)
  335.   local total = 0
  336.   for _, n in ipairs(invs) do
  337.     total = total + countInInv(n, id)
  338.   end
  339.   return total
  340. end
  341. local function stacksAcrossNetwork(id, invs, exclude)
  342.   local out = {}
  343.   for _, n in ipairs(invs) do
  344.     if n ~= exclude then
  345.       for slot, it in pairs(list(n)) do
  346.         if it.name == id and (it.count or 0) > 0 then
  347.           table.insert(out, { periph = n, slot = slot, count = it.count })
  348.         end
  349.       end
  350.     end
  351.   end
  352.   return out
  353. end
  354.  
  355. local function invFreeSlots(name)
  356.   local okS, size = pcall(function() return peripheral.call(name, "size") end)
  357.   if not okS or type(size) ~= "number" then return 0 end
  358.   local used = 0
  359.   for _ in pairs(list(name)) do used = used + 1 end
  360.   return math.max(0, size - used)
  361. end
  362.  
  363. local function canReach(src, dst)
  364.   local ok, err = pcall(function() peripheral.call(src, "pushItems", dst, 1, 0) end)
  365.   if ok then return true end
  366.   local msg = tostring(err or "")
  367.   if msg:find("does not exist") or msg:find("No such peripheral") then return false end
  368.   return true
  369. end
  370.  
  371. -- Prefer an inventory that already has the item, else fallback chest, else max free slots
  372. local function chooseOutputTarget(itemId, invs, mill, fallback)
  373.   local best, bestCount = nil, -1
  374.   for _, n in ipairs(invs) do
  375.     if n ~= mill then
  376.       local c = countInInv(n, itemId)
  377.       if c > 0 and canReach(mill, n) then
  378.         if c > bestCount then best, bestCount = n, c end
  379.       end
  380.     end
  381.   end
  382.   if best then return best end
  383.   if fallback and fallback ~= mill and canReach(mill, fallback) then return fallback end
  384.   -- else pick most free slots
  385.   local mfName, mf = nil, -1
  386.   for _, n in ipairs(invs) do
  387.     if n ~= mill and canReach(mill, n) then
  388.       local free = invFreeSlots(n)
  389.       if free > mf then mfName, mf = n, free end
  390.     end
  391.   end
  392.   return mfName
  393. end
  394.  
  395. local function classify(amount)
  396.   if amount < CONFIG.scarceLT then return "scarce"
  397.   elseif amount <= CONFIG.availLE then return "available"
  398.   elseif amount <= CONFIG.abundLE then return "abundant"
  399.   else return "trash" end
  400. end
  401.  
  402. local function sumCounts(ids, invs)
  403.   local t = 0
  404.   for _, id in ipairs(ids) do
  405.     t = t + countAcrossNetwork(id, invs)
  406.   end
  407.   return t
  408. end
  409.  
  410. local function outputsStatus(outputs, invs, classifyFn)
  411.   local per = {}
  412.   local allAbundant = true
  413.   local anyBelowAbundant = false
  414.   local anyScarce = false
  415.  
  416.   for _, oid in ipairs(outputs or {}) do
  417.     local c = countAcrossNetwork(oid, invs)
  418.     local b = classifyFn(c) -- uses CONFIG thresholds
  419.     per[oid] = { count = c, band = b }
  420.  
  421.     if b == "scarce" or b == "available" then
  422.       anyBelowAbundant = true
  423.     end
  424.     if b == "scarce" then
  425.       anyScarce = true
  426.     end
  427.     if b ~= "abundant" and b ~= "trash" then
  428.       allAbundant = false
  429.     end
  430.   end
  431.  
  432.   -- If there are no outputs mapped, treat as "unknown" => do not block processing
  433.   if not outputs or #outputs == 0 then
  434.     allAbundant = false
  435.     anyBelowAbundant = true
  436.   end
  437.  
  438.   return per, allAbundant, anyBelowAbundant, anyScarce
  439. end
  440.  
  441. -- =========================
  442. -- ===== CORE ACTIONS ======
  443. -- =========================
  444. local function pushToMill(fromStacks, mill, maxToPush)
  445.   local remain = maxToPush
  446.   local movedTotal = 0
  447.   for _, st in ipairs(fromStacks) do
  448.     if remain <= 0 then break end
  449.     local move = math.min(remain, st.count)
  450.     local ok, movedOrErr = pcall(function()
  451.       -- No toSlot: let the mill route to valid input
  452.       return peripheral.call(st.periph, "pushItems", mill, st.slot, move)
  453.     end)
  454.     if ok and type(movedOrErr) == "number" and movedOrErr > 0 then
  455.       remain = remain - movedOrErr
  456.       movedTotal = movedTotal + movedOrErr
  457.       log("[MILL] Pushed ", movedOrErr, " from ", st.periph, " -> ", mill)
  458.     else
  459.       log("[WARN] push to mill failed from ", st.periph, ": ", tostring(movedOrErr))
  460.     end
  461.   end
  462.   return movedTotal
  463. end
  464.  
  465. local function pullOutputsFromMill(mill, invs, inputsSet)
  466.   local lst = list(mill)
  467.   local pulled = 0
  468.   local pulledItems = {}
  469.  
  470.   for slot, it in pairs(lst) do
  471.     if it and it.name then
  472.       local id = it.name
  473.       local isInput = inputsSet[id] == true
  474.  
  475.       if (not isInput) and it.count > 0 then
  476.         local dest = chooseOutputTarget(id, invs, mill, CONFIG.fallbackOutputChest)
  477.         if dest then
  478.           local moved = peripheral.call(mill, "pushItems", dest, slot, it.count)
  479.           if moved and moved > 0 then
  480.             pulled = pulled + moved
  481.             pulledItems[id] = (pulledItems[id] or 0) + moved
  482.             log("[ROUTE] ", id, " x", moved, " -> ", dest)
  483.           end
  484.         else
  485.           log("[WARN] No reachable output destination for ", id, " (holding in mill for now)")
  486.         end
  487.       end
  488.     end
  489.   end
  490.  
  491.   return pulled, pulledItems
  492. end
  493.  
  494. local function disposeExcessInput(inputId, overAmount, invs, mill, trash)
  495.   if not trash or not peripheral.isPresent(trash) then return 0 end
  496.   if overAmount <= 0 then return 0 end
  497.   local stacks = stacksAcrossNetwork(inputId, invs, mill)
  498.   local remaining = overAmount
  499.   local disposed = 0
  500.   for _, st in ipairs(stacks) do
  501.     if remaining <= 0 then break end
  502.     local move = math.min(remaining, st.count)
  503.     local ok, movedOrErr = pcall(function()
  504.       return peripheral.call(st.periph, "pushItems", trash, st.slot, move)
  505.     end)
  506.     if ok and type(movedOrErr) == "number" and movedOrErr > 0 then
  507.       remaining = remaining - movedOrErr
  508.       disposed = disposed + movedOrErr
  509.       log("[TRASH] ", inputId, " x", movedOrErr, " from ", st.periph, " -> ", trash)
  510.     else
  511.       log("[WARN] Dispose failed from ", st.periph, " -> ", trash, ": ", tostring(movedOrErr))
  512.     end
  513.   end
  514.   return disposed
  515. end
  516.  
  517. -- =========================
  518. -- ====== MAIN LOOP ========
  519. -- =========================
  520. local function tick()
  521.   local mill = CONFIG.mill
  522.   if not peripheral.isPresent(mill) then
  523.     log("[WARN] Mill not present: ", mill)
  524.     sleep(CONFIG.sleepError)
  525.     return
  526.   end
  527.  
  528.   local invs = allInventories()
  529.   if #invs == 0 then
  530.     log("[INFO] No inventories on network.")
  531.     sleep(CONFIG.sleepIdle)
  532.     return
  533.   end
  534.  
  535.   -- Build a set of input IDs for quick checks
  536.   local inputsSet, recipeOrder = {}, {}
  537.   for inputId, _ in pairs(CONFIG.inputs) do
  538.     inputsSet[inputId] = true
  539.     table.insert(recipeOrder, inputId)
  540.   end
  541.   table.sort(recipeOrder)
  542.  
  543.   local didSomething = false
  544.  
  545.   for _, inputId in ipairs(recipeOrder) do
  546.     local recipe = CONFIG.inputs[inputId]
  547.     local outputs = recipe.outputs or {}
  548.     local inputCount = countAcrossNetwork(inputId, invs)
  549.     if inputCount <= 0 then
  550.       log("[SKIP] No input found: ", inputId)
  551.       goto continue_input
  552.     end
  553.  
  554.     local inputBand = classify(inputCount)
  555.     local per, allAbundant, anyBelowAbundant, anyScarce = outputsStatus(outputs, invs, classify)
  556.  
  557.     -- Debug snapshot
  558.     do
  559.       local dbg = {}
  560.       for oid, st in pairs(per) do
  561.         table.insert(dbg, (oid .. "=" .. st.count .. "(" .. st.band .. ")"))
  562.       end
  563.       table.sort(dbg)
  564.       log(string.format("[EVAL] %s input=%d(%s) | outputs: %s",
  565.         inputId, inputCount, inputBand, table.concat(dbg, ", ")))
  566.     end
  567.  
  568.     -- Decision rules:
  569.  
  570.     if inputBand == "scarce" then
  571.       -- Rule: scarce inputs are preserved (don't process)
  572.       log("[HOLD] Scarce input, skipping: ", inputId)
  573.  
  574.     elseif inputBand == "available" then
  575.       -- Process only if ANY output is SCARCE (not just below abundant)
  576.       if anyScarce then
  577.         local stacks = stacksAcrossNetwork(inputId, invs, mill)
  578.         -- Reserve a floor of input; process only what's above reserve
  579.         local pushBudget = math.max(0, inputCount - CONFIG.reserveCount)
  580.         pushBudget = math.min(pushBudget, CONFIG.maxPushPerCycle)
  581.         if pushBudget > 0 then
  582.           local moved = pushToMill(stacks, mill, pushBudget)
  583.           if moved > 0 then didSomething = true end
  584.         else
  585.           log("[OK] Reserve intact; nothing to push for ", inputId)
  586.         end
  587.       else
  588.         log("[OK] No scarce outputs; skipping processing for available input ", inputId)
  589.       end
  590.     elseif inputBand == "abundant" then
  591.       -- Process if ANY output is below abundant; otherwise skip
  592.       if anyBelowAbundant then
  593.         local stacks = stacksAcrossNetwork(inputId, invs, mill)
  594.         local moved = pushToMill(stacks, mill, CONFIG.maxPushPerCycle)
  595.         if moved > 0 then didSomething = true end
  596.       else
  597.         log("[OK] All outputs abundant; nothing to process for ", inputId)
  598.       end
  599.  
  600.     else -- inputBand == "trash" (> 128)
  601.       if anyBelowAbundant then
  602.         -- Still need outputs: process rather than dispose
  603.         local stacks = stacksAcrossNetwork(inputId, invs, mill)
  604.         local moved = pushToMill(stacks, mill, CONFIG.maxPushPerCycle)
  605.         if moved > 0 then didSomething = true end
  606.       else
  607.         -- All outputs abundant: safe to dispose input above 128 if trash is configured
  608.         if CONFIG.trashPeripheral then
  609.           local over = inputCount - CONFIG.abundLE
  610.           if over > 0 then
  611.             local disposed = disposeExcessInput(inputId, over, invs, mill, CONFIG.trashPeripheral)
  612.             if disposed > 0 then didSomething = true end
  613.           else
  614.             log("[OK] No excess to dispose for ", inputId)
  615.           end
  616.         else
  617.           log("[OK] Would dispose excess of ", inputId, " but no trash configured")
  618.         end
  619.       end
  620.     end
  621.  
  622.     ::continue_input::
  623.   end
  624.  
  625.   -- After any pushes, pull outputs once to clear machine buffers
  626.   local pulled, pulledItems = pullOutputsFromMill(mill, invs, inputsSet)
  627.   if pulled > 0 then
  628.     didSomething = true
  629.   end
  630.  
  631.   if didSomething then
  632.     sleep(CONFIG.sleepShort)
  633.   else
  634.     sleep(CONFIG.sleepIdle)
  635.   end
  636. end
  637.  
  638. local function main()
  639.   print("Mill Manager starting...")
  640.   while true do
  641.     tick()
  642.   end
  643. end
  644.  
  645. main()
Add Comment
Please, Sign In to add comment