Files
Inventory-Manager-CC/inventoryManager.lua

3208 lines
126 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 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(barrelOverride)
local barrelTarget = (barrelOverride and barrelOverride ~= "") and barrelOverride or BARREL_NAME
local barrel = peripheral.wrap(barrelTarget)
if not barrel then return end
local contents = barrel.list()
if not contents or not next(contents) then return end
activity.sorting = true
needsRedraw = true
local catalogue = cache.catalogue
local chests = getChests()
for slot, item in pairs(contents) do
local moved = 0
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 == "sort_barrel" and message.barrelName then
print("[NET] Sort barrel: " .. message.barrelName)
pcall(sortBarrel, message.barrelName)
pcall(broadcastState)
elseif message.type == "craft" and message.recipeIdx then
print(string.format("[NET] Craft request: recipe #%d", message.recipeIdx))
local 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()