-- Inventory Manager: Touch UI on monitor -- Main computer (networked). Computer 1 sits next to dropper_0 and auto-dispenses. local DROPPER_NAME = "minecraft:dropper_9" local BARREL_NAME = "minecraft:barrel_0" local POLL_INTERVAL = 2 -- seconds between barrel checks local MONITOR_SIDE = "left" local SCAN_INTERVAL = 120 -- seconds between full background scans local SMELT_INTERVAL = 3 -- seconds between furnace checks local SMELT_RESERVE = 128 -- keep at least 2 stacks of each raw material local DEFRAG_INTERVAL = 600 -- seconds between defrag passes (10 min) local COMPOST_INTERVAL = 3 -- 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" -- Network sync (for client displays) local BROADCAST_CHANNEL = 4200 local ORDER_CHANNEL = 4201 local BROADCAST_INTERVAL = 1 -- seconds between state broadcasts -- Crafting turtle local CRAFT_CHANNEL = 4203 local CRAFT_REPLY_CHANNEL = 4204 ------------------------------------------------- -- Furnace types to manage ------------------------------------------------- local FURNACE_TYPES = { "minecraft:furnace", "minecraft:smoker", "minecraft:blast_furnace", } -- Furnace slots: 1 = input, 2 = fuel, 3 = output (standard Minecraft) local SLOT_INPUT = 1 local SLOT_FUEL = 2 local SLOT_OUTPUT = 3 ------------------------------------------------- -- Smeltable items: input -> output -- Items in chests matching a key here get auto-smelted. -- Add/remove entries to control what gets cooked. ------------------------------------------------- local SMELTABLE = { -- Ores (furnace + blast furnace only) ["minecraft:raw_iron"] = { result = "minecraft:iron_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:raw_gold"] = { result = "minecraft:gold_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:raw_copper"] = { result = "minecraft:copper_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:iron_ore"] = { result = "minecraft:iron_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:gold_ore"] = { result = "minecraft:gold_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:copper_ore"] = { result = "minecraft:copper_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:deepslate_iron_ore"] = { result = "minecraft:iron_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:deepslate_gold_ore"] = { result = "minecraft:gold_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:deepslate_copper_ore"] = { result = "minecraft:copper_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:ancient_debris"] = { result = "minecraft:netherite_scrap",furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, -- Sand / stone / clay (regular furnace only — blast furnace rejects these) ["minecraft:sand"] = { result = "minecraft:glass", furnaces = {"minecraft:furnace"} }, ["minecraft:red_sand"] = { result = "minecraft:glass", furnaces = {"minecraft:furnace"} }, ["minecraft:cobblestone"] = { result = "minecraft:stone", furnaces = {"minecraft:furnace"} }, ["minecraft:stone"] = { result = "minecraft:smooth_stone", furnaces = {"minecraft:furnace"} }, ["minecraft:clay_ball"] = { result = "minecraft:brick", furnaces = {"minecraft:furnace"} }, ["minecraft:netherrack"] = { result = "minecraft:nether_brick", furnaces = {"minecraft:furnace"} }, ["minecraft:sandstone"] = { result = "minecraft:smooth_sandstone", furnaces = {"minecraft:furnace"} }, -- Food (furnace + smoker only) ["minecraft:beef"] = { result = "minecraft:cooked_beef", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["minecraft:porkchop"] = { result = "minecraft:cooked_porkchop", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["minecraft:chicken"] = { result = "minecraft:cooked_chicken", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["minecraft:mutton"] = { result = "minecraft:cooked_mutton", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["minecraft:rabbit"] = { result = "minecraft:cooked_rabbit", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["minecraft:cod"] = { result = "minecraft:cooked_cod", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["minecraft:salmon"] = { result = "minecraft:cooked_salmon", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["minecraft:potato"] = { result = "minecraft:baked_potato", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["minecraft:kelp"] = { result = "minecraft:dried_kelp", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, -- Misc (furnace only) ["minecraft:wet_sponge"] = { result = "minecraft:sponge", furnaces = {"minecraft:furnace"} }, ["minecraft:cactus"] = { result = "minecraft:green_dye", furnaces = {"minecraft:furnace"} }, ["minecraft:sea_pickle"] = { result = "minecraft:lime_dye", furnaces = {"minecraft:furnace"} }, ["minecraft:log"] = { result = "minecraft:charcoal", furnaces = {"minecraft:furnace"} }, ["minecraft:oak_log"] = { result = "minecraft:charcoal", furnaces = {"minecraft:furnace"} }, ["minecraft:spruce_log"] = { result = "minecraft:charcoal", furnaces = {"minecraft:furnace"} }, ["minecraft:birch_log"] = { result = "minecraft:charcoal", furnaces = {"minecraft:furnace"} }, ["minecraft:jungle_log"] = { result = "minecraft:charcoal", furnaces = {"minecraft:furnace"} }, ["minecraft:acacia_log"] = { result = "minecraft:charcoal", furnaces = {"minecraft:furnace"} }, ["minecraft:dark_oak_log"] = { result = "minecraft:charcoal", furnaces = {"minecraft:furnace"} }, ["minecraft:mangrove_log"] = { result = "minecraft:charcoal", furnaces = {"minecraft:furnace"} }, ["minecraft:cherry_log"] = { result = "minecraft:charcoal", furnaces = {"minecraft:furnace"} }, ------------------------------------------------- -- Mythic Metals — raw ores (furnace + blast furnace) ------------------------------------------------- ["mythicmetals:raw_adamantite"] = { result = "mythicmetals:adamantite_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:raw_aquarium"] = { result = "mythicmetals:aquarium_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:raw_banglum"] = { result = "mythicmetals:banglum_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:raw_carmot"] = { result = "mythicmetals:carmot_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:raw_kyber"] = { result = "mythicmetals:kyber_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:raw_manganese"] = { result = "mythicmetals:manganese_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:raw_midas_gold"] = { result = "mythicmetals:midas_gold_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:raw_mythril"] = { result = "mythicmetals:mythril_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:raw_orichalcum"] = { result = "mythicmetals:orichalcum_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:raw_palladium"] = { result = "mythicmetals:palladium_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:raw_prometheum"] = { result = "mythicmetals:prometheum_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:raw_quadrillum"] = { result = "mythicmetals:quadrillum_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:raw_runite"] = { result = "mythicmetals:runite_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:raw_star_platinum"] = { result = "mythicmetals:star_platinum_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:raw_stormyx"] = { result = "mythicmetals:stormyx_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:raw_tin"] = { result = "mythicmetals:tin_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:raw_silver"] = { result = "mythicmetals:silver_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ------------------------------------------------- -- Mythic Metals — ore blocks (furnace + blast furnace) ------------------------------------------------- ["mythicmetals:adamantite_ore"] = { result = "mythicmetals:adamantite_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:deepslate_adamantite_ore"] = { result = "mythicmetals:adamantite_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:aquarium_ore"] = { result = "mythicmetals:aquarium_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:banglum_ore"] = { result = "mythicmetals:banglum_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:deepslate_banglum_ore"] = { result = "mythicmetals:banglum_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:nether_banglum_ore"] = { result = "mythicmetals:banglum_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:carmot_ore"] = { result = "mythicmetals:carmot_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:deepslate_carmot_ore"] = { result = "mythicmetals:carmot_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:kyber_ore"] = { result = "mythicmetals:kyber_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:deepslate_kyber_ore"] = { result = "mythicmetals:kyber_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:manganese_ore"] = { result = "mythicmetals:manganese_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:deepslate_manganese_ore"] = { result = "mythicmetals:manganese_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:midas_gold_ore"] = { result = "mythicmetals:midas_gold_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:mythril_ore"] = { result = "mythicmetals:mythril_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:deepslate_mythril_ore"] = { result = "mythicmetals:mythril_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:orichalcum_ore"] = { result = "mythicmetals:orichalcum_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:deepslate_orichalcum_ore"] = { result = "mythicmetals:orichalcum_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:end_stone_orichalcum_ore"] = { result = "mythicmetals:orichalcum_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:palladium_ore"] = { result = "mythicmetals:palladium_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:prometheum_ore"] = { result = "mythicmetals:prometheum_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:deepslate_prometheum_ore"] = { result = "mythicmetals:prometheum_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:quadrillum_ore"] = { result = "mythicmetals:quadrillum_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:deepslate_quadrillum_ore"] = { result = "mythicmetals:quadrillum_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:runite_ore"] = { result = "mythicmetals:runite_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:deepslate_runite_ore"] = { result = "mythicmetals:runite_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:star_platinum_ore"] = { result = "mythicmetals:star_platinum_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:end_stone_star_platinum_ore"] = { result = "mythicmetals:star_platinum_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:stormyx_ore"] = { result = "mythicmetals:stormyx_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:end_stone_stormyx_ore"] = { result = "mythicmetals:stormyx_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:tin_ore"] = { result = "mythicmetals:tin_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:deepslate_tin_ore"] = { result = "mythicmetals:tin_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:silver_ore"] = { result = "mythicmetals:silver_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["mythicmetals:deepslate_silver_ore"] = { result = "mythicmetals:silver_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ------------------------------------------------- -- Ad Astra — raw ores + planetary ores (furnace + blast furnace) ------------------------------------------------- ["ad_astra:raw_desh"] = { result = "ad_astra:desh_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["ad_astra:raw_ostrum"] = { result = "ad_astra:ostrum_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["ad_astra:raw_calorite"] = { result = "ad_astra:calorite_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["ad_astra:moon_desh_ore"] = { result = "ad_astra:desh_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["ad_astra:deepslate_desh_ore"] = { result = "ad_astra:desh_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["ad_astra:mars_ostrum_ore"] = { result = "ad_astra:ostrum_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["ad_astra:deepslate_ostrum_ore"] = { result = "ad_astra:ostrum_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["ad_astra:venus_calorite_ore"] = { result = "ad_astra:calorite_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["ad_astra:deepslate_calorite_ore"] = { result = "ad_astra:calorite_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["ad_astra:moon_iron_ore"] = { result = "minecraft:iron_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["ad_astra:mars_iron_ore"] = { result = "minecraft:iron_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["ad_astra:venus_gold_ore"] = { result = "minecraft:gold_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["ad_astra:venus_diamond_ore"] = { result = "minecraft:diamond", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["ad_astra:mars_diamond_ore"] = { result = "minecraft:diamond", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["ad_astra:moon_ice_shard_ore"] = { result = "ad_astra:ice_shard", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ------------------------------------------------- -- Create — zinc + crushed ores (furnace + blast furnace) ------------------------------------------------- ["create:raw_zinc"] = { result = "create:zinc_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["create:zinc_ore"] = { result = "create:zinc_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["create:deepslate_zinc_ore"] = { result = "create:zinc_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["create:crushed_raw_iron"] = { result = "minecraft:iron_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["create:crushed_raw_gold"] = { result = "minecraft:gold_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["create:crushed_raw_copper"] = { result = "minecraft:copper_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["create:crushed_raw_zinc"] = { result = "create:zinc_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ------------------------------------------------- -- BetterEnd — thallasium (furnace + blast furnace) ------------------------------------------------- ["betterend:thallasium_raw"] = { result = "betterend:thallasium_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["betterend:thallasium_ore"] = { result = "betterend:thallasium_ingot", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ------------------------------------------------- -- Farmer's Delight — food (furnace + smoker) ------------------------------------------------- ["farmersdelight:chicken_cuts"] = { result = "farmersdelight:cooked_chicken_cuts", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["farmersdelight:bacon"] = { result = "farmersdelight:cooked_bacon", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["farmersdelight:cod_slice"] = { result = "farmersdelight:cooked_cod_slice", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["farmersdelight:salmon_slice"] = { result = "farmersdelight:cooked_salmon_slice", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["farmersdelight:mutton_chops"] = { result = "farmersdelight:cooked_mutton_chops", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["farmersdelight:ham"] = { result = "farmersdelight:smoked_ham", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, ["farmersdelight:minced_beef"] = { result = "farmersdelight:beef_patty", furnaces = {"minecraft:furnace", "minecraft:smoker"} }, } -- Pre-build furnace compatibility sets for O(1) lookup for _, recipe in pairs(SMELTABLE) do recipe.furnaceSet = {} for _, ft in ipairs(recipe.furnaces) do recipe.furnaceSet[ft] = true end end -- Pre-built smelt candidate lists per furnace type (sorted: food first, then alpha) -- Avoids rebuilding & re-sorting in autoSmelt on every cycle. local smeltCandidatesByType = {} do for _, ftype in ipairs(FURNACE_TYPES) do smeltCandidatesByType[ftype] = {} end for itemName, recipe in pairs(SMELTABLE) do local isFood = recipe.furnaceSet["minecraft:smoker"] or false for ft, _ in pairs(recipe.furnaceSet) do table.insert(smeltCandidatesByType[ft], { name = itemName, recipe = recipe, food = isFood }) end end for _, list in pairs(smeltCandidatesByType) do table.sort(list, function(a, b) if a.food ~= b.food then return a.food end return a.name < b.name end) end end -- Fuel items, ordered by preference (best first) -- burn_time = how many items one fuel smelts local FUEL_LIST = { { name = "minecraft:coal", burn_time = 8 }, { name = "minecraft:charcoal", burn_time = 8 }, { name = "minecraft:coal_block", burn_time = 80 }, { name = "minecraft:blaze_rod", burn_time = 12 }, { name = "minecraft:dried_kelp_block", burn_time = 20 }, { name = "minecraft:lava_bucket", burn_time = 100 }, { name = "minecraft:oak_planks", burn_time = 1.5 }, { name = "minecraft:spruce_planks",burn_time = 1.5 }, { name = "minecraft:birch_planks", burn_time = 1.5 }, { name = "minecraft:jungle_planks",burn_time = 1.5 }, { name = "minecraft:acacia_planks",burn_time = 1.5 }, { name = "minecraft:dark_oak_planks",burn_time = 1.5 }, { name = "minecraft:mangrove_planks",burn_time = 1.5 }, { name = "minecraft:cherry_planks",burn_time = 1.5 }, { name = "minecraft:stick", burn_time = 0.5 }, } -- Build a set for quick lookup local FUEL_SET = {} for _, f in ipairs(FUEL_LIST) do FUEL_SET[f.name] = true end ------------------------------------------------- -- Compostable items (pushed into dropper_10, -- which distributes to composters via hoppers). -- Bone meal returns through hopper_0. ------------------------------------------------- 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: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 = 128 -- 2 stacks local COMPOST_DROPPER = "minecraft:dropper_10" local COMPOST_HOPPER = "minecraft:hopper_0" -- Trash items: compostables with zero reserve (always fully composted) local COMPOST_TRASH = { ["minecraft:rotten_flesh"] = true, ["minecraft:spider_eye"] = true, ["minecraft:poisonous_potato"] = true, ["minecraft:fermented_spider_eye"] = true, } ------------------------------------------------- -- Crafting recipes (for networked crafting turtle) -- grid: 9 entries mapping to turtle slots 1-3, 5-7, 9-11 ------------------------------------------------- local GRID_TO_SLOT = {1, 2, 3, 5, 6, 7, 9, 10, 11} local CRAFTABLE = { -- Basic materials { output = "minecraft:oak_planks", count = 4, grid = { "minecraft:oak_log", nil, nil, nil, nil, nil, nil, nil, nil, }, }, { output = "minecraft:spruce_planks", count = 4, grid = { "minecraft:spruce_log", nil, nil, nil, nil, nil, nil, nil, nil, }, }, { output = "minecraft:birch_planks", count = 4, grid = { "minecraft:birch_log", nil, nil, nil, nil, nil, nil, nil, nil, }, }, { output = "minecraft:stick", count = 4, grid = { "minecraft:oak_planks", nil, nil, "minecraft:oak_planks", nil, nil, nil, nil, nil, }, }, { output = "minecraft:oak_slab", count = 6, grid = { "minecraft:oak_planks", "minecraft:oak_planks", "minecraft:oak_planks", nil, nil, nil, nil, nil, nil, }, }, { output = "minecraft:torch", count = 4, grid = { "minecraft:coal", nil, nil, "minecraft:stick", nil, nil, nil, nil, nil, }, }, -- Crafting & storage { output = "minecraft:crafting_table", count = 1, grid = { "minecraft:oak_planks", "minecraft:oak_planks", nil, "minecraft:oak_planks", "minecraft:oak_planks", nil, nil, nil, nil, }, }, { output = "minecraft:chest", count = 1, grid = { "minecraft:oak_planks", "minecraft:oak_planks", "minecraft:oak_planks", "minecraft:oak_planks", nil, "minecraft:oak_planks", "minecraft:oak_planks", "minecraft:oak_planks", "minecraft:oak_planks", }, }, { output = "minecraft:barrel", count = 1, grid = { "minecraft:oak_planks", "minecraft:oak_slab", "minecraft:oak_planks", "minecraft:oak_planks", nil, "minecraft:oak_planks", "minecraft:oak_planks", "minecraft:oak_slab", "minecraft:oak_planks", }, }, { output = "minecraft:hopper", count = 1, grid = { "minecraft:iron_ingot", nil, "minecraft:iron_ingot", "minecraft:iron_ingot", "minecraft:chest", "minecraft:iron_ingot", nil, "minecraft:iron_ingot", nil, }, }, -- Building { output = "minecraft:furnace", count = 1, grid = { "minecraft:cobblestone", "minecraft:cobblestone", "minecraft:cobblestone", "minecraft:cobblestone", nil, "minecraft:cobblestone", "minecraft:cobblestone", "minecraft:cobblestone", "minecraft:cobblestone", }, }, { output = "minecraft:ladder", count = 3, grid = { "minecraft:stick", nil, "minecraft:stick", "minecraft:stick", "minecraft:stick", "minecraft:stick", "minecraft:stick", nil, "minecraft:stick", }, }, { output = "minecraft:glass_pane", count = 16, grid = { "minecraft:glass", "minecraft:glass", "minecraft:glass", "minecraft:glass", "minecraft:glass", "minecraft:glass", nil, nil, nil, }, }, { output = "minecraft:iron_bars", count = 16, grid = { "minecraft:iron_ingot", "minecraft:iron_ingot", "minecraft:iron_ingot", "minecraft:iron_ingot", "minecraft:iron_ingot", "minecraft:iron_ingot", nil, nil, nil, }, }, -- Tools & combat { output = "minecraft:bucket", count = 1, grid = { "minecraft:iron_ingot", nil, "minecraft:iron_ingot", nil, "minecraft:iron_ingot", nil, nil, nil, nil, }, }, { output = "minecraft:arrow", count = 4, grid = { "minecraft:flint", nil, nil, "minecraft:stick", nil, nil, "minecraft:feather", nil, nil, }, }, -- Redstone { output = "minecraft:piston", count = 1, grid = { "minecraft:oak_planks", "minecraft:oak_planks", "minecraft:oak_planks", "minecraft:cobblestone", "minecraft:iron_ingot", "minecraft:cobblestone", "minecraft:cobblestone", "minecraft:redstone", "minecraft:cobblestone", }, }, { output = "minecraft:rail", count = 16, grid = { "minecraft:iron_ingot", nil, "minecraft:iron_ingot", "minecraft:iron_ingot", "minecraft:stick", "minecraft:iron_ingot", "minecraft:iron_ingot", nil, "minecraft:iron_ingot", }, }, { output = "minecraft:powered_rail", count = 6, grid = { "minecraft:gold_ingot", nil, "minecraft:gold_ingot", "minecraft:gold_ingot", "minecraft:stick", "minecraft:gold_ingot", "minecraft:gold_ingot", "minecraft:redstone", "minecraft:gold_ingot", }, }, -- Food & misc { output = "minecraft:bread", count = 1, grid = { "minecraft:wheat", "minecraft:wheat", "minecraft:wheat", nil, nil, nil, nil, nil, nil, }, }, { output = "minecraft:paper", count = 3, grid = { "minecraft:sugar_cane", "minecraft:sugar_cane", "minecraft:sugar_cane", nil, nil, nil, nil, nil, nil, }, }, { output = "minecraft:compass", count = 1, grid = { nil, "minecraft:iron_ingot", nil, "minecraft:iron_ingot", "minecraft:redstone", "minecraft:iron_ingot", nil, "minecraft:iron_ingot", nil, }, }, { output = "minecraft:clock", count = 1, grid = { nil, "minecraft:gold_ingot", nil, "minecraft:gold_ingot", "minecraft:redstone", "minecraft:gold_ingot", nil, "minecraft:gold_ingot", nil, }, }, } ------------------------------------------------- -- 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) ------------------------------------------------- local cache = { catalogue = {}, -- itemName -> { {chest=name, total=N}, ... } itemList = {}, -- sorted list of { name, total } itemListDirty = false, -- lazy rebuild flag for itemList grandTotal = 0, chestCount = 0, totalSlots = 0, usedSlots = 0, freeSlots = 0, usedRatio = 0, dropperOk = false, barrelOk = false, furnaceCount = 0, furnaceStatus = {}, -- per-furnace { name, type, input, fuel, output, active } } ------------------------------------------------- -- Activity state (shown on monitor) ------------------------------------------------- local activity = { sorting = false, -- barrel sort in progress 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 crafting = false, -- crafting in progress } ------------------------------------------------- -- Instant cache adjustment (no scan needed) ------------------------------------------------- --- Adjust cached counts for a single item move. -- delta > 0 means items arrived in chestName; -- delta < 0 means items left chestName. local function adjustCache(itemName, chestName, delta) if delta == 0 then return end -- 1) catalogue: per-chest totals local cat = cache.catalogue if delta > 0 then -- Items added to a chest if not cat[itemName] then cat[itemName] = {} end local found = false for _, entry in ipairs(cat[itemName]) do if entry.chest == chestName then entry.total = entry.total + delta found = true break end end if not found then table.insert(cat[itemName], { chest = chestName, total = delta }) end else -- Items removed from a chest if cat[itemName] then for idx, entry in ipairs(cat[itemName]) do if entry.chest == chestName then entry.total = entry.total + delta -- delta is negative if entry.total <= 0 then table.remove(cat[itemName], idx) end break end end -- Remove item from catalogue entirely if no sources left if #cat[itemName] == 0 then cat[itemName] = nil end end end -- 2) Mark itemList as needing rebuild (deferred until actually read) cache.itemListDirty = true -- 3) Update grandTotal incrementally cache.grandTotal = cache.grandTotal + delta end --- Rebuild itemList from catalogue if dirty (lazy rebuild) local function ensureItemList() if not cache.itemListDirty then return end local itemList = {} local grandTotal = 0 for name, sources in pairs(cache.catalogue) do local total = 0 for _, s in ipairs(sources) do total = total + s.total end grandTotal = grandTotal + total table.insert(itemList, { name = name, total = total }) end table.sort(itemList, function(a, b) return a.total > b.total end) cache.itemList = itemList cache.grandTotal = grandTotal cache.itemListDirty = false end ------------------------------------------------- -- Inventory helpers (cached peripheral lists) ------------------------------------------------- local PERIPHERAL_CACHE_TTL = 5 -- seconds local cachedChests = nil local cachedChestsTime = 0 local cachedFurnaces = nil local cachedFurnacesTime = 0 local function invalidatePeripheralCaches() cachedChests = nil cachedFurnaces = nil end local function getChests() local now = os.clock() if cachedChests and (now - cachedChestsTime) < PERIPHERAL_CACHE_TTL then return cachedChests end local chests = {} for _, name in ipairs(peripheral.getNames()) do if peripheral.getType(name) == "minecraft:chest" then table.insert(chests, name) end end cachedChests = chests cachedChestsTime = now return chests end local function getFurnaces() local now = os.clock() if cachedFurnaces and (now - cachedFurnacesTime) < PERIPHERAL_CACHE_TTL then return cachedFurnaces end local furnaces = {} for _, ftype in ipairs(FURNACE_TYPES) do for _, name in ipairs(peripheral.getNames()) do if peripheral.getType(name) == ftype then table.insert(furnaces, name) end end end cachedFurnaces = furnaces cachedFurnacesTime = now return furnaces end local function refreshFurnaceStatus() local furnaces = getFurnaces() local status = {} for _, fname in ipairs(furnaces) do local furnace = peripheral.wrap(fname) if furnace then local contents = furnace.list() local entry = { name = fname, type = peripheral.getType(fname), input = contents[SLOT_INPUT] or nil, fuel = contents[SLOT_FUEL] or nil, output = contents[SLOT_OUTPUT] or nil, active = (contents[SLOT_INPUT] ~= nil and contents[SLOT_FUEL] ~= nil), } table.insert(status, entry) end end cache.furnaceStatus = status end local function scanInventory(deviceName) local inv = peripheral.wrap(deviceName) if not inv then return {} end local result = {} for slot, item in pairs(inv.list()) do if not result[item.name] then result[item.name] = { total = 0, slots = {} } end result[item.name].total = result[item.name].total + item.count result[item.name].slots[slot] = { name = item.name, count = item.count } end return result end -- Full scan: updates the global cache -- onProgress(current, total, chestName) is called per chest if provided local function refreshCache(onProgress) activity.scanning = true -- Single pass over peripherals: classify into chests and furnaces local chests = {} local furnaces = {} local furnaceTypeSet = {} for _, ft in ipairs(FURNACE_TYPES) do furnaceTypeSet[ft] = true end for _, name in ipairs(peripheral.getNames()) do local ptype = peripheral.getType(name) if ptype == "minecraft:chest" then table.insert(chests, name) elseif furnaceTypeSet[ptype] then table.insert(furnaces, name) end end -- Update peripheral caches so getChests()/getFurnaces() stay fresh local now = os.clock() cachedChests = chests cachedChestsTime = now cachedFurnaces = furnaces cachedFurnacesTime = now local catalogue = {} local totalSlots = 0 local usedSlots = 0 for ci, chest in ipairs(chests) do if onProgress then onProgress(ci, #chests, chest) end local inv = peripheral.wrap(chest) if inv then totalSlots = totalSlots + inv.size() local contents = inv.list() for slot, item in pairs(contents) do usedSlots = usedSlots + 1 if not catalogue[item.name] then catalogue[item.name] = {} end -- Accumulate per-chest totals local found = false for _, entry in ipairs(catalogue[item.name]) do if entry.chest == chest then entry.total = entry.total + item.count found = true break end end if not found then table.insert(catalogue[item.name], { chest = chest, total = item.count }) end end end end -- Build sorted item list local itemList = {} local grandTotal = 0 for itemName, sources in pairs(catalogue) do local total = 0 for _, s in ipairs(sources) do total = total + s.total end grandTotal = grandTotal + total table.insert(itemList, { name = itemName, total = total }) end table.sort(itemList, function(a, b) return a.total > b.total end) -- Update cache atomically cache.catalogue = catalogue cache.itemList = itemList cache.grandTotal = grandTotal cache.chestCount = #chests cache.totalSlots = totalSlots cache.usedSlots = usedSlots cache.freeSlots = totalSlots - usedSlots cache.usedRatio = totalSlots > 0 and (usedSlots / totalSlots) or 0 cache.dropperOk = peripheral.wrap(DROPPER_NAME) ~= nil cache.barrelOk = peripheral.wrap(BARREL_NAME) ~= nil -- Furnace count already computed from single-pass above cache.furnaceCount = #furnaces -- Scan furnace contents for smelter dashboard refreshFurnaceStatus() activity.scanning = false -- Persist cache to disk pcall(function() local data = { catalogue = cache.catalogue, itemList = cache.itemList, grandTotal = cache.grandTotal, chestCount = cache.chestCount, totalSlots = cache.totalSlots, usedSlots = cache.usedSlots, freeSlots = cache.freeSlots, usedRatio = cache.usedRatio, dropperOk = cache.dropperOk, barrelOk = cache.barrelOk, furnaceCount = cache.furnaceCount, furnaceStatus = cache.furnaceStatus, savedAt = os.epoch("utc"), } local f = fs.open(CACHE_FILE, "w") f.write(textutils.serialise(data)) f.close() end) end -- Load cache from disk (returns true if loaded) local function loadCacheFromDisk() if not fs.exists(CACHE_FILE) then return false end local ok, err = pcall(function() local f = fs.open(CACHE_FILE, "r") local raw = f.readAll() f.close() local data = textutils.unserialise(raw) if data and data.catalogue and data.itemList then cache.catalogue = data.catalogue cache.itemList = data.itemList cache.grandTotal = data.grandTotal or 0 cache.chestCount = data.chestCount or 0 cache.totalSlots = data.totalSlots or 0 cache.usedSlots = data.usedSlots or 0 cache.freeSlots = data.freeSlots or 0 cache.usedRatio = data.usedRatio or 0 cache.dropperOk = data.dropperOk or false cache.barrelOk = data.barrelOk or false cache.furnaceCount = data.furnaceCount or 0 cache.furnaceStatus = data.furnaceStatus or {} else error("invalid cache data") end end) if not ok then print("[WARN] Could not load cache: " .. tostring(err)) return false end return true end ------------------------------------------------- -- Monitor setup ------------------------------------------------- local mon = nil local monName = nil local smelterMon = nil local smelterMonName = nil local networkModem = nil local networkModemName = nil local craftTurtleName = nil local function setupMonitor() mon = peripheral.wrap(MONITOR_SIDE) if mon and mon.setTextScale then monName = MONITOR_SIDE else mon = nil end if not mon then -- Search for a monitor on the network (skip smelter side) for _, name in ipairs(peripheral.getNames()) do if peripheral.getType(name) == "monitor" and name ~= SMELTER_MONITOR_SIDE then mon = peripheral.wrap(name) monName = name break end end end if not mon then return false end mon.setTextScale(0.5) mon.clear() return true end local function setupSmelterMonitor() smelterMon = peripheral.wrap(SMELTER_MONITOR_SIDE) if smelterMon and smelterMon.setTextScale then smelterMonName = SMELTER_MONITOR_SIDE else smelterMon = nil end if not smelterMon then -- Search for a second monitor on the network for _, name in ipairs(peripheral.getNames()) do if peripheral.getType(name) == "monitor" and name ~= monName then smelterMon = peripheral.wrap(name) smelterMonName = name break end end end if not smelterMon then return false end smelterMon.setTextScale(0.5) smelterMon.clear() return true end ------------------------------------------------- -- UI State ------------------------------------------------- local selectedAmount = 1 local amountOptions = {1, 4, 8, 16, 32, 64} local statusMessage = "" local statusColor = colors.white local statusTimer = 0 local touchZones = {} local pendingZones = {} local needsRedraw = true local currentPage = 1 local totalPages = 1 local searchQuery = "" local showKeyboard = false -- Keyboard layout local kbRows = { {"Q","W","E","R","T","Y","U","I","O","P"}, {"A","S","D","F","G","H","J","K","L"}, {"Z","X","C","V","B","N","M"}, } ------------------------------------------------- -- Smelter dashboard state ------------------------------------------------- local smelterView = "status" -- "status" or "recipes" local smelterPage = 1 local smelterTotalPages = 1 local smelterTouchZones = {} local smelterPendingZones = {} local smelterNeedsRedraw = true local smeltingPaused = false local disabledRecipes = {} -- { ["minecraft:raw_iron"] = true } local function loadDisabledRecipes() if not fs.exists(DISABLED_RECIPES_FILE) then return end pcall(function() local f = fs.open(DISABLED_RECIPES_FILE, "r") local raw = f.readAll() f.close() local data = textutils.unserialise(raw) if type(data) == "table" then if data.disabled then disabledRecipes = data.disabled end if data.paused ~= nil then smeltingPaused = data.paused end end end) end local function saveDisabledRecipes() pcall(function() local f = fs.open(DISABLED_RECIPES_FILE, "w") f.write(textutils.serialise({ disabled = disabledRecipes, paused = smeltingPaused })) f.close() end) end -- Get items filtered by search query local function getFilteredItems() ensureItemList() local filtered = {} for _, item in ipairs(cache.itemList) do if searchQuery == "" then table.insert(filtered, item) else local lower = item.name:lower():gsub("minecraft:", ""):gsub("_", " ") if lower:find(searchQuery:lower(), 1, true) then table.insert(filtered, item) end end end return filtered end local function addZone(x1, y1, x2, y2, action, data) table.insert(pendingZones, { x1 = x1, y1 = y1, x2 = x2, y2 = y2, action = action, data = data }) end local function hitTest(x, y) for _, zone in ipairs(touchZones) do if x >= zone.x1 and x <= zone.x2 and y >= zone.y1 and y <= zone.y2 then return zone.action, zone.data end end return nil, nil end local function addSmelterZone(x1, y1, x2, y2, action, data) table.insert(smelterPendingZones, { x1 = x1, y1 = y1, x2 = x2, y2 = y2, action = action, data = data }) end local function smelterHitTest(x, y) for _, zone in ipairs(smelterTouchZones) do if x >= zone.x1 and x <= zone.x2 and y >= zone.y1 and y <= zone.y2 then return zone.action, zone.data end end return nil, nil end ------------------------------------------------- -- Drawing helpers (write to draw target) ------------------------------------------------- local draw = nil local function monWrite(x, y, text, fg, bg) draw.setCursorPos(x, y) if fg then draw.setTextColor(fg) end if bg then draw.setBackgroundColor(bg) end draw.write(text) end local function monFill(y, color) local w, _ = draw.getSize() draw.setCursorPos(1, y) draw.setBackgroundColor(color) draw.write(string.rep(" ", w)) end local function monCenter(y, text, fg, bg) local w, _ = draw.getSize() local x = math.floor((w - #text) / 2) + 1 monWrite(x, y, text, fg, bg) end local function monBar(x, y, width, ratio, barColor, bgColor) local filled = math.floor(ratio * width) draw.setCursorPos(x, y) draw.setBackgroundColor(barColor) draw.write(string.rep(" ", filled)) draw.setBackgroundColor(bgColor) draw.write(string.rep(" ", width - filled)) end local function drawButton(x, y, text, fg, bg, padLeft, padRight) padLeft = padLeft or 1 padRight = padRight or 1 local full = string.rep(" ", padLeft) .. text .. string.rep(" ", padRight) monWrite(x, y, full, fg, bg) return x, y, x + #full - 1, y end ------------------------------------------------- -- Dashboard (reads ONLY from cache — no peripheral calls — instant) ------------------------------------------------- local function drawDashboard() if not mon then return end local w, h = mon.getSize() pendingZones = {} -- Offscreen buffer draw = window.create(mon, 1, 1, w, h, false) draw.setBackgroundColor(colors.black) draw.clear() -- ===== Title bar ===== monFill(1, colors.blue) monCenter(1, " ** INVENTORY MANAGER ** ", colors.white, colors.blue) -- ===== Status bar ===== monFill(2, colors.gray) local statusParts = {} table.insert(statusParts, string.format(" Chests: %d", cache.chestCount)) table.insert(statusParts, cache.dropperOk and "Dropper: OK" or "Dropper: --") table.insert(statusParts, cache.barrelOk and "Barrel: OK" or "Barrel: --") if cache.furnaceCount and cache.furnaceCount > 0 then table.insert(statusParts, string.format("Furnaces: %d", cache.furnaceCount)) end -- Activity indicators local actParts = {} if activity.sorting then table.insert(actParts, "SORTING") end 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) if #actParts > 0 then local actStr = " " .. table.concat(actParts, " | ") .. " " monWrite(w - #actStr, 2, actStr, colors.white, colors.orange) end -- ===== Divider ===== monFill(3, colors.lightBlue) monCenter(3, string.rep("-", math.min(w - 4, 60)), colors.cyan, colors.lightBlue) -- ===== Storage capacity ===== monFill(4, colors.black) local capLabel = string.format(" Storage: %d/%d slots (%d free)", cache.usedSlots, cache.totalSlots, cache.freeSlots) monWrite(2, 4, capLabel, colors.lightGray, colors.black) local barStart = #capLabel + 4 local barWidth = w - barStart - 2 if barWidth > 4 then local barColor = colors.lime if cache.usedRatio > 0.9 then barColor = colors.red elseif cache.usedRatio > 0.7 then barColor = colors.orange elseif cache.usedRatio > 0.5 then barColor = colors.yellow end monBar(barStart, 4, barWidth, cache.usedRatio, barColor, colors.gray) local pctStr = string.format(" %d%% ", math.floor(cache.usedRatio * 100)) local pctX = barStart + math.floor(barWidth / 2) - math.floor(#pctStr / 2) monWrite(pctX, 4, pctStr, colors.white, barColor) end -- ===== Amount selector (row 5) ===== monFill(5, colors.black) monWrite(2, 5, "Qty:", colors.lightGray, colors.black) local btnX = 7 for _, amt in ipairs(amountOptions) do local label = tostring(amt) local bg = (amt == selectedAmount) and colors.cyan or colors.gray local fg = (amt == selectedAmount) and colors.white or colors.lightGray local x1, y1, x2, y2 = drawButton(btnX, 5, label, fg, bg) addZone(x1, y1, x2, y2, "amount", amt) btnX = x2 + 2 end -- Refresh button local refreshBg = activity.scanning and colors.yellow or colors.green local refreshFg = activity.scanning and colors.black or colors.white local refreshTxt = activity.scanning and "Scanning" or "Refresh" local scanX = w - #refreshTxt - 3 local sx1, sy1, sx2, sy2 = drawButton(scanX, 5, refreshTxt, refreshFg, refreshBg, 1, 1) addZone(sx1, sy1, sx2, sy2, "scan", nil) -- ===== Search bar + Pagination (row 6) ===== monFill(6, colors.black) -- Keyboard toggle button local kbLabel = showKeyboard and " X " or " ? " local kbBg = showKeyboard and colors.red or colors.purple monWrite(2, 6, kbLabel, colors.white, kbBg) addZone(2, 6, 4, 6, "kb_toggle", nil) -- Search query display local queryDisplay = searchQuery if showKeyboard then queryDisplay = queryDisplay .. "|" elseif queryDisplay == "" then queryDisplay = "search..." end local fieldW = math.floor(w * 0.4) if fieldW < 10 then fieldW = 10 end local displayText = queryDisplay:sub(1, fieldW) displayText = displayText .. string.rep("_", math.max(0, fieldW - #displayText)) monWrite(6, 6, displayText, (searchQuery == "" and not showKeyboard) and colors.gray or colors.white, colors.black) addZone(6, 6, 5 + fieldW, 6, "kb_toggle", nil) -- Filter items local filteredItems = getFilteredItems() -- Pagination local maxRows = h - 10 if maxRows < 1 then maxRows = 1 end totalPages = math.max(1, math.ceil(#filteredItems / maxRows)) if currentPage > totalPages then currentPage = totalPages end if currentPage < 1 then currentPage = 1 end -- Page controls (right side of row 6) local pageStr = string.format("Pg %d/%d", currentPage, totalPages) local navW = 3 + 1 + #pageStr + 1 + 3 local navX = w - navW if currentPage > 1 then monWrite(navX, 6, " < ", colors.white, colors.gray) addZone(navX, 6, navX + 2, 6, "page_prev", nil) else monWrite(navX, 6, " < ", colors.lightGray, colors.black) end monWrite(navX + 4, 6, pageStr, colors.lightGray, colors.black) local nextX = navX + 4 + #pageStr + 1 if currentPage < totalPages then monWrite(nextX, 6, " > ", colors.white, colors.gray) addZone(nextX, 6, nextX + 2, 6, "page_next", nil) else monWrite(nextX, 6, " > ", colors.lightGray, colors.black) end -- ===== Column headers (row 7) ===== local row = 7 monFill(row, colors.gray) monWrite(2, row, "#", colors.lightGray, colors.gray) monWrite(5, row, "Item", colors.lightGray, colors.gray) monWrite(w - 22, row, "Qty", colors.lightGray, colors.gray) monWrite(w - 14, row, "Stock", colors.lightGray, colors.gray) monWrite(w - 1, row, ">", colors.lightGray, colors.gray) row = row + 1 -- ===== Item rows (paginated + filtered) ===== local maxCount = 0 for _, item in ipairs(filteredItems) do if item.total > maxCount then maxCount = item.total end end if maxCount == 0 then maxCount = 1 end local startIdx = (currentPage - 1) * maxRows + 1 local endIdx = math.min(startIdx + maxRows - 1, #filteredItems) if #filteredItems == 0 then monFill(8, colors.black) monFill(9, colors.black) if searchQuery ~= "" then monCenter(9, "No items match \"" .. searchQuery .. "\"", colors.gray, colors.black) else monCenter(9, "No items in storage", colors.gray, colors.black) end row = 10 else for i = startIdx, endIdx do local item = filteredItems[i] local y = row local short = item.name:gsub("^minecraft:", ""):gsub("_", " ") short = short:sub(1,1):upper() .. short:sub(2) local maxNameLen = w - 30 if #short > maxNameLen then short = short:sub(1, maxNameLen - 2) .. ".." end local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray monFill(y, rowBg) monWrite(2, y, string.format("%2d", i), colors.lightBlue, rowBg) monWrite(5, y, short, colors.white, rowBg) monWrite(w - 22, y, tostring(item.total), colors.yellow, rowBg) local ratio = item.total / maxCount local barColor = colors.lime if ratio < 0.25 then barColor = colors.red elseif ratio < 0.5 then barColor = colors.orange end monBar(w - 14, y, 12, ratio, barColor, rowBg == colors.gray and colors.lightGray or colors.gray) monWrite(w - 1, y, ">", colors.orange, rowBg) addZone(1, y, w, y, "order", item.name) row = row + 1 end end -- Fill remaining empty item rows local lastItemRow = h - 3 while row <= lastItemRow do monFill(row, colors.black) row = row + 1 end if showKeyboard then -- ===== Keyboard overlay (bottom 3 rows: h-2, h-1, h) ===== local keyW = 3 local keyGap = 1 local kbDefs = { { keys = kbRows[1], specials = {{ label = " Bksp ", action = "kb_bksp", bg = colors.red }} }, { keys = kbRows[2], specials = {{ label = " Done ", action = "kb_done", bg = colors.green }} }, { keys = kbRows[3], specials = { { label = " Space ", action = "kb_space", bg = colors.lightGray }, { label = " Clr ", action = "kb_clear", bg = colors.orange }, }}, } for rowIdx, def in ipairs(kbDefs) do local y = h - 3 + rowIdx monFill(y, colors.black) -- Calculate total row width for centering local keysW = #def.keys * keyW + math.max(0, #def.keys - 1) * keyGap local specialsW = 0 for _, sp in ipairs(def.specials) do specialsW = specialsW + keyGap + #sp.label end local rowW = keysW + specialsW local x = math.floor((w - rowW) / 2) + 1 -- Draw letter keys for ki, key in ipairs(def.keys) do monWrite(x, y, " " .. key .. " ", colors.white, colors.gray) addZone(x, y, x + keyW - 1, y, "kb_key", key:lower()) x = x + keyW if ki < #def.keys then x = x + keyGap end end -- Draw special keys for _, sp in ipairs(def.specials) do x = x + keyGap monWrite(x, y, sp.label, colors.white, sp.bg) addZone(x, y, x + #sp.label - 1, y, sp.action, nil) x = x + #sp.label end end else -- ===== Status message (h-2) ===== monFill(h - 2, colors.black) 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 -- ===== Footer (h-1) ===== ensureItemList() monFill(h - 1, colors.gray) local footerLeft = string.format(" Total: %d items | %d types ", cache.grandTotal, #cache.itemList) monWrite(2, h - 1, footerLeft, colors.white, colors.gray) if searchQuery ~= "" then local filterNote = string.format("| Showing %d ", #filteredItems) monWrite(2 + #footerLeft + 1, h - 1, filterNote, colors.yellow, colors.gray) end local timeStr = textutils.formatTime(os.time(), true) monWrite(w - #timeStr - 1, h - 1, timeStr, colors.lightGray, colors.gray) -- ===== Bottom accent (h) ===== monFill(h, colors.blue) local bottomMsg = " Tap item to order " if activity.dispensing then bottomMsg = " DISPENSING... " elseif activity.smelting then 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 -- Flush to monitor draw.setVisible(true) -- Swap zones touchZones = pendingZones end ------------------------------------------------- -- Crafting helpers ------------------------------------------------- --- Get ingredient counts from a recipe's grid local function getRecipeIngredients(recipe) local ingredients = {} for _, item in ipairs(recipe.grid) do if item then ingredients[item] = (ingredients[item] or 0) + 1 end end return ingredients end --- Check if a recipe can be crafted with current stock local function canCraftRecipe(recipe) local ingredients = getRecipeIngredients(recipe) for itemName, needed in pairs(ingredients) do local have = 0 if cache.catalogue[itemName] then for _, src in ipairs(cache.catalogue[itemName]) do have = have + src.total end end if have < needed then return false end end return true end --- How many times a recipe can be crafted local function maxCraftBatches(recipe) local ingredients = getRecipeIngredients(recipe) local minBatches = math.huge for itemName, needed in pairs(ingredients) do local have = 0 if cache.catalogue[itemName] then for _, src in ipairs(cache.catalogue[itemName]) do have = have + src.total end end local batches = math.floor(have / needed) if batches < minBatches then minBatches = batches end end if minBatches == math.huge then return 0 end return minBatches end --- Get list of missing ingredients for a recipe local function getMissingIngredients(recipe) local ingredients = getRecipeIngredients(recipe) local missing = {} for itemName, needed in pairs(ingredients) do local have = 0 if cache.catalogue[itemName] then for _, src in ipairs(cache.catalogue[itemName]) do have = have + src.total end end if have < needed then table.insert(missing, { name = itemName, have = have, need = needed, }) end end return missing end --- Execute a craft via the networked turtle -- Uses chest-side pullItems/pushItems to move items to/from the turtle, -- since remote turtles may not expose the inventory API (list/pushItems). local function craftItem(recipeIdx) local recipe = CRAFTABLE[recipeIdx] if not recipe then print("[CRAFT] Invalid recipe index: " .. tostring(recipeIdx)) return false, "Invalid recipe" end if not craftTurtleName then print("[CRAFT] No turtle detected on network") return false, "No turtle" end -- Verify the turtle is still on the network if not peripheral.isPresent(craftTurtleName) then print("[CRAFT] Turtle offline: " .. craftTurtleName) return false, "Turtle offline" end print(string.format("[CRAFT] Starting craft: %s (turtle: %s)", recipe.output, craftTurtleName)) activity.crafting = true needsRedraw = true smelterNeedsRedraw = true local chests = getChests() -- Helper: pull all 16 turtle slots into chests (chest-side) local function clearTurtle(knownItems) for slot = 1, 16 do for _, ch in ipairs(chests) do local chest = peripheral.wrap(ch) if chest then local n = chest.pullItems(craftTurtleName, slot) if n and n > 0 then if knownItems and knownItems[slot] then adjustCache(knownItems[slot], ch, n) end break end end end end end -- 1. Clear turtle inventory (safety – turtle should be empty) clearTurtle(nil) -- 2. Push ingredients into correct grid slots, track what we placed local placedItems = {} -- turtleSlot -> itemName for gridPos = 1, 9 do local itemName = recipe.grid[gridPos] if itemName then local turtleSlot = GRID_TO_SLOT[gridPos] local placed = false if cache.catalogue[itemName] then for _, source in ipairs(cache.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 print(string.format("[CRAFT] Pushing %s from %s slot %d -> turtle slot %d", itemName, source.chest, slot, turtleSlot)) local ok, n = pcall(chest.pushItems, craftTurtleName, slot, 1, turtleSlot) if ok and n and n > 0 then adjustCache(itemName, source.chest, -n) placedItems[turtleSlot] = itemName placed = true print(string.format("[CRAFT] Placed %s in turtle slot %d", itemName, turtleSlot)) break elseif not ok then print(string.format("[CRAFT] pushItems error: %s", tostring(n))) else print(string.format("[CRAFT] pushItems returned %s", tostring(n))) end end end end if placed then break end end else print(string.format("[CRAFT] Item %s not in catalogue!", itemName)) end if not placed then -- Cleanup: pull placed items back using tracked names print("[CRAFT] Missing ingredient, aborting") clearTurtle(placedItems) activity.crafting = false needsRedraw = true smelterNeedsRedraw = true return false, "Missing ingredient" end end end -- 3. Wait for turtle to auto-craft -- The turtle polls its crafting slots and crafts automatically. -- Give it time: SETTLE_DELAY (1s) + craft time + margin. print(string.format("[CRAFT] Ingredients placed for %s, waiting for turtle...", recipe.output)) sleep(3) -- 4. Pull all items from turtle back to chests -- Strategy: first check crafting grid slots. If they're empty, -- the turtle consumed the ingredients = craft succeeded. -- Then pull everything remaining (output items or leftover ingredients). local ingredientsRemain = false -- Check crafting grid slots first for gridPos = 1, 9 do if placedItems[GRID_TO_SLOT[gridPos]] then for _, ch in ipairs(chests) do local chest = peripheral.wrap(ch) if chest then local n = chest.pullItems(craftTurtleName, GRID_TO_SLOT[gridPos]) if n and n > 0 then -- Ingredient still there = craft failed for this slot ingredientsRemain = true local itemName = placedItems[GRID_TO_SLOT[gridPos]] adjustCache(itemName, ch, n) print(string.format("[CRAFT] Ingredient returned: %s x%d from slot %d", itemName, n, GRID_TO_SLOT[gridPos])) break end end end end end local success = not ingredientsRemain local pulledOutput = 0 -- Pull everything remaining (output on success, or stray items) for slot = 1, 16 do for _, ch in ipairs(chests) do local chest = peripheral.wrap(ch) if chest then local n = chest.pullItems(craftTurtleName, slot) if n and n > 0 then -- On success these are crafted output; on failure, leftovers local itemName = success and recipe.output or (placedItems[slot] or recipe.output) adjustCache(itemName, ch, n) if success then pulledOutput = pulledOutput + n end print(string.format("[CRAFT] Pulled %s x%d from slot %d -> %s", itemName, n, slot, ch)) break end end end end activity.crafting = false needsRedraw = true smelterNeedsRedraw = true if success then local short = recipe.output:gsub("^minecraft:", ""):gsub("_", " ") print(string.format("[CRAFT] OK: %s x%d", short, pulledOutput)) return true else print("[CRAFT] Failed: ingredients were not consumed by turtle") return false, "Craft failed" end end ------------------------------------------------- -- Smelter Dashboard ------------------------------------------------- local function drawSmelterDashboard() if not smelterMon then return end local w, h = smelterMon.getSize() smelterPendingZones = {} -- Offscreen buffer draw = window.create(smelterMon, 1, 1, w, h, false) draw.setBackgroundColor(colors.black) draw.clear() -- ===== Title bar ===== monFill(1, colors.purple) monCenter(1, " ** SMELTER DASHBOARD ** ", colors.white, colors.purple) -- ===== Status bar ===== monFill(2, colors.gray) local activeCount = 0 for _, fs in ipairs(cache.furnaceStatus or {}) do if fs.active then activeCount = activeCount + 1 end end local statusStr = string.format(" Furnaces: %d Active: %d", cache.furnaceCount or 0, activeCount) monWrite(2, 2, statusStr, colors.white, colors.gray) -- Pause/Resume button local pauseLabel = smeltingPaused and " PAUSED " or " ACTIVE " local pauseBg = smeltingPaused and colors.red or colors.lime local pauseFg = smeltingPaused and colors.white or colors.black monWrite(w - #pauseLabel, 2, pauseLabel, pauseFg, pauseBg) addSmelterZone(w - #pauseLabel, 2, w - 1, 2, "toggle_pause", nil) -- ===== Divider ===== monFill(3, colors.magenta) monCenter(3, string.rep("-", math.min(w - 4, 60)), colors.pink, colors.magenta) -- ===== Tab row ===== monFill(4, colors.black) local tabStatusBg = smelterView == "status" and colors.purple or colors.gray local tabSmeltBg = smelterView == "smelt" and colors.purple or colors.gray local tabCraftBg = smelterView == "craft" and colors.purple or colors.gray local tabMissingBg = smelterView == "missing" and colors.purple or colors.gray local bx1, by1, bx2, by2 bx1, by1, bx2, by2 = drawButton(2, 4, "Status", colors.white, tabStatusBg) addSmelterZone(bx1, by1, bx2, by2, "tab", "status") bx1, by1, bx2, by2 = drawButton(bx2 + 2, 4, "Smelt", colors.white, tabSmeltBg) addSmelterZone(bx1, by1, bx2, by2, "tab", "smelt") bx1, by1, bx2, by2 = drawButton(bx2 + 2, 4, "Craft", colors.white, tabCraftBg) addSmelterZone(bx1, by1, bx2, by2, "tab", "craft") bx1, by1, bx2, by2 = drawButton(bx2 + 2, 4, "Missing", colors.white, tabMissingBg) addSmelterZone(bx1, by1, bx2, by2, "tab", "missing") -- Hoisted counter: reused by bottom accent bar to avoid re-scanning recipes local craftAvailCount = nil if smelterView == "status" then -- ===== Furnace Status View ===== -- Column headers monFill(5, colors.gray) local outCol = math.floor(w * 0.40) local fuelCol = math.floor(w * 0.65) local statCol = w - 6 monWrite(2, 5, "#", colors.lightGray, colors.gray) monWrite(4, 5, "T", colors.lightGray, colors.gray) monWrite(6, 5, "Input", colors.lightGray, colors.gray) monWrite(outCol, 5, "Output", colors.lightGray, colors.gray) monWrite(fuelCol, 5, "Fuel", colors.lightGray, colors.gray) monWrite(statCol, 5, "State", colors.lightGray, colors.gray) -- Furnace rows local furnaceList = cache.furnaceStatus or {} local maxRows = h - 8 if maxRows < 1 then maxRows = 1 end smelterTotalPages = math.max(1, math.ceil(#furnaceList / maxRows)) if smelterPage > smelterTotalPages then smelterPage = smelterTotalPages end if smelterPage < 1 then smelterPage = 1 end local startIdx = (smelterPage - 1) * maxRows + 1 local endIdx = math.min(startIdx + maxRows - 1, #furnaceList) local row = 6 if #furnaceList == 0 then monFill(7, colors.black) monCenter(7, "No furnaces found on network", colors.gray, colors.black) row = 8 else for i = startIdx, endIdx do local fs = furnaceList[i] local y = row local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray monFill(y, rowBg) -- Number monWrite(2, y, string.format("%d", i), colors.lightBlue, rowBg) -- Type abbreviation local typeAbbr = "F" local typeColor = colors.orange if fs.type == "minecraft:smoker" then typeAbbr = "S" typeColor = colors.green elseif fs.type == "minecraft:blast_furnace" then typeAbbr = "B" typeColor = colors.cyan end monWrite(4, y, typeAbbr, typeColor, rowBg) -- Input if fs.input then local inName = fs.input.name:gsub("^minecraft:", ""):gsub("_", " ") local maxIn = outCol - 8 if #inName > maxIn then inName = inName:sub(1, maxIn - 2) .. ".." end monWrite(6, y, inName, colors.white, rowBg) monWrite(outCol - 4, y, "x" .. fs.input.count, colors.yellow, rowBg) else monWrite(6, y, "(empty)", colors.lightGray, rowBg) end -- Output if fs.output then local outName = fs.output.name:gsub("^minecraft:", ""):gsub("_", " ") local maxOut = fuelCol - outCol - 5 if #outName > maxOut then outName = outName:sub(1, maxOut - 2) .. ".." end monWrite(outCol, y, outName, colors.white, rowBg) monWrite(fuelCol - 4, y, "x" .. fs.output.count, colors.yellow, rowBg) else monWrite(outCol, y, "-", colors.lightGray, rowBg) end -- Fuel if fs.fuel then local fuelName = fs.fuel.name:gsub("^minecraft:", ""):gsub("_", " ") local maxFuel = statCol - fuelCol - 4 if #fuelName > maxFuel then fuelName = fuelName:sub(1, maxFuel - 2) .. ".." end monWrite(fuelCol, y, fuelName, colors.white, rowBg) monWrite(statCol - 4, y, "x" .. fs.fuel.count, colors.yellow, rowBg) else monWrite(fuelCol, y, "-", colors.lightGray, rowBg) end -- Status indicator if smeltingPaused then monWrite(statCol, y, "PAUSE", colors.red, rowBg) elseif fs.active then monWrite(statCol, y, " COOK", colors.lime, rowBg) elseif fs.input and not fs.fuel then monWrite(statCol, y, "FUEL?", colors.orange, rowBg) else monWrite(statCol, y, " IDLE", colors.lightGray, rowBg) end row = row + 1 end end -- Fill remaining rows while row <= h - 2 do monFill(row, colors.black) row = row + 1 end elseif smelterView == "smelt" then -- ===== Smelt Recipe Manager View ===== -- Build sorted recipe list local recipeList = {} for inputName, recipe in pairs(SMELTABLE) do local short = inputName:gsub("^minecraft:", ""):gsub("_", " ") short = short:sub(1,1):upper() .. short:sub(2) local resultShort = recipe.result:gsub("^minecraft:", ""):gsub("_", " ") resultShort = resultShort:sub(1,1):upper() .. resultShort:sub(2) local types = "" if recipe.furnaceSet["minecraft:furnace"] then types = types .. "F" end if recipe.furnaceSet["minecraft:smoker"] then types = types .. "S" end if recipe.furnaceSet["minecraft:blast_furnace"] then types = types .. "B" end local enabled = not disabledRecipes[inputName] local inStorage = 0 if cache.catalogue[inputName] then for _, s in ipairs(cache.catalogue[inputName]) do inStorage = inStorage + s.total end end table.insert(recipeList, { inputName = inputName, inputShort = short, resultShort = resultShort, types = types, enabled = enabled, inStorage = inStorage, }) end table.sort(recipeList, function(a, b) return a.inputShort < b.inputShort end) -- Column positions local arrowCol = math.floor(w * 0.30) local typeCol = math.floor(w * 0.60) local stockCol = math.floor(w * 0.72) local toggleCol = w - 5 -- Column headers monFill(5, colors.gray) monWrite(2, 5, "Input", colors.lightGray, colors.gray) monWrite(arrowCol, 5, "Output", colors.lightGray, colors.gray) monWrite(typeCol, 5, "Type", colors.lightGray, colors.gray) monWrite(stockCol, 5, "Stock", colors.lightGray, colors.gray) monWrite(toggleCol, 5, "On?", colors.lightGray, colors.gray) -- Bulk action buttons on tab row local bulkX = w - 22 bx1, by1, bx2, by2 = drawButton(bulkX, 4, "All On", colors.white, colors.green) addSmelterZone(bx1, by1, bx2, by2, "enable_all", nil) bx1, by1, bx2, by2 = drawButton(bx2 + 2, 4, "All Off", colors.white, colors.red) addSmelterZone(bx1, by1, bx2, by2, "disable_all", nil) -- Recipe rows local maxRows = h - 8 if maxRows < 1 then maxRows = 1 end smelterTotalPages = math.max(1, math.ceil(#recipeList / maxRows)) if smelterPage > smelterTotalPages then smelterPage = smelterTotalPages end if smelterPage < 1 then smelterPage = 1 end local startIdx = (smelterPage - 1) * maxRows + 1 local endIdx = math.min(startIdx + maxRows - 1, #recipeList) local row = 6 for i = startIdx, endIdx do local r = recipeList[i] local y = row local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray monFill(y, rowBg) -- Input name local maxInputLen = arrowCol - 3 local inputDisplay = r.inputShort if #inputDisplay > maxInputLen then inputDisplay = inputDisplay:sub(1, maxInputLen - 2) .. ".." end monWrite(2, y, inputDisplay, colors.white, rowBg) -- Output local maxOutLen = typeCol - arrowCol - 2 local outDisplay = r.resultShort if #outDisplay > maxOutLen then outDisplay = outDisplay:sub(1, maxOutLen - 2) .. ".." end monWrite(arrowCol, y, outDisplay, colors.lightBlue, rowBg) -- Types monWrite(typeCol, y, r.types, colors.orange, rowBg) -- Stock monWrite(stockCol, y, tostring(r.inStorage), colors.yellow, rowBg) -- Toggle button if r.enabled then monWrite(toggleCol, y, " ON ", colors.white, colors.green) else monWrite(toggleCol, y, " OFF", colors.white, colors.red) end addSmelterZone(1, y, w, y, "toggle_recipe", r.inputName) row = row + 1 end -- Fill remaining rows while row <= h - 2 do monFill(row, colors.black) row = row + 1 end elseif smelterView == "craft" then -- ===== Available Crafting Recipes ===== -- Turtle status on tab row local turtleOk = craftTurtleName and peripheral.wrap(craftTurtleName) ~= nil local tLabel = turtleOk and " Turtle OK " or " No Turtle " local tBg = turtleOk and colors.lime or colors.red local tFg = turtleOk and colors.black or colors.white monWrite(w - #tLabel, 4, tLabel, tFg, tBg) -- Build list of craftable recipes local availList = {} for idx, recipe in ipairs(CRAFTABLE) do if canCraftRecipe(recipe) then local short = recipe.output:gsub("^minecraft:", ""):gsub("_", " ") short = short:sub(1,1):upper() .. short:sub(2) local batches = maxCraftBatches(recipe) table.insert(availList, { idx = idx, short = short, count = recipe.count, batches = batches, }) end end craftAvailCount = #availList -- Column headers monFill(5, colors.gray) local makeCol = w - 6 monWrite(2, 5, "#", colors.lightGray, colors.gray) monWrite(4, 5, "Output", colors.lightGray, colors.gray) monWrite(math.floor(w * 0.45), 5, "Yield", colors.lightGray, colors.gray) monWrite(math.floor(w * 0.60), 5, "Can Make", colors.lightGray, colors.gray) monWrite(makeCol, 5, "Go", colors.lightGray, colors.gray) local maxRows = h - 8 if maxRows < 1 then maxRows = 1 end smelterTotalPages = math.max(1, math.ceil(#availList / maxRows)) if smelterPage > smelterTotalPages then smelterPage = smelterTotalPages end if smelterPage < 1 then smelterPage = 1 end local startIdx = (smelterPage - 1) * maxRows + 1 local endIdx = math.min(startIdx + maxRows - 1, #availList) local row = 6 if #availList == 0 then monFill(7, colors.black) monCenter(7, "No recipes available to craft", colors.gray, colors.black) row = 8 else for i = startIdx, endIdx do local r = availList[i] local y = row local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray monFill(y, rowBg) monWrite(2, y, string.format("%2d", i), colors.lightBlue, rowBg) local maxNameLen = math.floor(w * 0.40) local nameDisplay = r.short if #nameDisplay > maxNameLen then nameDisplay = nameDisplay:sub(1, maxNameLen - 2) .. ".." end monWrite(4, y, nameDisplay, colors.white, rowBg) monWrite(math.floor(w * 0.45), y, "x" .. r.count, colors.yellow, rowBg) monWrite(math.floor(w * 0.60), y, string.format("x%d", r.batches), colors.lime, rowBg) -- MAKE button if turtleOk then monWrite(makeCol, y, " MAKE ", colors.white, colors.green) addSmelterZone(makeCol, y, makeCol + 5, y, "craft", r.idx) else monWrite(makeCol, y, " ---- ", colors.gray, colors.black) end row = row + 1 end end while row <= h - 2 do monFill(row, colors.black) row = row + 1 end elseif smelterView == "missing" then -- ===== Unavailable Crafting Recipes ===== -- Build list of recipes that CANNOT be crafted local missList = {} for idx, recipe in ipairs(CRAFTABLE) do if not canCraftRecipe(recipe) then local short = recipe.output:gsub("^minecraft:", ""):gsub("_", " ") short = short:sub(1,1):upper() .. short:sub(2) local missing = getMissingIngredients(recipe) -- Build summary string local parts = {} for _, m in ipairs(missing) do local mShort = m.name:gsub("^minecraft:", ""):gsub("_", " ") table.insert(parts, string.format("%s %d/%d", mShort, m.have, m.need)) end table.insert(missList, { idx = idx, short = short, count = recipe.count, summary = table.concat(parts, ", "), }) end end craftAvailCount = #CRAFTABLE - #missList -- Column headers monFill(5, colors.gray) monWrite(2, 5, "#", colors.lightGray, colors.gray) monWrite(4, 5, "Output", colors.lightGray, colors.gray) monWrite(math.floor(w * 0.35), 5, "Missing (have/need)", colors.lightGray, colors.gray) local maxRows = h - 8 if maxRows < 1 then maxRows = 1 end smelterTotalPages = math.max(1, math.ceil(#missList / maxRows)) if smelterPage > smelterTotalPages then smelterPage = smelterTotalPages end if smelterPage < 1 then smelterPage = 1 end local startIdx = (smelterPage - 1) * maxRows + 1 local endIdx = math.min(startIdx + maxRows - 1, #missList) local row = 6 if #missList == 0 then monFill(7, colors.black) monCenter(7, "All recipes can be crafted!", colors.lime, colors.black) row = 8 else for i = startIdx, endIdx do local r = missList[i] local y = row local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray monFill(y, rowBg) monWrite(2, y, string.format("%2d", i), colors.lightBlue, rowBg) local nameCol = math.floor(w * 0.35) - 5 local nameDisplay = r.short .. " x" .. r.count if #nameDisplay > nameCol then nameDisplay = nameDisplay:sub(1, nameCol - 2) .. ".." end monWrite(4, y, nameDisplay, colors.white, rowBg) -- Missing items summary local missCol = math.floor(w * 0.35) local missW = w - missCol - 1 local summaryDisplay = r.summary if #summaryDisplay > missW then summaryDisplay = summaryDisplay:sub(1, missW - 2) .. ".." end monWrite(missCol, y, summaryDisplay, colors.red, rowBg) row = row + 1 end end while row <= h - 2 do monFill(row, colors.black) row = row + 1 end end -- ===== Pagination (h - 1) ===== monFill(h - 1, colors.gray) local pageStr = string.format("Pg %d/%d", smelterPage, smelterTotalPages) monCenter(h - 1, pageStr, colors.white, colors.gray) if smelterPage > 1 then monWrite(2, h - 1, " < ", colors.white, colors.lightGray) addSmelterZone(2, h - 1, 4, h - 1, "page_prev", nil) end if smelterPage < smelterTotalPages then monWrite(w - 3, h - 1, " > ", colors.white, colors.lightGray) addSmelterZone(w - 3, h - 1, w - 1, h - 1, "page_next", nil) end -- ===== Bottom accent ===== monFill(h, colors.purple) local bottomMsg = "" if smelterView == "status" or smelterView == "smelt" then local enabledCount = 0 local totalRecipes = 0 for _ in pairs(SMELTABLE) do totalRecipes = totalRecipes + 1 end for inputName in pairs(SMELTABLE) do if not disabledRecipes[inputName] then enabledCount = enabledCount + 1 end end bottomMsg = string.format(" Smelt: %d/%d enabled ", enabledCount, totalRecipes) if activity.smelting then bottomMsg = " SMELTING... " end elseif smelterView == "craft" then bottomMsg = " Tap MAKE to craft " if activity.crafting then bottomMsg = " CRAFTING... " end elseif smelterView == "missing" then local availC = craftAvailCount or 0 bottomMsg = string.format(" Available: %d/%d recipes ", availC, #CRAFTABLE) end monCenter(h, bottomMsg, colors.pink, colors.purple) -- Flush to monitor draw.setVisible(true) -- Swap zones smelterTouchZones = smelterPendingZones end ------------------------------------------------- -- Barrel auto-sort ------------------------------------------------- local function sortBarrel(barrelOverride) local barrelTarget = (barrelOverride and barrelOverride ~= "") and barrelOverride or BARREL_NAME local barrel = peripheral.wrap(barrelTarget) if not barrel then return end local contents = barrel.list() if not contents or not next(contents) then return end activity.sorting = true needsRedraw = true local catalogue = cache.catalogue local chests = getChests() for slot, item in pairs(contents) do local moved = 0 local triedChests = {} -- dedup: skip chests already tried via catalogue if catalogue[item.name] then for _, entry in ipairs(catalogue[item.name]) do triedChests[entry.chest] = true local n = barrel.pushItems(entry.chest, slot) if n and n > 0 then moved = moved + n adjustCache(item.name, entry.chest, n) needsRedraw = true smelterNeedsRedraw = true print(string.format("[SORT] %s x%d -> %s", item.name, n, entry.chest)) end if moved >= item.count then break end end end if moved < item.count then for _, chest in ipairs(chests) do if not triedChests[chest] then local n = barrel.pushItems(chest, slot) if n and n > 0 then moved = moved + n adjustCache(item.name, chest, n) needsRedraw = true smelterNeedsRedraw = true print(string.format("[SORT] %s x%d -> %s", item.name, n, chest)) end if moved >= item.count then break end end end end if moved < item.count then print(string.format("[WARN] Could not sort %d remaining %s", item.count - moved, item.name)) end end activity.sorting = false needsRedraw = true end ------------------------------------------------- -- Auto-smelt ------------------------------------------------- local function autoSmelt() if smeltingPaused then return false end local furnaces = getFurnaces() if #furnaces == 0 then return end local chests = getChests() local catalogue = cache.catalogue local didWork = false local emptyInputFurnaces = {} -- collected during steps 1-3, filled in step 5 for _, fname in ipairs(furnaces) do local furnace = peripheral.wrap(fname) if furnace then local contents = furnace.list() -- 1) Pull finished output (slot 3) back to chests if contents[SLOT_OUTPUT] then local outputItem = contents[SLOT_OUTPUT] local remaining = outputItem.count for _, chest in ipairs(chests) do local n = furnace.pushItems(chest, SLOT_OUTPUT) if n and n > 0 then adjustCache(outputItem.name, chest, n) remaining = remaining - n print(string.format("[SMELT] Output %s x%d -> %s", outputItem.name, n, chest)) didWork = true if remaining <= 0 then break end end end if remaining > 0 then print(string.format("[WARN] Could not move %d %s from %s output (chests full?)", remaining, outputItem.name, fname)) end end -- Also check all slots in case output ended up elsewhere -- Some modded furnaces or CC versions may use different slot indices for slot, item in pairs(contents) do if slot ~= SLOT_INPUT and slot ~= SLOT_FUEL and slot ~= SLOT_OUTPUT then for _, chest in ipairs(chests) do local n = furnace.pushItems(chest, slot) if n and n > 0 then adjustCache(item.name, chest, n) print(string.format("[SMELT] Extra slot %d: %s x%d -> %s", slot, item.name, n, chest)) didWork = true break end end end end -- Re-read after output pull contents = furnace.list() -- 2) Check for incompatible items in input slot and remove them local furnaceType = peripheral.getType(fname) local inputItem = contents[SLOT_INPUT] if inputItem then local recipe = SMELTABLE[inputItem.name] local validHere = recipe and recipe.furnaceSet[furnaceType] or false if not validHere then -- This item doesn't belong in this furnace type — pull it out for _, chest in ipairs(chests) do local n = furnace.pushItems(chest, SLOT_INPUT) if n and n > 0 then adjustCache(inputItem.name, chest, n) print(string.format("[SMELT] Removed incompatible %s x%d from %s -> %s", inputItem.name, n, fname, chest)) didWork = true break end end -- Re-read after removal contents = furnace.list() end end -- 3) Refuel if fuel slot is empty or low local fuelItem = contents[SLOT_FUEL] local needFuel = not fuelItem or fuelItem.count < 8 if needFuel then for _, fuel in ipairs(FUEL_LIST) do if catalogue[fuel.name] then for _, source in ipairs(catalogue[fuel.name]) do local chest = peripheral.wrap(source.chest) if chest then for slot, slotItem in pairs(chest.list()) do if slotItem.name == fuel.name then local toMove = math.min(16, slotItem.count) local n = chest.pushItems(fname, slot, toMove, SLOT_FUEL) if n and n > 0 then adjustCache(fuel.name, source.chest, -n) print(string.format("[SMELT] Fuel %s x%d -> %s", fuel.name, n, fname)) didWork = true needFuel = false break end end end end if not needFuel then break end end end if not needFuel then break end end end -- 4) Collect furnaces with empty input for balanced loading below inputItem = contents[SLOT_INPUT] if not inputItem then table.insert(emptyInputFurnaces, { name = fname, type = furnaceType }) end end end -- 5) Balanced distribution of smeltable items across all empty furnaces -- Instead of greedily filling one furnace at a time, we spread items -- evenly so all compatible furnaces work in parallel. if #emptyInputFurnaces > 0 then -- Build a unified, deduplicated candidate list across all empty furnace types local typesSeen = {} for _, ef in ipairs(emptyInputFurnaces) do typesSeen[ef.type] = true end local allCandidates = {} local candSeen = {} for ftype in pairs(typesSeen) do for _, cand in ipairs(smeltCandidatesByType[ftype] or {}) do if not candSeen[cand.name] then candSeen[cand.name] = true table.insert(allCandidates, cand) end end end -- Sort: food first, then alphabetical (same priority as before) table.sort(allCandidates, function(a, b) if a.food ~= b.food then return a.food end return a.name < b.name end) local usedFurnaces = {} -- furnaces already assigned an item for _, cand in ipairs(allCandidates) do local itemName = cand.name if not disabledRecipes[itemName] and catalogue[itemName] then -- Find all compatible empty furnaces not yet used local compatFurnaces = {} for _, ef in ipairs(emptyInputFurnaces) do if not usedFurnaces[ef.name] and cand.recipe.furnaceSet[ef.type] then table.insert(compatFurnaces, ef) end end if #compatFurnaces > 0 then local totalInStorage = 0 for _, src in ipairs(catalogue[itemName]) do totalInStorage = totalInStorage + src.total end local available = totalInStorage - SMELT_RESERVE if available > 0 then local perFurnace = math.min(64, math.ceil(available / #compatFurnaces)) for _, ef in ipairs(compatFurnaces) do if available <= 0 then break end local toLoad = math.min(perFurnace, available) local remaining = toLoad local loaded = 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 toMove = math.min(slotItem.count, remaining) local n = chest.pushItems(ef.name, slot, toMove, SLOT_INPUT) if n and n > 0 then adjustCache(itemName, source.chest, -n) print(string.format("[SMELT] Input %s x%d -> %s (balanced %d/furnace)", itemName, n, ef.name, perFurnace)) didWork = true remaining = remaining - n available = available - n if remaining <= 0 then loaded = true break end end end end end if loaded then break end end if loaded or remaining < toLoad then usedFurnaces[ef.name] = true end end end end end end end 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 autoCompost() local catalogue = cache.catalogue local chests = getChests() local didWork = false -- 1) Pull bone meal from hopper back to chests local hopper = peripheral.wrap(COMPOST_HOPPER) if hopper then local contents = hopper.list() if contents then for slot, item in pairs(contents) do for _, chest in ipairs(chests) do local n = hopper.pushItems(chest, slot) if n and n > 0 then adjustCache(item.name, chest, n) print(string.format("[COMPOST] %s x%d -> %s", item.name, n, chest)) didWork = true break end end end end end -- 2) Feed compostable items into dropper local dropper = peripheral.wrap(COMPOST_DROPPER) if not dropper then return didWork end -- Check how much item capacity the dropper has (slots * 64 - current items) local dropperContents = dropper.list() local dropperUsedSlots = 0 local dropperUsedItems = 0 if dropperContents then for _, item in pairs(dropperContents) do dropperUsedSlots = dropperUsedSlots + 1 dropperUsedItems = dropperUsedItems + item.count end end local dropperSize = dropper.size() local dropperFreeSlots = dropperSize - dropperUsedSlots local dropperFreeItems = (dropperSize * 64) - dropperUsedItems if dropperFreeItems <= 0 then return didWork end for _, itemName in ipairs(COMPOSTABLE) do if dropperFreeItems <= 0 then break end if catalogue[itemName] then -- Count total in storage local totalInStorage = 0 for _, src in ipairs(catalogue[itemName]) do totalInStorage = totalInStorage + src.total end local reserve = COMPOST_TRASH[itemName] and 0 or COMPOST_RESERVE local available = totalInStorage - reserve if available > 0 then local toFeed = math.min(available, dropperFreeItems) local fed = 0 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 batch = math.min(slotItem.count, toFeed - fed) local n = chest.pushItems(COMPOST_DROPPER, slot, batch) if n and n > 0 then adjustCache(itemName, source.chest, -n) fed = fed + n didWork = true print(string.format("[COMPOST] Fed %s x%d -> dropper", itemName, n)) if fed >= toFeed then break end end end end end if fed >= toFeed then break end end dropperFreeItems = dropperFreeItems - fed 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 ------------------------------------------------- local function orderItem(itemName, amount, dropperOverride) activity.dispensing = true needsRedraw = true -- Use client-specified dropper if provided, otherwise master's default local dropperTarget = (dropperOverride and dropperOverride ~= "") and dropperOverride or DROPPER_NAME local catalogue = cache.catalogue if not catalogue[itemName] then statusMessage = "Not found: " .. itemName:gsub("^minecraft:", "") statusColor = colors.red statusTimer = 5 activity.dispensing = false needsRedraw = true return false end local dropper = peripheral.wrap(dropperTarget) if not dropper then statusMessage = "Dropper offline: " .. dropperTarget statusColor = colors.red statusTimer = 5 activity.dispensing = false needsRedraw = true return false end local remaining = amount for _, entry in ipairs(catalogue[itemName]) do local chest = peripheral.wrap(entry.chest) if chest then for slot, slotItem in pairs(chest.list()) do if slotItem.name == itemName then local toMove = math.min(remaining, slotItem.count) local moved = chest.pushItems(dropperTarget, slot, toMove) if moved and moved > 0 then remaining = remaining - moved adjustCache(itemName, entry.chest, -moved) needsRedraw = true smelterNeedsRedraw = true print(string.format("[ORDER] %s x%d from %s", itemName, moved, entry.chest)) end if remaining <= 0 then break end end end end if remaining <= 0 then break end end local sent = amount - remaining local short = itemName:gsub("^minecraft:", ""):gsub("_", " ") if sent > 0 then statusMessage = string.format("Dispensing %s x%d", short, sent) statusColor = colors.lime print(string.format("[OK] Ordered %s x%d", short, sent)) else statusMessage = "Could not order " .. short statusColor = colors.red end statusTimer = 5 activity.dispensing = false needsRedraw = true return sent > 0 end ------------------------------------------------- -- Touch handler (no peripheral calls — instant) ------------------------------------------------- local function handleTouch(x, y) local action, data = hitTest(x, y) if not action then print("[TOUCH] No zone hit") return end if action == "amount" then selectedAmount = data print("[UI] Amount set to " .. data) needsRedraw = true elseif action == "order" then local itemName = data if itemName then local short = itemName:gsub("^minecraft:", ""):gsub("_", " ") statusMessage = string.format("Ordering %s x%d...", short, selectedAmount) statusColor = colors.cyan statusTimer = 10 activity.dispensing = true needsRedraw = true orderItem(itemName, selectedAmount) end elseif action == "scan" then statusMessage = "Refreshing..." statusColor = colors.cyan statusTimer = 3 needsRedraw = true print("[UI] Manual refresh") elseif action == "kb_toggle" then showKeyboard = not showKeyboard print("[UI] Keyboard " .. (showKeyboard and "open" or "closed")) needsRedraw = true elseif action == "kb_key" then if #searchQuery < 30 then searchQuery = searchQuery .. data end currentPage = 1 needsRedraw = true elseif action == "kb_bksp" then if #searchQuery > 0 then searchQuery = searchQuery:sub(1, -2) end currentPage = 1 needsRedraw = true elseif action == "kb_space" then if #searchQuery < 30 then searchQuery = searchQuery .. " " end currentPage = 1 needsRedraw = true elseif action == "kb_done" then showKeyboard = false print("[UI] Keyboard closed") needsRedraw = true elseif action == "kb_clear" then searchQuery = "" currentPage = 1 print("[UI] Search cleared") needsRedraw = true elseif action == "page_prev" then if currentPage > 1 then currentPage = currentPage - 1 print("[UI] Page " .. currentPage) end needsRedraw = true elseif action == "page_next" then if currentPage < totalPages then currentPage = currentPage + 1 print("[UI] Page " .. currentPage) end needsRedraw = true end end ------------------------------------------------- -- Smelter touch handler ------------------------------------------------- local function handleSmelterTouch(x, y) local action, data = smelterHitTest(x, y) if not action then return end if action == "tab" then smelterView = data smelterPage = 1 print("[SMELT-UI] Tab: " .. data) smelterNeedsRedraw = true elseif action == "toggle_pause" then smeltingPaused = not smeltingPaused print("[SMELT-UI] Smelting " .. (smeltingPaused and "PAUSED" or "RESUMED")) saveDisabledRecipes() smelterNeedsRedraw = true needsRedraw = true elseif action == "toggle_recipe" then if disabledRecipes[data] then disabledRecipes[data] = nil else disabledRecipes[data] = true end local short = data:gsub("^minecraft:", ""):gsub("_", " ") print("[SMELT-UI] Recipe " .. short .. ": " .. (disabledRecipes[data] and "OFF" or "ON")) saveDisabledRecipes() smelterNeedsRedraw = true elseif action == "enable_all" then disabledRecipes = {} print("[SMELT-UI] All recipes enabled") saveDisabledRecipes() smelterNeedsRedraw = true elseif action == "disable_all" then for inputName in pairs(SMELTABLE) do disabledRecipes[inputName] = true end print("[SMELT-UI] All recipes disabled") saveDisabledRecipes() smelterNeedsRedraw = true elseif action == "page_prev" then if smelterPage > 1 then smelterPage = smelterPage - 1 end smelterNeedsRedraw = true elseif action == "page_next" then if smelterPage < smelterTotalPages then smelterPage = smelterPage + 1 end smelterNeedsRedraw = true elseif action == "craft" then local recipeIdx = data local recipe = CRAFTABLE[recipeIdx] if recipe then local short = recipe.output:gsub("^minecraft:", ""):gsub("_", " ") print(string.format("[CRAFT-UI] Craft request: %s (#%d)", short, recipeIdx)) local ok, err = craftItem(recipeIdx) if ok then statusMessage = "Crafted " .. short .. " x" .. recipe.count statusColor = colors.lime else statusMessage = "Craft failed: " .. (err or "unknown") statusColor = colors.red end statusTimer = 5 needsRedraw = true smelterNeedsRedraw = true end end end ------------------------------------------------- -- Network broadcast (sends state to client displays) ------------------------------------------------- local function broadcastState() if not networkModem then return end ensureItemList() local state = { type = "state", cache = { itemList = cache.itemList, grandTotal = cache.grandTotal, chestCount = cache.chestCount, totalSlots = cache.totalSlots, usedSlots = cache.usedSlots, freeSlots = cache.freeSlots, usedRatio = cache.usedRatio, dropperOk = cache.dropperOk, barrelOk = cache.barrelOk, furnaceCount = cache.furnaceCount, furnaceStatus = cache.furnaceStatus, }, activity = activity, alerts = activeAlerts, smeltingPaused = smeltingPaused, disabledRecipes = disabledRecipes, smeltable = SMELTABLE, craftable = CRAFTABLE, craftTurtleOk = craftTurtleName and peripheral.wrap(craftTurtleName) ~= nil, } networkModem.transmit(BROADCAST_CHANNEL, ORDER_CHANNEL, state) end ------------------------------------------------- -- Main ------------------------------------------------- local function main() print("=================================") print(" Inventory Manager v2 (Touch)") print("=================================") print("") if peripheral.wrap(DROPPER_NAME) then print("[OK] Dropper: " .. DROPPER_NAME) else print("[WARN] Dropper not found: " .. DROPPER_NAME) end if peripheral.wrap(BARREL_NAME) then print("[OK] Barrel: " .. BARREL_NAME) else print("[WARN] Barrel not found: " .. BARREL_NAME) end if setupMonitor() then print("[OK] Monitor: " .. (monName or MONITOR_SIDE)) else print("[WARN] No monitor on " .. MONITOR_SIDE) end if setupSmelterMonitor() then print("[OK] Smelter monitor: " .. (smelterMonName or SMELTER_MONITOR_SIDE)) else print("[WARN] No smelter monitor on " .. SMELTER_MONITOR_SIDE) end -- Find modem for client communication for _, name in ipairs(peripheral.getNames()) do if peripheral.getType(name) == "modem" then networkModem = peripheral.wrap(name) networkModemName = name networkModem.open(ORDER_CHANNEL) networkModem.open(CRAFT_REPLY_CHANNEL) break end end if networkModem then print("[OK] Network modem: " .. networkModemName) else print("[WARN] No modem found for client sync") end -- Detect crafting turtle on network for _, name in ipairs(peripheral.getNames()) do if name:match("^turtle_") then craftTurtleName = name break end end if craftTurtleName then print("[OK] Crafting turtle: " .. craftTurtleName) else print("[WARN] No crafting turtle found") end -- Load recipe toggles from disk loadDisabledRecipes() if smeltingPaused then print("[INIT] Smelting is PAUSED (toggle on smelter monitor)") end local enabledCount = 0 local totalRecipeCount = 0 for _ in pairs(SMELTABLE) do totalRecipeCount = totalRecipeCount + 1 end for k in pairs(SMELTABLE) do if not disabledRecipes[k] then enabledCount = enabledCount + 1 end end print(string.format("[INIT] %d/%d recipes enabled", enabledCount, totalRecipeCount)) -- Detect compost peripherals if peripheral.wrap(COMPOST_DROPPER) then print("[OK] Compost dropper: " .. COMPOST_DROPPER) else print("[WARN] Compost dropper not found: " .. COMPOST_DROPPER) end if peripheral.wrap(COMPOST_HOPPER) then print("[OK] Compost hopper: " .. COMPOST_HOPPER) else print("[WARN] Compost hopper not found: " .. COMPOST_HOPPER) end print(string.format("[INIT] Tracking %d low-stock alerts", #LOW_STOCK_ALERTS)) print("") print("Console shows log. Use the monitors to interact.") print("") -- Try loading cached inventory from disk for instant startup local cacheLoaded = loadCacheFromDisk() if cacheLoaded then print("[INIT] Loaded cached inventory (" .. #cache.itemList .. " types)") print("[INIT] Background refresh starting...") else -- No cache: do full scan with progress bar print("[INIT] No cache found. Scanning inventories...") if mon then local w, h = mon.getSize() local buf = window.create(mon, 1, 1, w, h, false) local function drawBoot(current, total, chestName) buf.setBackgroundColor(colors.black) buf.clear() -- Title buf.setBackgroundColor(colors.blue) buf.setCursorPos(1, 1) buf.write(string.rep(" ", w)) local title = " INVENTORY MANAGER " buf.setCursorPos(math.floor((w - #title) / 2) + 1, 1) buf.setTextColor(colors.white) buf.write(title) -- Scanning label local midY = math.floor(h / 2) buf.setBackgroundColor(colors.black) buf.setTextColor(colors.lightGray) local label = "Scanning inventories..." buf.setCursorPos(math.floor((w - #label) / 2) + 1, midY - 2) buf.write(label) -- Chest name local short = chestName or "" if #short > w - 4 then short = ".." .. short:sub(-(w - 6)) end buf.setTextColor(colors.gray) buf.setCursorPos(math.floor((w - #short) / 2) + 1, midY - 1) buf.write(short) -- Progress bar local barW = math.min(w - 8, 40) local barX = math.floor((w - barW) / 2) + 1 local ratio = total > 0 and (current / total) or 0 local filled = math.floor(ratio * barW) buf.setCursorPos(barX, midY + 1) buf.setBackgroundColor(colors.lime) buf.write(string.rep(" ", filled)) buf.setBackgroundColor(colors.gray) buf.write(string.rep(" ", barW - filled)) -- Percentage + count buf.setBackgroundColor(colors.black) buf.setTextColor(colors.white) local pct = string.format("%d/%d (%d%%)", current, total, math.floor(ratio * 100)) buf.setCursorPos(math.floor((w - #pct) / 2) + 1, midY + 3) buf.write(pct) -- Bottom accent buf.setCursorPos(1, h) buf.setBackgroundColor(colors.blue) buf.write(string.rep(" ", w)) buf.setVisible(true) buf.setVisible(false) end refreshCache(drawBoot) else refreshCache() end print("[INIT] Done. Found " .. #cache.itemList .. " item types.") end print("") parallel.waitForAny( -- Task 1: Background inventory scanner function() -- 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.") end while true do sleep(SCAN_INTERVAL) pcall(refreshCache) pcall(checkAlerts) needsRedraw = true smelterNeedsRedraw = true end end, -- Task 2: Barrel auto-sort function() while true do pcall(sortBarrel) sleep(POLL_INTERVAL) end end, -- Task 3: Auto-smelt function() while true do activity.smelting = true needsRedraw = true smelterNeedsRedraw = true local ok, didWork = pcall(autoSmelt) activity.smelting = false -- Update furnace status quickly after smelt cycle pcall(refreshFurnaceStatus) needsRedraw = true smelterNeedsRedraw = true sleep(SMELT_INTERVAL) end end, -- 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 if needsRedraw then needsRedraw = false pcall(drawDashboard) end -- Decrement status timer if statusTimer > 0 then statusTimer = statusTimer - 0.1 if statusTimer <= 0 then statusTimer = 0 needsRedraw = true end end -- Redraw periodically for alert cycling if #activeAlerts > 0 then needsRedraw = true end sleep(0.1) end end, -- Task 8: Smelter dashboard redraw function() smelterNeedsRedraw = true while true do if smelterNeedsRedraw then smelterNeedsRedraw = false pcall(drawSmelterDashboard) end sleep(0.1) end end, -- Task 9: Touch event listener (both monitors) function() while true do local event, side, x, y = os.pullEvent("monitor_touch") if smelterMonName and side == smelterMonName then print(string.format("[SMELT-TOUCH] x=%d y=%d", x, y)) handleSmelterTouch(x, y) else print(string.format("[TOUCH] x=%d y=%d", x, y)) handleTouch(x, y) end end end, -- Task 10: Network state broadcast function() while true do pcall(broadcastState) sleep(BROADCAST_INTERVAL) end end, -- Task 11: Network order/command listener function() if not networkModem then return end while true do local event, side, channel, replyChannel, message, distance = os.pullEvent("modem_message") if channel == ORDER_CHANNEL and type(message) == "table" then local handlerOk, handlerErr = pcall(function() if message.type == "order" and message.itemName and message.amount then print(string.format("[NET] Order: %s x%d", message.itemName, message.amount)) local pok, success = pcall(orderItem, message.itemName, message.amount, message.dropperName) if not pok then print("[NET] orderItem crashed: " .. tostring(success)) success = false statusMessage = "Order error" statusColor = colors.red statusTimer = 5 end pcall(function() networkModem.transmit(replyChannel, ORDER_CHANNEL, { type = "order_result", success = success, message = statusMessage, color = statusColor, }) end) pcall(broadcastState) elseif message.type == "scan" then print("[NET] Scan request from client") pcall(refreshCache) pcall(checkAlerts) needsRedraw = true smelterNeedsRedraw = true pcall(broadcastState) elseif message.type == "toggle_pause" then smeltingPaused = not smeltingPaused print("[NET] Smelting " .. (smeltingPaused and "PAUSED" or "RESUMED")) saveDisabledRecipes() smelterNeedsRedraw = true needsRedraw = true pcall(broadcastState) elseif message.type == "toggle_recipe" and message.recipe then if disabledRecipes[message.recipe] then disabledRecipes[message.recipe] = nil else disabledRecipes[message.recipe] = true end print("[NET] Recipe toggle: " .. message.recipe) saveDisabledRecipes() smelterNeedsRedraw = true pcall(broadcastState) elseif message.type == "enable_all" then disabledRecipes = {} print("[NET] All recipes enabled") saveDisabledRecipes() smelterNeedsRedraw = true pcall(broadcastState) elseif message.type == "disable_all" then for inputName in pairs(SMELTABLE) do disabledRecipes[inputName] = true end print("[NET] All recipes disabled") saveDisabledRecipes() smelterNeedsRedraw = true pcall(broadcastState) elseif message.type == "sort_barrel" and message.barrelName then print("[NET] Sort barrel: " .. message.barrelName) pcall(sortBarrel, message.barrelName) pcall(broadcastState) elseif message.type == "craft" and message.recipeIdx then print(string.format("[NET] Craft request: recipe #%d", message.recipeIdx)) local pok, ok, err = pcall(craftItem, message.recipeIdx) if not pok then print("[NET] craftItem crashed: " .. tostring(ok)) err = tostring(ok) ok = false end pcall(function() networkModem.transmit(replyChannel, ORDER_CHANNEL, { type = "craft_result", success = ok, error = err, }) end) smelterNeedsRedraw = true needsRedraw = true pcall(broadcastState) end end) -- end pcall handler if not handlerOk then print("[NET] Handler error: " .. tostring(handlerErr)) end end end end ) end main()