local component = require("component") local robot = require("robot") local sides = require("sides") local io = require("io") local recipes = require('recipes') local crafting = component.crafting local inventory = component.inventory_controller RESULT_SLOT = 4 -- robots inventory is 4 wide and crafting uses the top left portion SLOT_MAP = { 1, 2, 3, 5, 6, 7, 9, 10, 11 } ---"item" being pair string id and damage separated by ';' eg. "minecraft:wool;3" ---returns "minecraft:wool", 3 ---@param str string ---@param delimiter string ---@return string|nil ---@return number|nil local function parse_item(str, delimiter) local pos = string.find(str, delimiter) if not pos then return nil, nil end return string.sub(str, 1, pos - 1), tonumber(string.sub(str, pos + 1)) end ---duplicate the table t ---@param t table ---@return table local function clone(t) local t2 = {} for k, v in pairs(t) do t2[k] = v end return t2 end ---add count to the selected key, or create it if it doesn't exist ---@param dict table ---@param key string ---@param count number local function add(dict, key, count) if dict[key] then dict[key] = dict[key] + count else dict[key] = count end end ---append the array b to the end of array a ---@param a table ---@param b table ---@return table local function append(a, b) for i = 1, #b do a[#a + 1] = b[i] end return a end ---Ask the user if they want to proceed ---@return boolean local function confirm() while true do print("Proceed? [Y/n]") local ch = io.read() if not ch then return false -- interrupted end if ch == 'y' or ch == 'Y' or ch == '' then return true elseif ch == 'n' then return false end end end ---read the facing inventory and return a table of item names and their count or nil if not facing an inventory ---@return table|nil local function read_chest_contents() local contents = {} local iter = inventory.getAllStacks(sides.forward) if not iter then return nil -- not facing an inventory end local stack = iter() while stack do if stack.name then -- check if the slot has an item in it local name = stack.name .. ";" .. stack.damage add(contents, name, stack.size) end stack = iter() end return contents end ---store the item in the selected slot into the facing inventory ---@return boolean local function store_item() -- check if that item is present in the inventory local item = inventory.getStackInInternalSlot() local iter = inventory.getAllStacks(sides.forward) local slot = 1 local empty_slot = -1 local stack = iter() while stack do if stack.name then if stack.name == item.name then inventory.dropIntoSlot(sides.forward, slot) -- check if all items have been stored local remaining = inventory.getStackInInternalSlot() if not remaining then -- print('Stored successfully') return true end end else if empty_slot == -1 then empty_slot = slot end end stack = iter() slot = slot + 1 end -- item not present in inventory if empty_slot ~= -1 then -- store in first empty slot local result, error = inventory.dropIntoSlot(sides.forward, empty_slot) if not result then print('Cannot store item: ' .. error .. "\n") return false else -- print('Stored successfully') return true end else print('Cannot store item - Inventory full') return false end end ---Search the facing chest for "item" and take "count" items and place them in "slot" ---"item" being pair string id and damage separated by ';' eg. "minecraft:wool;3" ---Returns number of items fetched ---@param item string ---@param output_slot number ---@param count number how many items to take ---@return boolean local function fetch_item(item, output_slot, count) if count == nil then count = 1 end local name, damage = parse_item(item, ";") robot.select(output_slot) local iter = inventory.getAllStacks(sides.forward) if not iter then print('Not facing a chest') return false end local stack = iter() local slot = 1 while stack do if stack.name then if stack.name == name and stack.damage == damage then inventory.suckFromSlot(sides.forward, slot, count) return true end end stack = iter() slot = slot + 1 end print('Missing ' .. item) return false end ---clear the 3x3 grid of any leftover items local function clear_crafting_space() for _, slot in ipairs(SLOT_MAP) do if inventory.getStackInInternalSlot(slot) then robot.select(slot) store_item() end end if inventory.getStackInInternalSlot(RESULT_SLOT) then robot.select(RESULT_SLOT) store_item() end end ---accepts a recipe table containing result and ingredients ---@param recipe table ---@param count number how many items to craft ---@return boolean local function craft_single_recipe(recipe, count) if inventory.getStackInInternalSlot(RESULT_SLOT) then print('Cannot craft - output slot occupied') return false end for i, ingredient in pairs(recipe.shape) do -- print('Fetching ' .. ingredient) if not fetch_item(ingredient, SLOT_MAP[i], count) then print('Failed to fetch ' .. ingredient) confirm() end end -- select slot for the result robot.select(RESULT_SLOT) -- craft the resulting item local crafted = crafting.craft() if not crafted then print('Crafting failed') return false end return store_item() end ---comment ---@param recipe table ---@param available_items table available initially ---@param currently_available table available from previous crafring ---@return table total_required_items ---@return table recipe_crafting_order array of recipes local function craft_recipe_recursion(recipe, available_items, currently_available) local total_required_items = {} local total_crafting_order = {} -- print('Crafting ' .. recipe.result) -- go through all the required ingredients for item, count in pairs(recipe.requires) do -- check if some crafting result provided this item local available = currently_available[item] if not available then available = 0 end -- print('Requires ' .. count .. 'x ' .. item ) -- print('I have ' .. available .. ' ' .. item .. ' from previous crafting') if available >= count then -- remove it from tracked available items so if other recipe requires it, it needs to craft it currently_available[item] = currently_available[item] - count else -- remove the items it has from tracked available items so if other recipe requires it, it needs to craft it currently_available[item] = 0 local required_count = count - available -- check if there is enough in the main available storage available = available_items[item] if not available then available = 0 end if available >= required_count then -- print('I have ' .. available .. ' ' .. item .. ' available') -- add the required amount to required items add(total_required_items, item, required_count) -- remove it from tracked available items so if other recipe requires it, it needs to craft it available_items[item] = available_items[item] - required_count else if available > 0 then -- add what it has to required items add(total_required_items, item, available) -- remove it from tracked available items so if other recipe requires it, it needs to craft it available_items[item] = 0 end -- update count required_count = required_count - available -- print('Have ' .. available .. ' and is missing ' .. required_count .. ' ' .. item) -- check if I can craft it local sub_recipe = recipes[item] if sub_recipe then -- print('Trying to craft ' .. item) local number_of_crafting_operations = math.ceil(required_count / sub_recipe.count) local excess = number_of_crafting_operations * sub_recipe.count - required_count -- print(' Will make ' .. number_of_crafting_operations .. ' crafting operations') -- print(' That will produce ' .. excess .. ' items I can use later') -- do the crafting local current_crafting_order = {} for _ = 1, number_of_crafting_operations do local required_items, crafting_order = craft_recipe_recursion(sub_recipe, available_items, currently_available) for k, v in pairs(required_items) do add(total_required_items, k, v) end current_crafting_order = append(current_crafting_order, crafting_order) end total_crafting_order = append(total_crafting_order, current_crafting_order) if excess > 0 then add(currently_available, item, excess) end else -- print('Need ' .. required_count .. ' more ' .. item) add(total_required_items, item, required_count) end end end end -- add the recipe it self to the end total_crafting_order[#total_crafting_order+1] = recipe return total_required_items, total_crafting_order end ---accepts a recipe table ---@param search string search for an craftable item ---@param amount number how many items to craft ---@return boolean local function craft_item(search, amount) search = string.lower(search) -- convert to unified lowercase local matches = {} for key, value in pairs(recipes) do if string.find(value.result, search) then print(#matches+1 .. ") " .. value.result) matches[#matches+1] = key elseif value.name ~= nil then if string.find(value.name, search) then print(#matches+1 .. ") " .. value.result) matches[#matches+1] = key end end end local selected if #matches == 0 then print(search .. ' not found') return false elseif #matches == 1 then selected = matches[1] else local input = io.read() if not input then print('Interrupted') return false end local index = tonumber(input) if not index then print('Invalid index') return false end if index < 1 or index > #matches then print('Invalid index') return false end selected = matches[index] end clear_crafting_space() local recipe = recipes[selected] print('Trying to craft ' .. amount .. 'x ' .. recipe.result) local repeating = false local count = math.ceil(amount / recipe.count) for _ = 1, count do local available_items = read_chest_contents() if not available_items then print('Not facing a chest') return false end local required_items, crafting_order = craft_recipe_recursion(recipe, clone(available_items), {}) print('\nRequires these items:') local failed = false for required_item, required_count in pairs(required_items) do local available = available_items[required_item] if not available then available = 0 end if available < required_count then print(available .. "/" .. required_count .. "\t" .. required_item .. "\t- Missing " .. required_count - available) failed = true else print(available .. "/" .. required_count .. "\t" .. required_item) end end if failed then return false end if not repeating then if not confirm() then print('Operation cancelled') return false end end -- proceed to crafting the things local steps = #crafting_order for i, rec in ipairs(crafting_order) do print('Step ' .. i .. '/' .. steps .. ' - ' .. rec.result) if not craft_single_recipe(rec, 1) then print('Unexpected error') return false end end repeating = true end return true end local target, amount = ... if not target then print("Usage: craft [count]") return end if not amount then amount = 1 end craft_item(target, amount)