Enhance crafting functionality: implement modem communication for turtle crafting requests and responses

This commit is contained in:
MayaTheShy
2026-03-22 01:28:50 -04:00
parent cc6b1e999a
commit 98f8157bea

View File

@@ -1593,9 +1593,12 @@ local function getMissingIngredients(recipe)
return missing return missing
end end
--- Execute a craft via the networked turtle --- Execute a craft via the networked turtle.
-- Uses chest-side pullItems/pushItems to move items to/from the turtle, -- Sends a craft_request message via modem with ingredient locations.
-- since remote turtles may not expose the inventory API (list/pushItems). -- 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 function craftItem(recipeIdx)
local recipe = CRAFTABLE[recipeIdx] local recipe = CRAFTABLE[recipeIdx]
if not recipe then if not recipe then
@@ -1606,6 +1609,10 @@ local function craftItem(recipeIdx)
print("[CRAFT] No turtle detected on network") print("[CRAFT] No turtle detected on network")
return false, "No turtle" return false, "No turtle"
end 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 -- Verify the turtle is still on the network
if not peripheral.isPresent(craftTurtleName) then if not peripheral.isPresent(craftTurtleName) then
@@ -1621,124 +1628,85 @@ local function craftItem(recipeIdx)
local chests = getChests() local chests = getChests()
-- Helper: pull all 16 turtle slots into chests (chest-side) -- Build the slot map: for each grid position that needs an ingredient,
local function clearTurtle(knownItems) -- find the exact chest and slot where the item is located.
for slot = 1, 16 do local slotMap = {} -- turtleSlot -> { chestName, chestSlot, itemName, count }
for _, ch in ipairs(chests) do local reservedSlots = {} -- "chestName:slot" -> true (prevent double-booking)
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 for gridPos = 1, 9 do
local itemName = recipe.grid[gridPos] local itemName = recipe.grid[gridPos]
if itemName then if itemName then
local turtleSlot = GRID_TO_SLOT[gridPos] local turtleSlot = GRID_TO_SLOT[gridPos]
local placed = false local found = false
if cache.catalogue[itemName] then if cache.catalogue[itemName] then
for _, source in ipairs(cache.catalogue[itemName]) do for _, source in ipairs(cache.catalogue[itemName]) do
local chest = peripheral.wrap(source.chest) local chest = peripheral.wrap(source.chest)
if chest then if chest then
for slot, slotItem in pairs(chest.list()) do for slot, slotItem in pairs(chest.list()) do
if slotItem.name == itemName then local key = source.chest .. ":" .. slot
print(string.format("[CRAFT] Pushing %s from %s slot %d -> turtle slot %d", itemName, source.chest, slot, turtleSlot)) if slotItem.name == itemName and not reservedSlots[key] then
local ok, n = pcall(chest.pushItems, craftTurtleName, slot, 1, turtleSlot) slotMap[tostring(turtleSlot)] = {
if ok and n and n > 0 then chestName = source.chest,
adjustCache(itemName, source.chest, -n) chestSlot = slot,
placedItems[turtleSlot] = itemName itemName = itemName,
placed = true count = 1,
print(string.format("[CRAFT] Placed %s in turtle slot %d", itemName, turtleSlot)) }
break reservedSlots[key] = true
elseif not ok then found = true
print(string.format("[CRAFT] pushItems error: %s", tostring(n))) break
else
print(string.format("[CRAFT] pushItems returned %s", tostring(n)))
end
end end
end end
end end
if placed then break end if found then break end
end end
else
print(string.format("[CRAFT] Item %s not in catalogue!", itemName))
end end
if not placed then
-- Cleanup: pull placed items back using tracked names if not found then
print("[CRAFT] Missing ingredient, aborting") print(string.format("[CRAFT] Cannot find %s in storage, aborting", itemName))
clearTurtle(placedItems)
activity.crafting = false activity.crafting = false
needsRedraw = true needsRedraw = true
smelterNeedsRedraw = true smelterNeedsRedraw = true
return false, "Missing ingredient" return false, "Missing ingredient: " .. itemName
end end
end end
end end
-- 3. Wait for turtle to auto-craft -- Send craft request to turtle via modem
-- The turtle polls its crafting slots and crafts automatically. local craftMessage = {
-- Give it time: SETTLE_DELAY (1s) + craft time + margin. type = "craft_request",
print(string.format("[CRAFT] Ingredients placed for %s, waiting for turtle...", recipe.output)) recipeIdx = recipeIdx,
sleep(3) output = recipe.output,
slots = slotMap,
returnChests = chests,
}
-- 4. Pull all items from turtle back to chests print(string.format("[CRAFT] Sending craft request to turtle on channel %d", CRAFT_CHANNEL))
-- Strategy: first check crafting grid slots. If they're empty, networkModem.transmit(CRAFT_CHANNEL, CRAFT_REPLY_CHANNEL, craftMessage)
-- the turtle consumed the ingredients = craft succeeded.
-- Then pull everything remaining (output items or leftover ingredients).
local ingredientsRemain = false -- Adjust cache: mark ingredients as "in transit" (removed from chests)
for _, info in pairs(slotMap) do
-- Check crafting grid slots first adjustCache(info.itemName, info.chestName, -info.count)
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
end end
local success = not ingredientsRemain -- Wait for reply from turtle with timeout
local pulledOutput = 0 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) while os.clock() < deadline do
for slot = 1, 16 do local timerId = os.startTimer(math.max(0.1, deadline - os.clock()))
for _, ch in ipairs(chests) do local event, p1, p2, p3, p4 = os.pullEvent()
local chest = peripheral.wrap(ch)
if chest then if event == "modem_message" then
local n = chest.pullItems(craftTurtleName, slot) local channel = p2
if n and n > 0 then local message = p4
-- On success these are crafted output; on failure, leftovers if channel == CRAFT_REPLY_CHANNEL and type(message) == "table" and message.type == "craft_result" then
local itemName = success and recipe.output or (placedItems[slot] or recipe.output) result = message
adjustCache(itemName, ch, n) break
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
end end
elseif event == "timer" and p1 == timerId then
-- Timeout tick, loop will check deadline
end end
end end
@@ -1746,13 +1714,68 @@ local function craftItem(recipeIdx)
needsRedraw = true needsRedraw = true
smelterNeedsRedraw = 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("_", " ") 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 return true
else else
print("[CRAFT] Failed: ingredients were not consumed by turtle") -- Craft failed. The turtle returned ingredients to chests.
return false, "Craft failed" -- 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
end end