--- crafter.lua --- Library for automated crafting in Minecraft/SkyBlock ---@class crafter local lib = {} local player = require("player") local json = require("json") ---@type number|nil local schedule = nil ---@type number local currentIndex = 0 ---@type number local tickDelay = 2 ---@type number local tickCounter = 0 ---@type fun(slot: number)|nil local doneCallback = nil ---@type fun()|nil local timeoutCallback = nil ---@type number local clickDelay = 0 ---@type number local craftStartTick = nil local function getEmptySlot() for slot = 54, 89 do if not player.inventory.getStack(slot) then return slot end end return nil end registerClientTick(function() if schedule then if craftStartTick and (os.clock() - craftStartTick) * 20 > 100 then local closed = player.inventory.closeScreen() schedule = nil currentIndex = 0 craftStartTick = nil if timeoutCallback then timeoutCallback(closed) end return end local isOpen = player.inventory.isAnyScreenOpened() if isOpen then local containerSlots = player.inventory.getContainerSlots() if containerSlots and containerSlots > 34 then tickCounter = tickCounter + 1 if tickCounter >= tickDelay then tickCounter = 0 currentIndex = currentIndex + 1 local t = schedule[currentIndex] if t then local from = t.from + 45 if t.from >= 54 then from = t.from end if from >= 54 then local to = t.to if to >= 7 then to = 27 + to - 6 elseif to >= 4 then to = 18 + to - 3 elseif to >= 1 then to = 9 + to end player.inventory.leftClick(from) if clickDelay > 0 then tickCounter = tickDelay - clickDelay end player.inventory.leftClick(to) end end if currentIndex > #schedule then if currentIndex == #schedule + 1 then tickCounter = tickDelay - 1 elseif currentIndex == #schedule + 5 then for _, t in ipairs(schedule) do local to = t.to if to >= 7 then to = 27 + to - 6 elseif to >= 4 then to = 18 + to - 3 elseif to >= 1 then to = 9 + to end local stack = player.inventory.getStackFromContainer(to) if not stack then player.inventory.closeScreen() schedule = nil currentIndex = 0 craftStartTick = nil if timeoutCallback then timeoutCallback() end return end end local resultStack = player.inventory.getStackFromContainer(23) if resultStack then player.inventory.shiftLeftClick(23) else player.inventory.closeScreen() schedule = nil currentIndex = 0 craftStartTick = nil if timeoutCallback then timeoutCallback() end return end end if currentIndex >= #schedule + 10 then local emptySlot = getEmptySlot() schedule = nil currentIndex = 0 craftStartTick = nil if doneCallback then doneCallback(emptySlot) end end end end end end end end) ---@class CraftTask ---@field from number Source slot (9-44 for player inventory) ---@field to number Destination slot (1-9 for crafting grid) --- Crafts items from specified slots ---@param task CraftTask[] Array of craft tasks {from, to} ---@param delay number Delay between each craft action in ticks (default: 2) ---@param betweenClickDelay number Delay between from and to clicks in ticks ---@param callback fun(slot: number)|nil Callback function when crafting is complete, receives empty slot number ---@param timeout fun(closed: boolean)|nil Callback function when crafting fails (timeout or empty slot) function lib.craftFromSlots(task, delay, betweenClickDelay, callback, timeout) doneCallback = callback timeoutCallback = timeout schedule = task currentIndex = 0 if delay then tickDelay = delay else tickDelay = 2 end tickCounter = tickDelay - 1 craftStartTick = os.clock() clickDelay = betweenClickDelay or 1 end local function readFile(file) local f, err = io.open(file, "r") if not f then print("Error opening file: " .. (err or "unknown")) return nil end local content = f:read("*a") f:close() return content end --- Finds inventory slots containing the specified item ---@param id string Item skyblock_id to search for ---@param countNeeded number Minimum count required in a slot ---@return number[] slots Array of slot numbers containing the item function lib.findStacks(id, countNeeded) local slots = {} for slot = 9, 45 do local item = player.inventory.getStack(slot) if not item then goto continue end if item.skyblock_id == id and item.count >= countNeeded then slots[#slots + 1] = slot end ::continue:: end return slots end local function parseRecipeItem(str) if not str or str == "" then return nil, nil end local parts = {} for part in str:gmatch("([^:]+)") do parts[#parts + 1] = part end return parts[1], tonumber(parts[2]) or 1 end local function parse(recipeSlot, toSlot, task, usedSlots) if not recipeSlot or recipeSlot == "" then return end local id, countNeeded = parseRecipeItem(recipeSlot) if not id then return end local slots = lib.findStacks(id, countNeeded) for _, slot in ipairs(slots) do if not usedSlots[slot] then usedSlots[slot] = true table.insert(task, { from = slot, to = toSlot }) return end end end --- Crafts an item by its ID using recipe from JSON file --- Reads recipe from config/hypixelcry/item-repo/items/{id}.json ---@param id string Item ID to craft (e.g., "MELON") ---@param delay number Delay between each craft action in ticks (default: 2) ---@param betweenClickDelay number Delay between from and to clicks in ticks ---@param callback fun(slot: number)|nil Callback function when crafting is complete ---@param timeout fun(closed: boolean)|nil Callback function when crafting fails function lib.craftFromId(id, delay, betweenClickDelay, callback, timeout) local f = readFile("config/hypixelcry/item-repo/items/" .. id .. ".json") if f then local js = json.parse(f) local task = {} local usedSlots = {} parse(js.recipe.A1, 1, task, usedSlots) parse(js.recipe.A2, 2, task, usedSlots) parse(js.recipe.A3, 3, task, usedSlots) parse(js.recipe.B1, 4, task, usedSlots) parse(js.recipe.B2, 5, task, usedSlots) parse(js.recipe.B3, 6, task, usedSlots) parse(js.recipe.C1, 7, task, usedSlots) parse(js.recipe.C2, 8, task, usedSlots) parse(js.recipe.C3, 9, task, usedSlots) if #task == 0 then if callback then callback(nil) end return end doneCallback = callback timeoutCallback = timeout schedule = task currentIndex = 0 if delay then tickDelay = delay else tickDelay = 2 end tickCounter = tickDelay - 1 craftStartTick = os.clock() clickDelay = betweenClickDelay or 1 end end return lib