local autocraft = {} autocraft.TAG_MAPPINGS = { ["c:dusts/redstone"] = { "minecraft:redstone" }, ["c:buckets/water"] = { "minecraft:water_bucket" }, ["c:ingots/iron"] = { "minecraft:iron_ingot" }, ["c:ingots/gold"] = { "minecraft:gold_ingot" }, ["c:ender_pearls"] = { "minecraft:ender_pearl" }, ["c:leathers"] = { "minecraft:leather" }, ["c:strings"] = { "minecraft:string" }, ["c:eggs"] = { "minecraft:egg" }, ["c:coals"] = { "minecraft:coal", "minecraft:charcoal" }, ["c:gems/diamond"] = { "minecraft:diamond" }, ["c:gems/quartz"] = { "minecraft:quartz" }, ["c:storage_blocks/gold"] = { "minecraft:gold_block" }, ["c:planks"] = { "minecraft:oak_planks", "minecraft:spruce_planks", "minecraft:birch_planks", "minecraft:jungle_planks", "minecraft:acacia_planks", "minecraft:dark_oak_planks", "minecraft:mangrove_planks", "minecraft:cherry_planks", "minecraft:bamboo_planks", "minecraft:crimson_planks", "minecraft:warped_planks", "minecraft:pale_oak_planks" }, ["c:dyes/white"] = { "minecraft:white_dye" }, ["c:dyes/orange"] = { "minecraft:orange_dye" }, ["c:dyes/magenta"] = { "minecraft:magenta_dye" }, ["c:dyes/light_blue"] = { "minecraft:light_blue_dye" }, ["c:dyes/yellow"] = { "minecraft:yellow_dye" }, ["c:dyes/lime"] = { "minecraft:lime_dye" }, ["c:dyes/pink"] = { "minecraft:pink_dye" }, ["c:dyes/gray"] = { "minecraft:gray_dye" }, ["c:dyes/light_gray"] = { "minecraft:light_gray_dye" }, ["c:dyes/cyan"] = { "minecraft:cyan_dye" }, ["c:dyes/purple"] = { "minecraft:purple_dye" }, ["c:dyes/blue"] = { "minecraft:blue_dye" }, ["c:dyes/brown"] = { "minecraft:brown_dye" }, ["c:dyes/green"] = { "minecraft:green_dye" }, ["c:dyes/red"] = { "minecraft:red_dye" }, ["c:dyes/black"] = { "minecraft:black_dye" }, ["c:dyes"] = { "minecraft:white_dye", "minecraft:orange_dye", "minecraft:magenta_dye", "minecraft:light_blue_dye", "minecraft:yellow_dye", "minecraft:lime_dye", "minecraft:pink_dye", "minecraft:gray_dye", "minecraft:light_gray_dye", "minecraft:cyan_dye", "minecraft:purple_dye", "minecraft:blue_dye", "minecraft:brown_dye", "minecraft:green_dye", "minecraft:red_dye", "minecraft:black_dye" }, ["c:glass_panes"] = { "minecraft:glass_pane", "minecraft:white_stained_glass_pane", "minecraft:orange_stained_glass_pane", "minecraft:magenta_stained_glass_pane", "minecraft:light_blue_stained_glass_pane", "minecraft:yellow_stained_glass_pane", "minecraft:lime_stained_glass_pane", "minecraft:pink_stained_glass_pane", "minecraft:gray_stained_glass_pane", "minecraft:light_gray_stained_glass_pane", "minecraft:cyan_stained_glass_pane", "minecraft:purple_stained_glass_pane", "minecraft:blue_stained_glass_pane", "minecraft:brown_stained_glass_pane", "minecraft:green_stained_glass_pane", "minecraft:red_stained_glass_pane", "minecraft:black_stained_glass_pane" }, ["c:glass_blocks"] = { "minecraft:glass", "minecraft:white_stained_glass", "minecraft:orange_stained_glass", "minecraft:magenta_stained_glass", "minecraft:light_blue_stained_glass", "minecraft:yellow_stained_glass", "minecraft:lime_stained_glass", "minecraft:pink_stained_glass", "minecraft:gray_stained_glass", "minecraft:light_gray_stained_glass", "minecraft:cyan_stained_glass", "minecraft:purple_stained_glass", "minecraft:blue_stained_glass", "minecraft:brown_stained_glass", "minecraft:green_stained_glass", "minecraft:red_stained_glass", "minecraft:black_stained_glass" }, ["c:shulker_boxes"] = { "minecraft:shulker_box", "minecraft:white_shulker_box", "minecraft:orange_shulker_box", "minecraft:magenta_shulker_box", "minecraft:light_blue_shulker_box", "minecraft:yellow_shulker_box", "minecraft:lime_shulker_box", "minecraft:pink_shulker_box", "minecraft:gray_shulker_box", "minecraft:light_gray_shulker_box", "minecraft:cyan_shulker_box", "minecraft:purple_shulker_box", "minecraft:blue_shulker_box", "minecraft:brown_shulker_box", "minecraft:green_shulker_box", "minecraft:red_shulker_box", "minecraft:black_shulker_box" }, ["minecraft:wool"] = { "minecraft:white_wool", "minecraft:orange_wool", "minecraft:magenta_wool", "minecraft:light_blue_wool", "minecraft:yellow_wool", "minecraft:lime_wool", "minecraft:pink_wool", "minecraft:gray_wool", "minecraft:light_gray_wool", "minecraft:cyan_wool", "minecraft:purple_wool", "minecraft:blue_wool", "minecraft:brown_wool", "minecraft:green_wool", "minecraft:red_wool", "minecraft:black_wool" }, ["c:chests/wooden"] = { "minecraft:chest", "minecraft:trapped_chest" }, ["c:crops/beetroot"] = { "minecraft:beetroot" }, ["c:crops/cabbage"] = { "farmersdelight:cabbage" }, ["c:crops/carrot"] = { "minecraft:carrot" }, ["c:crops/onion"] = { "farmersdelight:onion" }, ["c:crops/tomato"] = { "farmersdelight:tomato" }, ["c:crops/wheat"] = { "minecraft:wheat" }, ["c:drinks/milk"] = { "minecraft:milk_bucket", "farmersdelight:milk_bottle" }, ["c:foods/berry"] = { "minecraft:sweet_berries", "minecraft:glow_berries" }, ["c:foods/bread"] = { "minecraft:bread" }, ["c:foods/cooked_bacon"] = { "farmersdelight:cooked_bacon" }, ["c:foods/cooked_beef"] = { "minecraft:cooked_beef" }, ["c:foods/cooked_chicken"] = { "minecraft:cooked_chicken" }, ["c:foods/cooked_egg"] = { "farmersdelight:cooked_egg" }, ["c:foods/cooked_meat"] = { "minecraft:cooked_beef", "minecraft:cooked_chicken", "minecraft:cooked_mutton", "minecraft:cooked_porkchop", "minecraft:cooked_rabbit", "minecraft:cooked_cod", "minecraft:cooked_salmon" }, ["c:foods/cooked_mutton"] = { "minecraft:cooked_mutton" }, ["c:foods/cooked_salmon"] = { "minecraft:cooked_salmon" }, ["c:foods/leafy_green"] = { "farmersdelight:lettuce" }, ["c:foods/vegetable"] = { "minecraft:carrot", "minecraft:potato", "minecraft:beetroot", "farmersdelight:onion", "farmersdelight:tomato", "farmersdelight:cabbage", "farmersdelight:rice_panicle" }, ["minecraft:acacia_logs"] = { "minecraft:acacia_log", "minecraft:acacia_wood" }, ["minecraft:bamboo_blocks"] = { "minecraft:bamboo_block", "minecraft:stripped_bamboo_block", "minecraft:bamboo_wood", "minecraft:stripped_bamboo_wood" }, ["minecraft:birch_logs"] = { "minecraft:birch_log", "minecraft:birch_wood" }, ["minecraft:cherry_logs"] = { "minecraft:cherry_log", "minecraft:cherry_wood" }, ["minecraft:crimson_stems"] = { "minecraft:crimson_stem", "minecraft:crimson_hyphae" }, ["minecraft:dark_oak_logs"] = { "minecraft:dark_oak_log", "minecraft:dark_oak_wood" }, ["minecraft:jungle_logs"] = { "minecraft:jungle_log", "minecraft:jungle_wood" }, ["minecraft:mangrove_logs"] = { "minecraft:mangrove_log", "minecraft:mangrove_wood" }, ["minecraft:oak_logs"] = { "minecraft:oak_log", "minecraft:oak_wood" }, ["minecraft:planks"] = { "minecraft:oak_planks", "minecraft:spruce_planks", "minecraft:birch_planks", "minecraft:jungle_planks", "minecraft:acacia_planks", "minecraft:dark_oak_planks", "minecraft:mangrove_planks", "minecraft:cherry_planks", "minecraft:bamboo_planks", "minecraft:crimson_planks", "minecraft:warped_planks", "minecraft:pale_oak_planks" }, ["minecraft:spruce_logs"] = { "minecraft:spruce_log", "minecraft:spruce_wood" }, ["minecraft:warped_stems"] = { "minecraft:warped_stem", "minecraft:warped_hyphae" }, ["minecraft:logs"] = { "minecraft:oak_log", "minecraft:spruce_log", "minecraft:birch_log", "minecraft:jungle_log", "minecraft:acacia_log", "minecraft:dark_oak_log", "minecraft:mangrove_log", "minecraft:cherry_log", "minecraft:oak_wood", "minecraft:spruce_wood", "minecraft:birch_wood", "minecraft:jungle_wood", "minecraft:acacia_wood", "minecraft:dark_oak_wood", "minecraft:mangrove_wood", "minecraft:cherry_wood", "minecraft:crimson_stem", "minecraft:crimson_hyphae", "minecraft:warped_stem", "minecraft:warped_hyphae" }, ["minecraft:logs_that_burn"] = { "minecraft:oak_log", "minecraft:spruce_log", "minecraft:birch_log", "minecraft:jungle_log", "minecraft:acacia_log", "minecraft:dark_oak_log", "minecraft:mangrove_log", "minecraft:cherry_log", "minecraft:oak_wood", "minecraft:spruce_wood", "minecraft:birch_wood", "minecraft:jungle_wood", "minecraft:acacia_wood", "minecraft:dark_oak_wood", "minecraft:mangrove_wood", "minecraft:cherry_wood" }, ["minecraft:skulls"] = { "minecraft:skeleton_skull", "minecraft:wither_skeleton_skull", "minecraft:player_head", "minecraft:zombie_head", "minecraft:creeper_head", "minecraft:dragon_head", "minecraft:piglin_head" }, ["minecraft:smelts_to_glass"] = { "minecraft:glass" }, ["minecraft:soul_fire_base_blocks"] = { "minecraft:soul_sand", "minecraft:soul_soil" }, ["minecraft:stone_crafting_materials"] = { "minecraft:cobblestone", "minecraft:stone", "minecraft:andesite", "minecraft:diorite", "minecraft:granite", "minecraft:basalt", "minecraft:blackstone", "minecraft:deepslate", "minecraft:cobbled_deepslate", "minecraft:netherrack", "minecraft:calcite", "minecraft:tuff" }, ["minecraft:stone_tool_materials"] = { "minecraft:cobblestone", "minecraft:blackstone", "minecraft:cobbled_deepslate", "minecraft:andesite", "minecraft:diorite", "minecraft:granite" }, ["minecraft:wooden_slabs"] = { "minecraft:oak_slab", "minecraft:spruce_slab", "minecraft:birch_slab", "minecraft:jungle_slab", "minecraft:acacia_slab", "minecraft:dark_oak_slab", "minecraft:mangrove_slab", "minecraft:cherry_slab", "minecraft:bamboo_slab", "minecraft:crimson_slab", "minecraft:warped_slab", "minecraft:pale_oak_slab" }, ["minecraft:wooden_stairs"] = { "minecraft:oak_stairs", "minecraft:spruce_stairs", "minecraft:birch_stairs", "minecraft:jungle_stairs", "minecraft:acacia_stairs", "minecraft:dark_oak_stairs", "minecraft:mangrove_stairs", "minecraft:cherry_stairs", "minecraft:bamboo_stairs", "minecraft:crimson_stairs", "minecraft:warped_stairs", "minecraft:pale_oak_stairs" }, ["computercraft:wired_modem"] = { "computercraft:wired_modem" }, ["farmersdelight:canvas_signs"] = { "farmersdelight:canvas_sign" }, ["farmersdelight:hanging_canvas_signs"] = { "farmersdelight:hanging_canvas_sign" }, ["sc-goodies:elytra"] = { "sc-goodies:elytra" }, ["sc-goodies:iron_shulker/gold"] = { "sc-goodies:iron_shulker" }, ["sc-goodies:iron_shulker/iron"] = { "sc-goodies:iron_shulker" }, } function autocraft.tagOptions(tagKey) local mapped = autocraft.TAG_MAPPINGS[tagKey] if mapped then return mapped end if tagKey:find(":") then return { tagKey } end return { "minecraft:" .. tagKey } end local function cellItem(cell, storage, db, avoid) if not cell then return nil end if cell.item then return (avoid and avoid[cell.item]) and nil or cell.item end if cell.tag then local opts = autocraft.tagOptions(cell.tag) for _, o in ipairs(opts) do if not (avoid and avoid[o]) and (storage[o] or 0) > 0 then return o end end if db and db.byResult then for _, o in ipairs(opts) do if not (avoid and avoid[o]) and db.byResult[o] and #db.byResult[o] > 0 then return o end end end for _, o in ipairs(opts) do if not (avoid and avoid[o]) then return o end end return opts[1] end if cell.options then local fallback for _, sub in ipairs(cell.options) do local resolved = cellItem(sub, storage, db, avoid) if resolved and (storage[resolved] or 0) > 0 then return resolved end end if db and db.byResult then for _, sub in ipairs(cell.options) do local resolved = cellItem(sub, storage, db, avoid) if resolved and db.byResult[resolved] and #db.byResult[resolved] > 0 then fallback = resolved end end end for _, sub in ipairs(cell.options) do local resolved = cellItem(sub, storage, db, avoid) if resolved then fallback = fallback or resolved end end return fallback end return nil end function autocraft.gridFromRecipe(recipe, storage, db, avoid) local grid = {} for i = 1, 9 do grid[i] = false end if recipe.shaped then for row = 0, recipe.height - 1 do for col = 0, recipe.width - 1 do local srcIdx = row * recipe.width + col + 1 local cell = recipe.grid[srcIdx] if cell then local item = cellItem(cell, storage, db, avoid) local dstIdx = row * 3 + col + 1 grid[dstIdx] = item end end end else for i, cell in ipairs(recipe.ingredients) do if i > 9 then break end grid[i] = cellItem(cell, storage, db, avoid) end end return grid end local TURTLE_SLOTS = { 1, 2, 3, 5, 6, 7, 9, 10, 11 } function autocraft.turtleSlotsFromGrid(grid) local slots = {} for i = 1, 9 do if grid[i] then slots[TURTLE_SLOTS[i]] = grid[i] end end return slots end function autocraft.crafterSlotsFromGrid(grid) local slots = {} for i = 1, 9 do if grid[i] then slots[i] = grid[i] end end return slots end local function perCraftIngredients(recipe, storage, db, avoid) local qty = {} local order = {} local function add(ing) if not ing then return end if not qty[ing] then qty[ing] = 0 order[#order + 1] = ing end qty[ing] = qty[ing] + 1 end if recipe.shaped then for _, cell in ipairs(recipe.grid) do add(cellItem(cell, storage, db, avoid)) end else for _, cell in ipairs(recipe.ingredients) do add(cellItem(cell, storage, db, avoid)) end end return qty, order end local function gridIngredients(grid) local qty = {} local order = {} for _, ing in ipairs(grid) do if ing then if not qty[ing] then qty[ing] = 0 order[#order + 1] = ing end qty[ing] = qty[ing] + 1 end end return qty, order end local function pickBestRecipe(db, candidates, storage, avoid) if #candidates == 1 then return candidates[1] end local bestRecipe = candidates[1] local bestScore = -math.huge for _, recipe in ipairs(candidates) do local qtyPerCraft, order = perCraftIngredients(recipe, storage, db, avoid) local score = 0 for _, ing in ipairs(order) do local stock = storage[ing] or 0 local needed = qtyPerCraft[ing] if stock >= needed then score = score + 1000 + stock elseif db.byResult[ing] and #db.byResult[ing] > 0 then score = score + 10 else score = score - 10000 end end if score > bestScore then bestScore = score bestRecipe = recipe end end return bestRecipe end local function resolve(db, storage, missing, steps, item, amount, stack, opts, avoid, depth) if amount <= 0 then return end if (depth or 0) > 64 then missing[item] = (missing[item] or 0) + (amount - (storage[item] or 0)) storage[item] = 0 return end local have = storage[item] or 0 if have >= amount then storage[item] = have - amount return end local shortfall = amount - have if stack[item] then missing[item] = (missing[item] or 0) + shortfall storage[item] = 0 return end local candidates = db.byResult[item] if not candidates or #candidates == 0 then missing[item] = (missing[item] or 0) + shortfall storage[item] = 0 return end local recipe if opts and opts.recipeIndex and opts.recipeIndex[item] then recipe = candidates[opts.recipeIndex[item]] elseif opts and opts.chooseRecipe then recipe = opts.chooseRecipe(item, candidates, storage) else recipe = pickBestRecipe(db, candidates, storage, avoid) end local times = math.ceil(shortfall / recipe.count) local childAvoid = {} for k in pairs(avoid) do childAvoid[k] = true end childAvoid[item] = true local grid = autocraft.gridFromRecipe(recipe, storage, db, childAvoid) local qtyPerCraft, order = gridIngredients(grid) stack[item] = true for _, ing in ipairs(order) do resolve(db, storage, missing, steps, ing, qtyPerCraft[ing] * times, stack, opts, childAvoid, (depth or 0) + 1) end stack[item] = nil local produced = times * recipe.count storage[item] = have + produced - amount steps[#steps + 1] = { item = item, times = times, producedPerCraft = recipe.count, totalProduced = produced, neededHere = amount, recipe = recipe, grid = grid, turtleSlots = autocraft.turtleSlotsFromGrid(grid), crafterSlots = autocraft.crafterSlotsFromGrid(grid), } end --- Plan a craft. Returns { ok, steps, missing, finalStorage }. ---@param db table recipe database from recipes.decode/load ---@param storage table { itemName = count } snapshot of what's on hand ---@param item string desired result item ---@param amount number desired amount ---@param opts table|nil { recipeIndex = {[item]=idx}, chooseRecipe = fn } function autocraft.plan(db, storage, item, amount, opts) amount = amount or 1 local working = {} for k, v in pairs(storage) do working[k] = v end local missing = {} local steps = {} resolve(db, working, missing, steps, item, amount, {}, opts, {}, 0) return { ok = next(missing) == nil, steps = steps, missing = missing, finalStorage = working, } end --- Return every distinct result item in the database (sorted). ---@param db table ---@return string[] function autocraft.allResults(db) local out = {} for name in pairs(db.byResult) do out[#out + 1] = name end table.sort(out) return out end --- Case-insensitive substring + fuzzy search over result item names. --- `query` is matched as a plain substring (lowercased). Empty query returns all. ---@param db table ---@param query string ---@return string[] sorted, filtered list function autocraft.searchResults(db, query) query = (query or ""):lower() local all = autocraft.allResults(db) if query == "" then return all end local out = {} for _, name in ipairs(all) do if name:lower():find(query, 1, true) then out[#out + 1] = name end end return out end return autocraft