-- 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 = 64 -- keep at least 1 stack of each raw material local DEFRAG_INTERVAL = 60 -- seconds between defrag passes 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 (furnace + blast furnace) ["minecraft:sand"] = { result = "minecraft:glass", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:red_sand"] = { result = "minecraft:glass", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:cobblestone"] = { result = "minecraft:stone", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:stone"] = { result = "minecraft:smooth_stone", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:clay_ball"] = { result = "minecraft:brick", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:netherrack"] = { result = "minecraft:nether_brick", furnaces = {"minecraft:furnace", "minecraft:blast_furnace"} }, ["minecraft:sandstone"] = { result = "minecraft:smooth_sandstone", furnaces = {"minecraft:furnace", "minecraft:blast_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"} }, } -- 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:bamboo", "minecraft:nether_wart", "minecraft:crimson_fungus", "minecraft:warped_fungus", "minecraft:crimson_roots", "minecraft:warped_roots", "minecraft:shroomlight", "minecraft:weeping_vines", "minecraft:twisting_vines", -- Leaves "minecraft:oak_leaves", "minecraft:spruce_leaves", "minecraft:birch_leaves", "minecraft:jungle_leaves", "minecraft:acacia_leaves", "minecraft:dark_oak_leaves", "minecraft:mangrove_leaves", "minecraft:cherry_leaves", "minecraft:azalea_leaves", "minecraft:flowering_azalea_leaves", -- Flowers "minecraft:dandelion", "minecraft:poppy", "minecraft:blue_orchid", "minecraft:allium", "minecraft:azure_bluet", "minecraft:red_tulip", "minecraft:orange_tulip", "minecraft:white_tulip", "minecraft:pink_tulip", "minecraft:oxeye_daisy", "minecraft:cornflower", "minecraft:lily_of_the_valley", "minecraft:sunflower", "minecraft:lilac", "minecraft:rose_bush", "minecraft:peony", "minecraft:wither_rose", "minecraft:torchflower", "minecraft:pitcher_plant", -- Saplings "minecraft:oak_sapling", "minecraft:spruce_sapling", "minecraft:birch_sapling", "minecraft:jungle_sapling", "minecraft:acacia_sapling", "minecraft:dark_oak_sapling", "minecraft:mangrove_propagule", "minecraft:cherry_sapling", -- Food waste "minecraft:rotten_flesh", "minecraft:spider_eye", "minecraft:poisonous_potato", "minecraft:fermented_spider_eye", "minecraft:apple", "minecraft:bread", "minecraft:cookie", "minecraft:cake", "minecraft:pumpkin_pie", -- Farmer's Delight compostables "farmersdelight:tree_bark", "farmersdelight:straw", "farmersdelight:canvas", "farmersdelight:rice", "farmersdelight:rice_panicle", "farmersdelight:onion", "farmersdelight:tomato", "farmersdelight:cabbage", "farmersdelight:cabbage_leaf", } -- Build set for quick lookup local COMPOSTABLE_SET = {} for _, name in ipairs(COMPOSTABLE) do COMPOSTABLE_SET[name] = true end -- Reserve: keep at least this many of each compostable before composting the rest local COMPOST_RESERVE = 16 local COMPOST_DROPPER = "minecraft:dropper_10" local COMPOST_HOPPER = "minecraft:hopper_0" ------------------------------------------------- -- 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 } 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) Rebuild itemList from catalogue (lightweight — just totals) local itemList = {} local grandTotal = 0 for name, sources in pairs(cat) 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 end ------------------------------------------------- -- Inventory helpers ------------------------------------------------- local function getChests() local chests = {} for _, name in ipairs(peripheral.getNames()) do if peripheral.getType(name) == "minecraft:chest" then table.insert(chests, name) end end return chests end local function getFurnaces() 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 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 local chests = getChests() 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 -- Count furnaces local furnaceCount = 0 for _, ftype in ipairs(FURNACE_TYPES) do for _, name in ipairs(peripheral.getNames()) do if peripheral.getType(name) == ftype then furnaceCount = furnaceCount + 1 end end end cache.furnaceCount = furnaceCount -- 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() 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) ===== 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 return false, "Invalid recipe" end if not craftTurtleName then return false, "No turtle" end if not networkModem then return false, "No modem" end -- Verify the turtle is still on the network if not peripheral.isPresent(craftTurtleName) then return false, "Turtle offline" end 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 local n = chest.pushItems(craftTurtleName, slot, 1, turtleSlot) if n and n > 0 then adjustCache(itemName, source.chest, -n) placedItems[turtleSlot] = itemName placed = true break end end end end if placed then break end end 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. Signal turtle to craft networkModem.transmit(CRAFT_CHANNEL, CRAFT_REPLY_CHANNEL, { type = "craft", count = 1 }) print(string.format("[CRAFT] Sent craft request: %s", recipe.output)) -- 4. Wait for reply with timeout local timer = os.startTimer(5) local success = false local errMsg = "Turtle timeout" while true do local event = {os.pullEvent()} if event[1] == "modem_message" and event[3] == CRAFT_REPLY_CHANNEL and type(event[5]) == "table" and event[5].type == "craft_result" then os.cancelTimer(timer) success = event[5].success errMsg = event[5].error break elseif event[1] == "timer" and event[2] == timer then break end end -- 5. Pull all items from turtle back to chests -- On success: slots contain crafted output (recipe.output) -- On failure/timeout: slots still have placed ingredients local resultItems = success and {} or placedItems if success then -- After a successful craft, only the output item remains for slot = 1, 16 do resultItems[slot] = recipe.output end end 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 local itemName = resultItems[slot] or recipe.output adjustCache(itemName, ch, n) print(string.format("[CRAFT] Result %s x%d -> %s", itemName, n, 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, recipe.count)) return true else print(string.format("[CRAFT] Failed: %s", errMsg or "unknown")) return false, errMsg or "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") 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 = "" for _, ft in ipairs(recipe.furnaces) do if ft == "minecraft:furnace" then types = types .. "F" elseif ft == "minecraft:smoker" then types = types .. "S" elseif ft == "minecraft:blast_furnace" then types = types .. "B" end 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 -- 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 -- 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 totalC = #CRAFTABLE local availC = 0 for _, r in ipairs(CRAFTABLE) do if canCraftRecipe(r) then availC = availC + 1 end end bottomMsg = string.format(" Available: %d/%d recipes ", availC, totalC) 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() local barrel = peripheral.wrap(BARREL_NAME) 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 if catalogue[item.name] then for _, entry in ipairs(catalogue[item.name]) do 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 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 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 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 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 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 = false if recipe then for _, ft in ipairs(recipe.furnaces) do if ft == furnaceType then validHere = true break end end end 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 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 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) Load smeltable items into empty input slot inputItem = contents[SLOT_INPUT] if not inputItem then -- Build sorted candidate list: food first, then everything else local candidates = {} for itemName, recipe in pairs(SMELTABLE) do -- Check if this furnace type is compatible local compatible = false for _, ft in ipairs(recipe.furnaces) do if ft == furnaceType then compatible = true break end end if compatible and not disabledRecipes[itemName] and catalogue[itemName] then local isFood = false for _, ft in ipairs(recipe.furnaces) do if ft == "minecraft:smoker" then isFood = true break end end table.insert(candidates, { name = itemName, recipe = recipe, food = isFood }) end end -- Sort: food first table.sort(candidates, function(a, b) if a.food ~= b.food then return a.food end return a.name < b.name end) -- Try each candidate for _, cand in ipairs(candidates) do local itemName = cand.name -- Count total of this item across all chests local totalInStorage = 0 for _, src in ipairs(catalogue[itemName]) do totalInStorage = totalInStorage + src.total end -- Only smelt the excess beyond SMELT_RESERVE local available = totalInStorage - SMELT_RESERVE if available > 0 then local loaded = false local remaining = math.min(available, 64) 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(fname, slot, toMove, SLOT_INPUT) if n and n > 0 then print(string.format("[SMELT] Input %s x%d -> %s (reserve %d)", itemName, n, fname, math.max(0, totalInStorage - n))) didWork = true remaining = remaining - n if remaining <= 0 then loaded = true break end end end end end if loaded then break end end if loaded or remaining < math.min(available, 64) then break 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 available = totalInStorage - COMPOST_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 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 if message.type == "order" and message.itemName and message.amount then print(string.format("[NET] Order: %s x%d", message.itemName, message.amount)) local success = orderItem(message.itemName, message.amount, message.dropperName) networkModem.transmit(replyChannel, ORDER_CHANNEL, { type = "order_result", success = success, message = statusMessage, color = statusColor, }) 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 == "craft" and message.recipeIdx then print(string.format("[NET] Craft request: recipe #%d", message.recipeIdx)) local ok, err = craftItem(message.recipeIdx) networkModem.transmit(replyChannel, ORDER_CHANNEL, { type = "craft_result", success = ok, error = err, }) smelterNeedsRedraw = true needsRedraw = true pcall(broadcastState) end end end end ) end main()