Add composting functionality and defragmentation process to inventory management
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user