diff --git a/inventoryManager.lua b/inventoryManager.lua index c6e69c6..a94c119 100644 --- a/inventoryManager.lua +++ b/inventoryManager.lua @@ -8,6 +8,9 @@ local MONITOR_SIDE = "left" local SCAN_INTERVAL = 120 -- seconds between full background scans local SMELT_INTERVAL = 3 -- seconds between furnace checks local SMELT_RESERVE = 64 -- keep at least 1 stack of each raw material +local DEFRAG_INTERVAL = 60 -- seconds between defrag passes +local COMPOST_INTERVAL = 10 -- seconds between composter checks +local ALERT_INTERVAL = 15 -- seconds between alert re-checks local CACHE_FILE = ".inventory_cache" -- persistent cache file local SMELTER_MONITOR_SIDE = "top" local DISABLED_RECIPES_FILE = ".disabled_recipes" @@ -207,6 +210,155 @@ local FUEL_LIST = { local FUEL_SET = {} for _, f in ipairs(FUEL_LIST) do FUEL_SET[f.name] = true end +------------------------------------------------- +-- Compostable items (pushed into composters) +-- Each item has a chance to raise the compost level: +-- 1.0 = always raises level, 0.3 = 30% chance etc. +-- CC:Tweaked composters accept pushItems; the +-- composter itself decides whether the item counts. +------------------------------------------------- + +local COMPOSTABLE = { + -- Seeds & crops + "minecraft:wheat_seeds", + "minecraft:beetroot_seeds", + "minecraft:pumpkin_seeds", + "minecraft:melon_seeds", + "minecraft:torchflower_seeds", + "minecraft:pitcher_pod", + "minecraft:wheat", + "minecraft:beetroot", + "minecraft:carrot", + "minecraft:melon_slice", + "minecraft:pumpkin", + "minecraft:carved_pumpkin", + "minecraft:sweet_berries", + "minecraft:glow_berries", + -- Plant blocks + "minecraft:tall_grass", + "minecraft:short_grass", + "minecraft:fern", + "minecraft:large_fern", + "minecraft:dead_bush", + "minecraft:vine", + "minecraft:hanging_roots", + "minecraft:small_dripleaf", + "minecraft:big_dripleaf", + "minecraft:moss_block", + "minecraft:moss_carpet", + "minecraft:azalea", + "minecraft:flowering_azalea", + "minecraft:spore_blossom", + "minecraft:seagrass", + "minecraft:sea_pickle", + "minecraft:lily_pad", + "minecraft:sugar_cane", + "minecraft:kelp", + "minecraft:dried_kelp", + "minecraft:cactus", + "minecraft:bamboo", + "minecraft:nether_wart", + "minecraft:crimson_fungus", + "minecraft:warped_fungus", + "minecraft:crimson_roots", + "minecraft:warped_roots", + "minecraft:shroomlight", + "minecraft:weeping_vines", + "minecraft:twisting_vines", + -- Leaves + "minecraft:oak_leaves", + "minecraft:spruce_leaves", + "minecraft:birch_leaves", + "minecraft:jungle_leaves", + "minecraft:acacia_leaves", + "minecraft:dark_oak_leaves", + "minecraft:mangrove_leaves", + "minecraft:cherry_leaves", + "minecraft:azalea_leaves", + "minecraft:flowering_azalea_leaves", + -- Flowers + "minecraft:dandelion", + "minecraft:poppy", + "minecraft:blue_orchid", + "minecraft:allium", + "minecraft:azure_bluet", + "minecraft:red_tulip", + "minecraft:orange_tulip", + "minecraft:white_tulip", + "minecraft:pink_tulip", + "minecraft:oxeye_daisy", + "minecraft:cornflower", + "minecraft:lily_of_the_valley", + "minecraft:sunflower", + "minecraft:lilac", + "minecraft:rose_bush", + "minecraft:peony", + "minecraft:wither_rose", + "minecraft:torchflower", + "minecraft:pitcher_plant", + -- Saplings + "minecraft:oak_sapling", + "minecraft:spruce_sapling", + "minecraft:birch_sapling", + "minecraft:jungle_sapling", + "minecraft:acacia_sapling", + "minecraft:dark_oak_sapling", + "minecraft:mangrove_propagule", + "minecraft:cherry_sapling", + -- Food waste + "minecraft:rotten_flesh", + "minecraft:spider_eye", + "minecraft:poisonous_potato", + "minecraft:fermented_spider_eye", + "minecraft:apple", + "minecraft:bread", + "minecraft:cookie", + "minecraft:cake", + "minecraft:pumpkin_pie", + -- Farmer's Delight compostables + "farmersdelight:tree_bark", + "farmersdelight:straw", + "farmersdelight:canvas", + "farmersdelight:rice", + "farmersdelight:rice_panicle", + "farmersdelight:onion", + "farmersdelight:tomato", + "farmersdelight:cabbage", + "farmersdelight:cabbage_leaf", +} + +-- Build set for quick lookup +local COMPOSTABLE_SET = {} +for _, name in ipairs(COMPOSTABLE) do COMPOSTABLE_SET[name] = true end + +-- Reserve: keep at least this many of each compostable before composting the rest +local COMPOST_RESERVE = 16 + +------------------------------------------------- +-- Low-stock alerts +-- When a tracked item drops below 'min', an alert +-- is shown on the inventory monitor. +------------------------------------------------- + +local LOW_STOCK_ALERTS = { + { name = "minecraft:coal", min = 64, label = "Coal" }, + { name = "minecraft:charcoal", min = 64, label = "Charcoal" }, + { name = "minecraft:torch", min = 64, label = "Torches" }, + { name = "minecraft:arrow", min = 64, label = "Arrows" }, + { name = "minecraft:cooked_beef", min = 32, label = "Steak" }, + { name = "minecraft:cooked_porkchop",min = 32, label = "Porkchops" }, + { name = "minecraft:bread", min = 32, label = "Bread" }, + { name = "minecraft:iron_ingot", min = 64, label = "Iron" }, + { name = "minecraft:gold_ingot", min = 32, label = "Gold" }, + { name = "minecraft:diamond", min = 16, label = "Diamond" }, + { name = "minecraft:bone_meal", min = 32, label = "Bone Meal" }, + { name = "minecraft:oak_planks", min = 64, label = "Planks" }, + { name = "minecraft:cobblestone", min = 128, label = "Cobblestone" }, +} + +-- Active alerts (populated by checkAlerts) +local activeAlerts = {} + ------------------------------------------------- -- Cached data (updated by background scanner) ------------------------------------------------- @@ -235,6 +387,8 @@ local activity = { dispensing = false, -- order in progress scanning = false, -- background scan in progress smelting = false, -- auto-smelt in progress + defragging = false, -- defrag in progress + composting = false, -- auto-compost in progress } ------------------------------------------------- @@ -731,6 +885,8 @@ local function drawDashboard() if activity.dispensing then table.insert(actParts, "DISPENSING") end if activity.smelting then table.insert(actParts, "SMELTING") end if activity.scanning then table.insert(actParts, "SCANNING") end + if activity.defragging then table.insert(actParts, "DEFRAG") end + if activity.composting then table.insert(actParts, "COMPOST") end monWrite(2, 2, table.concat(statusParts, " | "), colors.white, colors.gray) @@ -956,7 +1112,13 @@ local function drawDashboard() else -- ===== Status message (h-2) ===== monFill(h - 2, colors.black) - if statusTimer > 0 and #statusMessage > 0 then + if #activeAlerts > 0 then + -- Show low-stock alerts scrolling through them + local alertIdx = math.floor(os.epoch("utc") / 2000) % #activeAlerts + 1 + local a = activeAlerts[alertIdx] + local alertMsg = string.format(" LOW STOCK: %s (%d/%d) ", a.label, a.current, a.min) + monCenter(h - 2, alertMsg, colors.white, colors.red) + elseif statusTimer > 0 and #statusMessage > 0 then monCenter(h - 2, statusMessage, statusColor, colors.black) end @@ -983,6 +1145,10 @@ local function drawDashboard() bottomMsg = " SMELTING... " elseif activity.sorting then bottomMsg = " SORTING BARREL... " + elseif activity.defragging then + bottomMsg = " DEFRAGMENTING... " + elseif activity.composting then + bottomMsg = " COMPOSTING... " end monCenter(h, bottomMsg, colors.lightBlue, colors.blue) end @@ -1550,6 +1716,207 @@ local function autoSmelt() return didWork end +------------------------------------------------- +-- Defrag (consolidate partial stacks) +------------------------------------------------- + +local function defragInventory() + local chests = getChests() + if #chests == 0 then return end + + activity.defragging = true + needsRedraw = true + + -- Build a map: itemName -> list of { chest, slot, count, maxCount } + local itemSlots = {} + for _, chestName in ipairs(chests) do + local inv = peripheral.wrap(chestName) + if inv then + local contents = inv.list() + local detail_cache = {} + for slot, item in pairs(contents) do + if not itemSlots[item.name] then + itemSlots[item.name] = {} + end + -- CC:Tweaked: getItemDetail gives maxCount (stack size) + local maxCount = 64 + local ok, detail = pcall(inv.getItemDetail, slot) + if ok and detail and detail.maxCount then + maxCount = detail.maxCount + end + table.insert(itemSlots[item.name], { + chest = chestName, + slot = slot, + count = item.count, + max = maxCount, + }) + end + end + end + + -- For each item, try to merge partial stacks + local totalMerged = 0 + for itemName, slots in pairs(itemSlots) do + -- Sort: smallest stacks first (donors), fullest last (receivers) + table.sort(slots, function(a, b) return a.count < b.count end) + + local i = 1 -- donor (smallest) + local j = #slots -- receiver (largest, has room) + + while i < j do + local donor = slots[i] + local recv = slots[j] + + -- Skip if same slot + if donor.chest == recv.chest and donor.slot == recv.slot then + i = i + 1 + elseif donor.count == 0 then + i = i + 1 + elseif recv.count >= recv.max then + j = j - 1 + else + local space = recv.max - recv.count + local toMove = math.min(donor.count, space) + local donorInv = peripheral.wrap(donor.chest) + if donorInv then + local n = donorInv.pushItems(recv.chest, donor.slot, toMove, recv.slot) + if n and n > 0 then + donor.count = donor.count - n + recv.count = recv.count + n + totalMerged = totalMerged + n + end + end + if donor.count <= 0 then i = i + 1 end + if recv.count >= recv.max then j = j - 1 end + end + end + end + + if totalMerged > 0 then + print(string.format("[DEFRAG] Consolidated %d items", totalMerged)) + end + + activity.defragging = false + needsRedraw = true +end + +------------------------------------------------- +-- Auto-compost +------------------------------------------------- + +local function getComposters() + local composters = {} + for _, name in ipairs(peripheral.getNames()) do + if peripheral.getType(name) == "minecraft:composter" then + table.insert(composters, name) + end + end + return composters +end + +local function autoCompost() + local composters = getComposters() + if #composters == 0 then return end + + local chests = getChests() + local catalogue = cache.catalogue + local didWork = false + + for _, cname in ipairs(composters) do + local composter = peripheral.wrap(cname) + if composter then + -- 1) Pull output (bone meal, slot 1 when full) back to chests + local contents = composter.list() + if contents then + for slot, item in pairs(contents) do + if item.name == "minecraft:bone_meal" then + for _, chest in ipairs(chests) do + local n = composter.pushItems(chest, slot) + if n and n > 0 then + adjustCache("minecraft:bone_meal", chest, n) + print(string.format("[COMPOST] Bone meal x%d -> %s", n, chest)) + didWork = true + break + end + end + end + end + end + + -- 2) Feed compostable items (push 1 at a time — composter has 1 slot) + contents = composter.list() + local hasInput = contents and next(contents) + if not hasInput then + for _, itemName in ipairs(COMPOSTABLE) do + if catalogue[itemName] then + -- Count total in storage + local totalInStorage = 0 + for _, src in ipairs(catalogue[itemName]) do + totalInStorage = totalInStorage + src.total + end + + local available = totalInStorage - COMPOST_RESERVE + if available > 0 then + -- Push 1 item at a time to the composter + local fed = false + for _, source in ipairs(catalogue[itemName]) do + local chest = peripheral.wrap(source.chest) + if chest then + for slot, slotItem in pairs(chest.list()) do + if slotItem.name == itemName then + local n = chest.pushItems(cname, slot, 1) + if n and n > 0 then + adjustCache(itemName, source.chest, -n) + print(string.format("[COMPOST] Fed %s x%d -> %s", + itemName, n, cname)) + didWork = true + fed = true + break + end + end + end + end + if fed then break end + end + if fed then break end -- composter now has item, move to next composter + end + end + end + end + end + end + + return didWork +end + +------------------------------------------------- +-- Low-stock alert checker +------------------------------------------------- + +local function checkAlerts() + local alerts = {} + for _, alert in ipairs(LOW_STOCK_ALERTS) do + local total = 0 + if cache.catalogue[alert.name] then + for _, src in ipairs(cache.catalogue[alert.name]) do + total = total + src.total + end + end + if total < alert.min then + table.insert(alerts, { + label = alert.label, + current = total, + min = alert.min, + }) + end + end + activeAlerts = alerts + if #alerts > 0 then + needsRedraw = true + smelterNeedsRedraw = true + end +end + ------------------------------------------------- -- Order ------------------------------------------------- @@ -1813,6 +2180,16 @@ local function main() end print(string.format("[INIT] %d/%d recipes enabled", enabledCount, totalRecipeCount)) + -- Detect composters + local composters = getComposters() + if #composters > 0 then + print(string.format("[OK] Composters: %d", #composters)) + else + print("[INFO] No composters on network") + end + + print(string.format("[INIT] Tracking %d low-stock alerts", #LOW_STOCK_ALERTS)) + print("") print("Console shows log. Use the monitors to interact.") print("") @@ -1899,6 +2276,7 @@ local function main() -- If we loaded from disk cache, refresh immediately in background if cacheLoaded then pcall(refreshCache) + pcall(checkAlerts) needsRedraw = true smelterNeedsRedraw = true print("[INIT] Background refresh complete. " .. #cache.itemList .. " types.") @@ -1906,6 +2284,7 @@ local function main() while true do sleep(SCAN_INTERVAL) pcall(refreshCache) + pcall(checkAlerts) needsRedraw = true smelterNeedsRedraw = true end @@ -1935,7 +2314,45 @@ local function main() end end, - -- Task 4: Inventory dashboard redraw (event-driven, checks every 0.1s) + -- Task 4: Defrag (consolidate partial stacks) + function() + sleep(10) -- initial delay to let first scan finish + while true do + activity.defragging = true + needsRedraw = true + pcall(defragInventory) + activity.defragging = false + needsRedraw = true + sleep(DEFRAG_INTERVAL) + end + end, + + -- Task 5: Auto-compost + function() + while true do + activity.composting = true + needsRedraw = true + pcall(autoCompost) + activity.composting = false + needsRedraw = true + pcall(checkAlerts) + sleep(COMPOST_INTERVAL) + end + end, + + -- Task 6: Low-stock alert checker + function() + sleep(5) -- initial delay + pcall(checkAlerts) + needsRedraw = true + while true do + sleep(ALERT_INTERVAL) + pcall(checkAlerts) + needsRedraw = true + end + end, + + -- Task 7: Inventory dashboard redraw (event-driven, checks every 0.1s) function() needsRedraw = true while true do @@ -1951,11 +2368,15 @@ local function main() needsRedraw = true end end + -- Redraw periodically for alert cycling + if #activeAlerts > 0 then + needsRedraw = true + end sleep(0.1) end end, - -- Task 5: Smelter dashboard redraw + -- Task 8: Smelter dashboard redraw function() smelterNeedsRedraw = true while true do @@ -1967,7 +2388,7 @@ local function main() end end, - -- Task 6: Touch event listener (both monitors) + -- Task 9: Touch event listener (both monitors) function() while true do local event, side, x, y = os.pullEvent("monitor_touch")