hornedcommando

Minecraft ComputerCraft Modem ArchaeologistManager

Nov 23rd, 2025 (edited)
80
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 16.90 KB | Gaming | 0 0
  1.  
  2. --
  3. -- Desc: An Unearther Archaeologist manager program for FTB Stoneblock 4
  4. -- Manages the Archaeologist with specific slot handling:
  5. -- Slot 1: Material input (dirt, sand, dust)
  6. -- Slot 2: Food (optional, improves processing speed)
  7. -- Slot 3: Brush (required, auto-supplied from inventory)
  8. -- Slots 4-12: Output slots (carefully managed to avoid disrupting hidden token slot)
  9. -- IMPORTANT!: as you connect modems to archaeologists/chests, update the config to match
  10. -- their names, or this will not work correctly.
  11. --
  12.  
  13. -- By: hornedcommando
  14.  
  15. -- =========================
  16. -- ========== CONFIG =======
  17. -- =========================
  18.  
  19. local CONFIG = {
  20.   -- Your Unearther Archaeologist peripheral name
  21.   archaeologist = "ftbunearthed:frame_1",
  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 = "metalbarrels:crystal_2",
  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 archaeologist recipes
  49.   inputs = {
  50.     ["minecraft:dirt"] = {
  51.       outputs = {
  52.         "ars_nouveau:blue_archwood_sapling",
  53.         "ars_nouveau:red_archwood_sapling",
  54.         "ars_nouveau:green_archwood_sapling",
  55.         "ars_elemental:yellow_archwood_sapling",
  56.         "ars_nouveau:purple_archwood_sapling",
  57.         "ftb:andesite_pebble",
  58.         "ftb:diorite_pebble",
  59.         "jags:grass_seed"
  60.       }
  61.     },
  62.  
  63.     ["minecraft:sand"] = {
  64.       outputs = {
  65.         "minecraft:clay_ball",
  66.         "ftbmaterials:copper_chunk",
  67.         "ftbmaterials:silver_chunk",
  68.         "ftbmaterials:nickel_chunk",
  69.         "ftbmaterials:uranium_chunk"
  70.       }
  71.     },
  72.  
  73.     ["ftbstuff:dust"] = {
  74.       outputs = {
  75.         "minecraft:redstone",
  76.         "ftbmaterials:gold_chunk",
  77.         "ftbmaterials:tin_chunk",
  78.         "minecraft:bone_meal"
  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, archaeologist, fallback)
  194.   local best, bestCount = nil, -1
  195.   for _, n in ipairs(invs) do
  196.     if n ~= archaeologist then
  197.       local c = countInInv(n, itemId)
  198.       if c > 0 and canReach(archaeologist, 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 ~= archaeologist and canReach(archaeologist, 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 ~= archaeologist and canReach(archaeologist, 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(archaeologist, invs)
  268.   local archList = list(archaeologist)
  269.   local slot2 = archList[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, archaeologist)
  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", archaeologist, 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(archaeologist, invs)
  293.   local archList = list(archaeologist)
  294.   local slot3 = archList[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, archaeologist)
  300.       for _, st in ipairs(stacks) do
  301.         local ok, moved = pcall(function()
  302.           return peripheral.call(st.periph, "pushItems", archaeologist, 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 pushToArchaeologist(fromStacks, archaeologist, 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", archaeologist, 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("[ARCHAEOLOGIST] Pushed ", movedOrErr, " from ", st.periph, " -> ", archaeologist, " slot 1")
  330.     else
  331.       log("[WARN] push to archaeologist failed from ", st.periph, ": ", tostring(movedOrErr))
  332.     end
  333.   end
  334.   return movedTotal
  335. end
  336.  
  337. local function pullOutputsFromArchaeologist(archaeologist, invs, inputsSet)
  338.   local lst = list(archaeologist)
  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, archaeologist, CONFIG.fallbackOutputChest)
  364.         if dest then
  365.           local moved = peripheral.call(archaeologist, "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 archaeologist 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, archaeologist, 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, archaeologist)
  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 archaeologist = CONFIG.archaeologist
  409.   if not peripheral.isPresent(archaeologist) then
  410.     log("[WARN] Archaeologist not present: ", archaeologist)
  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(archaeologist, invs)
  424.   manageBrushSlot(archaeologist, 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, anyScarce = 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 SCARCE (not just below abundant)
  467.       if anyScarce then
  468.         local stacks = stacksAcrossNetwork(inputId, invs, archaeologist)
  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 = pushToArchaeologist(stacks, archaeologist, 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] No scarce outputs; skipping processing for available input ", inputId)
  480.       end
  481.     elseif inputBand == "abundant" then
  482.       -- Continue processing abundant inputs until they reach trash level (>128)
  483.       -- This prevents infinite processing when outputs are abundant but input isn't trash yet
  484.       local stacks = stacksAcrossNetwork(inputId, invs, archaeologist)
  485.       local moved = pushToArchaeologist(stacks, archaeologist, CONFIG.maxPushPerCycle)
  486.       if moved > 0 then didSomething = true end
  487.  
  488.     else -- inputBand == "trash" (> 128)
  489.       if anyBelowAbundant then
  490.         -- Still need outputs: process rather than dispose
  491.         local stacks = stacksAcrossNetwork(inputId, invs, archaeologist)
  492.         local moved = pushToArchaeologist(stacks, archaeologist, CONFIG.maxPushPerCycle)
  493.         if moved > 0 then didSomething = true end
  494.       else
  495.         -- All outputs abundant: safe to dispose input above 128 if trash is configured
  496.         if CONFIG.trashPeripheral then
  497.           local over = inputCount - CONFIG.abundLE
  498.           if over > 0 then
  499.             local disposed = disposeExcessInput(inputId, over, invs, archaeologist, CONFIG.trashPeripheral)
  500.             if disposed > 0 then didSomething = true end
  501.           else
  502.             log("[OK] No excess to dispose for ", inputId)
  503.           end
  504.         else
  505.           log("[OK] Would dispose excess of ", inputId, " but no trash configured")
  506.         end
  507.       end
  508.     end
  509.  
  510.     ::continue_input::
  511.   end
  512.  
  513.   -- After any pushes, pull outputs once to clear machine buffers
  514.   local pulled, pulledItems = pullOutputsFromArchaeologist(archaeologist, invs, inputsSet)
  515.   if pulled > 0 then
  516.     didSomething = true
  517.   end
  518.  
  519.   if didSomething then
  520.     sleep(CONFIG.sleepShort)
  521.   else
  522.     sleep(CONFIG.sleepIdle)
  523.   end
  524. end
  525.  
  526. local function main()
  527.   print("Unearther Archaeologist Manager starting...")
  528.   while true do
  529.     tick()
  530.   end
  531. end
  532.  
  533. main()
  534.  
Add Comment
Please, Sign In to add comment