From 98f8157beab734f364908bfd0c3c6f6586e565e6 Mon Sep 17 00:00:00 2001 From: MayaTheShy Date: Sun, 22 Mar 2026 01:28:50 -0400 Subject: [PATCH] Enhance crafting functionality: implement modem communication for turtle crafting requests and responses --- inventoryManager.lua | 217 ++++++++++++++++++++++++------------------- 1 file changed, 120 insertions(+), 97 deletions(-) diff --git a/inventoryManager.lua b/inventoryManager.lua index 2504ae9..b1e5c7a 100644 --- a/inventoryManager.lua +++ b/inventoryManager.lua @@ -1593,9 +1593,12 @@ local function getMissingIngredients(recipe) 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). +--- Execute a craft via the networked turtle. +-- Sends a craft_request message via modem with ingredient locations. +-- The turtle pulls items herself, crafts, pushes results back, and replies. +-- Requires the turtle to have a wired modem and run the updated craftingTurtle.lua. +local CRAFT_TIMEOUT = 15 -- seconds to wait for turtle reply + local function craftItem(recipeIdx) local recipe = CRAFTABLE[recipeIdx] if not recipe then @@ -1606,6 +1609,10 @@ local function craftItem(recipeIdx) print("[CRAFT] No turtle detected on network") return false, "No turtle" end + if not networkModem then + print("[CRAFT] No modem available for craft commands") + return false, "No modem" + end -- Verify the turtle is still on the network if not peripheral.isPresent(craftTurtleName) then @@ -1621,124 +1628,85 @@ local function craftItem(recipeIdx) 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 + -- Build the slot map: for each grid position that needs an ingredient, + -- find the exact chest and slot where the item is located. + local slotMap = {} -- turtleSlot -> { chestName, chestSlot, itemName, count } + local reservedSlots = {} -- "chestName:slot" -> true (prevent double-booking) - -- 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 + local found = 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 - print(string.format("[CRAFT] Pushing %s from %s slot %d -> turtle slot %d", itemName, source.chest, slot, turtleSlot)) - local ok, n = pcall(chest.pushItems, craftTurtleName, slot, 1, turtleSlot) - if ok and n and n > 0 then - adjustCache(itemName, source.chest, -n) - placedItems[turtleSlot] = itemName - placed = true - print(string.format("[CRAFT] Placed %s in turtle slot %d", itemName, turtleSlot)) - break - elseif not ok then - print(string.format("[CRAFT] pushItems error: %s", tostring(n))) - else - print(string.format("[CRAFT] pushItems returned %s", tostring(n))) - end + local key = source.chest .. ":" .. slot + if slotItem.name == itemName and not reservedSlots[key] then + slotMap[tostring(turtleSlot)] = { + chestName = source.chest, + chestSlot = slot, + itemName = itemName, + count = 1, + } + reservedSlots[key] = true + found = true + break end end end - if placed then break end + if found then break end end - else - print(string.format("[CRAFT] Item %s not in catalogue!", itemName)) end - if not placed then - -- Cleanup: pull placed items back using tracked names - print("[CRAFT] Missing ingredient, aborting") - clearTurtle(placedItems) + + if not found then + print(string.format("[CRAFT] Cannot find %s in storage, aborting", itemName)) activity.crafting = false needsRedraw = true smelterNeedsRedraw = true - return false, "Missing ingredient" + return false, "Missing ingredient: " .. itemName end end end - -- 3. Wait for turtle to auto-craft - -- The turtle polls its crafting slots and crafts automatically. - -- Give it time: SETTLE_DELAY (1s) + craft time + margin. - print(string.format("[CRAFT] Ingredients placed for %s, waiting for turtle...", recipe.output)) - sleep(3) + -- Send craft request to turtle via modem + local craftMessage = { + type = "craft_request", + recipeIdx = recipeIdx, + output = recipe.output, + slots = slotMap, + returnChests = chests, + } - -- 4. Pull all items from turtle back to chests - -- Strategy: first check crafting grid slots. If they're empty, - -- the turtle consumed the ingredients = craft succeeded. - -- Then pull everything remaining (output items or leftover ingredients). + print(string.format("[CRAFT] Sending craft request to turtle on channel %d", CRAFT_CHANNEL)) + networkModem.transmit(CRAFT_CHANNEL, CRAFT_REPLY_CHANNEL, craftMessage) - local ingredientsRemain = false - - -- Check crafting grid slots first - for gridPos = 1, 9 do - if placedItems[GRID_TO_SLOT[gridPos]] then - for _, ch in ipairs(chests) do - local chest = peripheral.wrap(ch) - if chest then - local n = chest.pullItems(craftTurtleName, GRID_TO_SLOT[gridPos]) - if n and n > 0 then - -- Ingredient still there = craft failed for this slot - ingredientsRemain = true - local itemName = placedItems[GRID_TO_SLOT[gridPos]] - adjustCache(itemName, ch, n) - print(string.format("[CRAFT] Ingredient returned: %s x%d from slot %d", itemName, n, GRID_TO_SLOT[gridPos])) - break - end - end - end - end + -- Adjust cache: mark ingredients as "in transit" (removed from chests) + for _, info in pairs(slotMap) do + adjustCache(info.itemName, info.chestName, -info.count) end - local success = not ingredientsRemain - local pulledOutput = 0 + -- Wait for reply from turtle with timeout + print(string.format("[CRAFT] Waiting for turtle reply (timeout: %ds)...", CRAFT_TIMEOUT)) + local deadline = os.clock() + CRAFT_TIMEOUT + local result = nil - -- Pull everything remaining (output on success, or stray items) - 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 - -- On success these are crafted output; on failure, leftovers - local itemName = success and recipe.output or (placedItems[slot] or recipe.output) - adjustCache(itemName, ch, n) - if success then pulledOutput = pulledOutput + n end - print(string.format("[CRAFT] Pulled %s x%d from slot %d -> %s", itemName, n, slot, ch)) - break - end + while os.clock() < deadline do + local timerId = os.startTimer(math.max(0.1, deadline - os.clock())) + local event, p1, p2, p3, p4 = os.pullEvent() + + if event == "modem_message" then + local channel = p2 + local message = p4 + if channel == CRAFT_REPLY_CHANNEL and type(message) == "table" and message.type == "craft_result" then + result = message + break end + elseif event == "timer" and p1 == timerId then + -- Timeout tick, loop will check deadline end end @@ -1746,13 +1714,68 @@ local function craftItem(recipeIdx) needsRedraw = true smelterNeedsRedraw = true - if success then + if not result then + -- Timeout — turtle didn't respond. Items may be stuck in turtle. + -- We've already adjusted the cache as if ingredients were consumed. + -- A manual scan will reconcile later. + print("[CRAFT] TIMEOUT: No reply from turtle within " .. CRAFT_TIMEOUT .. "s") + print("[CRAFT] Items may be stuck in turtle. Run a manual scan to reconcile.") + return false, "Turtle timeout" + end + + if result.success then + -- Craft succeeded. The turtle already pushed results back to chests. + -- We need to credit the output items to the cache. + -- The ingredients were already debited above. + -- The turtle reports what it pushed; we credit the output item. + local totalOutput = result.totalOutput or recipe.count + -- Credit output across the chests (we don't know exactly which chest + -- the turtle pushed to, so trigger a targeted rescan) + -- For now, credit to the first available chest as an approximation. + -- The next periodic scan will reconcile. + if result.results then + for _, r in ipairs(result.results) do + -- Find which chest likely received it + for _, ch in ipairs(chests) do + local chest = peripheral.wrap(ch) + if chest then + for slot, slotItem in pairs(chest.list()) do + if slotItem.name == r.name then + -- Found the output in a chest, credit it + adjustCache(r.name, ch, r.count) + goto credited + end + end + end + end + -- If we can't find it, still credit to first chest for cache consistency + if #chests > 0 then + adjustCache(r.name, chests[1], r.count) + end + ::credited:: + end + else + -- Fallback: credit the expected output + if #chests > 0 then + adjustCache(recipe.output, chests[1], totalOutput) + end + end + local short = recipe.output:gsub("^minecraft:", ""):gsub("_", " ") - print(string.format("[CRAFT] OK: %s x%d", short, pulledOutput)) + print(string.format("[CRAFT] OK: %s x%d", short, totalOutput)) return true else - print("[CRAFT] Failed: ingredients were not consumed by turtle") - return false, "Craft failed" + -- Craft failed. The turtle returned ingredients to chests. + -- We already debited the cache; now credit them back. + -- The returned items are back in the chests, so re-credit them. + for turtleSlotStr, info in pairs(slotMap) do + -- Re-credit the ingredient (it was returned) + adjustCache(info.itemName, info.chestName, info.count) + end + + local errMsg = result.error or "Craft failed" + print("[CRAFT] Failed: " .. errMsg) + return false, errMsg end end