hornedcommando

Minecraft ComputerCraft Modem GeologistManager

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