From b594173f46fb5e9cf6d865b95310598f4253424b Mon Sep 17 00:00:00 2001 From: GHXX Date: Fri, 11 Sep 2026 08:50:09 +0200 Subject: [PATCH] rewrite autocrafting script --- autocraft.lua | 126 ----------------- autocraft/addRecipe.lua | 71 ++++++++++ autocraft/craft.lua | 258 +++++++++++++++++++++++++++++++++++ autocraft/recipeParseLib.lua | 95 +++++++++++++ autocraft/recipes.txt | 27 ++++ 5 files changed, 451 insertions(+), 126 deletions(-) delete mode 100644 autocraft.lua create mode 100644 autocraft/addRecipe.lua create mode 100644 autocraft/craft.lua create mode 100644 autocraft/recipeParseLib.lua create mode 100644 autocraft/recipes.txt diff --git a/autocraft.lua b/autocraft.lua deleted file mode 100644 index 3d2aef4..0000000 --- a/autocraft.lua +++ /dev/null @@ -1,126 +0,0 @@ -function agc(name) - return assert(components:getFirst(name), "failed to get component "..name) -end - -local computer = agc("computer") -local fs = require("filesystem") -local kernel = require("kernel") -local crafter = agc("digitalCrafter") -print("it works") - - -local function craft(...) - while true do - local cooldown = crafter.cooldownRemaining - if cooldown <= 0 then break end - sleep(cooldown) - end - return crafter:craft(...) -end - -print("Which item would you like to craft?") -local buf = "" - -local ctrlCReceived = false -local keyEvents = {} -kernel:registerEventCallback("charTyped", function(...) table.insert(keyEvents, {...}) end) -kernel:registerEventCallback("keyPressed", function(...) table.insert(keyEvents, {...}) end) -local matchingItems = {} -while true do - if #keyEvents > 0 then - local event = table.remove(keyEvents, 1) - if event[1] == "charTyped" then - if event[2] == "\n" then - if #matchingItems == 1 then - print() - print("Selected item: "..tostring(matchingItems[1])) - break - end - elseif event[2] == "\b" then - if #buf > 0 then - buf = buf:sub(1, #buf-1) - end - else - buf = buf .. event[2] - end - matchingItems = #buf > 0 and crafter:searchItemsByName(buf) or {} - printInline("\r["..(#matchingItems).." match"..(#matchingItems ~= 1 and "es" or "").."] "..buf.." ") - elseif event[1] == "keyPressed" then - local charRepr, code, _, mod = table.unpack(event, 2) - if code == 67 and (mod&2>0) then -- ctrl+c - ctrlCReceived = true - error("ctrl c pressed") - end - end - else - sleep(0.25) - end -end - -assert(#matchingItems == 1, "not exactly 1 matching item") -local matchingItem = matchingItems[1] - -local function readNumber(prompt) - printInline(prompt) - keyEvents = {} - local buf = "" - while true do - if #keyEvents > 0 then - local event = table.remove(keyEvents, 1) - if event[1] == "charTyped" then - if event[2] == "\n" then - if #matchingItems == 1 then - local num = tonumber(buf) - if num ~= nil then print() return num end - buf = "" - end - elseif event[2] == "\b" then - if #buf > 0 then - buf = buf:sub(1, #buf-1) - end - else - buf = buf .. event[2] - end - printInline("\r"..prompt.. " "..buf.." ") - elseif event[1] == "keyPressed" then - local charRepr, code, _, mod = table.unpack(event, 2) - if code == 67 and (mod&2>0) then -- ctrl+c - ctrlCReceived = true - error("ctrl c pressed") - end - end - else - sleep(0.25) - end - end -end - -local quantity = readNumber("How many items do we want to craft?") -print("We want to craft "..quantity.."x "..matchingItem) - -local craftingRecipesToExecute = {} -- this is a stack, use table.insert to add new recipes to craft, and remove from end when executing the recipes -local itemsLeftToCraft = {[matchingItem] = quantity} -- key=itemname, value=amountNeeded -local reservedItems = {} -- itemname:reservedQuantity; needed so we dont queue two recipes that need more ingredients in total than we have in our storage system -while next(itemsLeftToCraft) ~= nil do -- if there is anything left that needs to be crafted - local toCraft = assert(next(itemsLeftToCraft), "somehow got nil1") - itemsLeftToCraft[toCraft] = 0 - - -- check if cached recipe exists, if so use that - - -- else - -- get possible recipes - - -- compute how often we apply this recipe - - -- add to craftingRecipesToExecute - -- reserve items we need for crafting, or add them to itemsLeftToCraft - - -- TODO ac: add extraoutputs to 'recipe' datastructure -end - - - - - - - diff --git a/autocraft/addRecipe.lua b/autocraft/addRecipe.lua new file mode 100644 index 0000000..e84c6a6 --- /dev/null +++ b/autocraft/addRecipe.lua @@ -0,0 +1,71 @@ +local fs = require("filesystem") +local recipeLib = load(fs:readAllText("./recipeParseLib.lua"), "recipeParseLib.lua")() + +local args = table.pack(...) + +local resultItem = args[1] +assert(resultItem, "supply the item you want to add a recipe for as the first item") + +local crafter = components:getFirst("digitalCrafter") + +local recipes = crafter:searchRecipesWithResult(resultItem) + +assert(#recipes > 0, ("no recipe found for %s."):format(resultItem)) + + +local function acToLuaRecipe(acRecipe) + return { + resultCount = acRecipe.resultCount, + slots = acRecipe.ingredients, + resultItem = acRecipe.result + } +end + +local kernel = require("kernel") +local keyEvents = {} +kernel:registerEventCallback("charTyped", function(...) table.insert(keyEvents, {...}) end) +kernel:registerEventCallback("keyPressed", function(...) table.insert(keyEvents, {...}) end) + +---@param eventFilterOrNil "charTyped"|"keyPressed"|nil +local function waitForInputEvent(eventFilterOrNil) + sleep(0) + while #keyEvents == 0 do sleep(0.5) end + local res = table.remove(keyEvents, 1) + if eventFilterOrNil == nil or res[1] == eventFilterOrNil then + return res + else + return waitForInputEvent(eventFilterOrNil) or tailCallsAreSus + end +end + +local recipe = recipes[1] +if #recipes > 1 then + print("Looks like there are multiple recipes. Please pick one of the following by typing the number on the left and then enter.") + for index, value in ipairs(recipes) do + print(("%d: %s"):format(index, recipeLib:serializeSingleRecipe(acToLuaRecipe(value)))) + end + + local buf = "" + while true do + printInline(("\rSelection: %s_"):format(buf)) + local ctyped, repr = table.unpack(waitForInputEvent("charTyped")) + if repr == "\b" then + buf = buf:sub(1, #buf-1) + elseif repr == "\n" then + break + elseif repr ~= nil then + buf = buf .. repr + end + end + + print() + local selection = tonumber(buf) + if selection == nil or selection < 1 or selection > #recipes then + error(("The inputted number '%s' is not a valid option"):format(buf)) + end + recipe = recipes[selection] +end + +recipeLib:addNewRecipe(acToLuaRecipe(recipe)) + +print(("Done, added the following recipe:\n%s"):format(recipeLib:serializeSingleRecipe(acToLuaRecipe(recipe)))) \ No newline at end of file diff --git a/autocraft/craft.lua b/autocraft/craft.lua new file mode 100644 index 0000000..cc7cb5a --- /dev/null +++ b/autocraft/craft.lua @@ -0,0 +1,258 @@ + + +--local itemToCraftInternalName, itemToCraftAmount = "nuclearcraft:empty_active_heat_sink", 15 +--local itemToCraftInternalName, itemToCraftAmount = "nuclearcraft:empty_heat_sink", 1 +--local itemToCraftInternalName, itemToCraftAmount = "nuclearcraft:helium_collector", 32 +--local itemToCraftInternalName, itemToCraftAmount = "nuclearcraft:plate_advanced", 32 +--local itemToCraftInternalName, itemToCraftAmount = "nuclearcraft:pump", 1 +--local itemToCraftInternalName, itemToCraftAmount = "nuclearcraft:fission_reactor_controller", 1 +--local itemToCraftInternalName, itemToCraftAmount = "nuclearcraft:fission_reactor_port", 6 +local itemToCraftInternalName, itemToCraftAmount = "mekanism:quantum_entangloporter", 2 + +local function agc(name) + return assert(components:getFirst(name), "failed to get component "..name) +end + +local function pp(tbl) + for k,v in pairs(tbl) do print(k, tostring(v)) end +end + +local computer = agc("computer") +local fs = require("filesystem") +local kernel = require("kernel") +local crafter = agc("digitalCrafter") +local interface = agc("adapter") + +local loadedRecipes = load(fs:readAllText("./recipeParseLib.lua"),"recipeParseLib.lua")():getRecipes() + +print("Loaded the following recipes:") +pp(loadedRecipes) + +local meStockCache = {} -- dict; how much we have in storage minus what we have reserved as we go +local function me_GetStock(internalItemName) + if itemToCraftInternalName == internalItemName then return 0 end -- dont allow using the item that we want to craft + local cached = meStockCache[internalItemName] + if not cached then + cached = interface:getCount(internalItemName) + meStockCache[internalItemName] = cached + end + return cached +end + +local itemsToConsume = {} +local function me_ReserveItem(internalItemName, amount) + assert(meStockCache[internalItemName], "why didnt we look up the stock earlier???") + assert(meStockCache[internalItemName] >= amount, "there are not enough items available to reserve them. Tried to reserve "..internalItemName.." x"..tostring(amount)) + meStockCache[internalItemName] = meStockCache[internalItemName] - amount + itemsToConsume[internalItemName] = (itemsToConsume[internalItemName] or 0) + amount +end + +local function me_PushIntoCrafter(internalItemName, amount) + assert(meStockCache[internalItemName], "why didnt we look up the stock earlier???") + assert((itemsToConsume[internalItemName] or 0) >= amount, "we never reserved enough for "..internalItemName.." x"..tostring(amount)..". Why are we trying to extract it?") + itemsToConsume[internalItemName] = (itemsToConsume[internalItemName] or 0) - amount + interface:extract(internalItemName, 1, amount) +end + +local function tblToKeyStr(tbl) return table.concat(tbl,";") end +local function keyStrToTbl(keyStr) + local rv = {} + local buf = "" + for i = 1, #keyStr do + local chr = keyStr:sub(i,i) + if chr == ";" then + table.insert(rv, buf) + buf = "" + else + buf = buf .. chr + end + end + table.insert(rv, buf) + return rv +end + +local missingItems = {} -- dict +local currentItemsNeededToCraft = {} -- dict +local plannedCraftSteps = {} -- array<(tblToKeyStr({*ingredients, result}), amount:int)> +local function planCraftForItem(itemNameToCraft, amount) + local recipe = loadedRecipes[itemNameToCraft] + if not recipe then + print(("Could not find a recipe for %s"):format(itemNameToCraft)) + return amount + end -- we dont have a recipe and therefore cannot craft it + -- compute concrete recipe applications in the form [concreteIngredientName1-9] --> concreteOutputName + + local meAmount = me_GetStock(itemNameToCraft) + local meAmountToTake = math.min(meAmount, amount) + local itemsLeftToCraft = amount - meAmountToTake + local craftIterations = math.ceil(itemsLeftToCraft / recipe.resultCount) + print(("looks like we want to craft %s times a recipe yielding %sx %s as we want %s more items of that."):format(craftIterations, recipe.resultCount, itemNameToCraft, itemsLeftToCraft)) + while itemsLeftToCraft > 0 do + local recipeHasFailed = false + local allIngredients = {} + for slotIdx=1,9 do + local recipeItemOptions = recipe.slots[slotIdx] + if recipeItemOptions then + --print("option cnt: ", #recipeItemOptions) + -- try to find one item we have in stock and reserve this item + local itemWasFound = false + for _, value in ipairs(recipeItemOptions) do + if me_GetStock(value) > 0 then + me_ReserveItem(value, 1) + itemWasFound = true + allIngredients[slotIdx] = value + break + end + end + + if not itemWasFound then + -- if not, try to find one that we have an recipe for + local foundAny = false + for _, value in ipairs(recipeItemOptions) do + assert(value ~= "", "candidate was empty?") + if loadedRecipes[value] then -- if this item is craftable, queue it + currentItemsNeededToCraft[value] = (currentItemsNeededToCraft[value] or 0) + 1 + allIngredients[slotIdx] = value + foundAny = true + end + end + if not foundAny then + local missingKey = tblToKeyStr(recipeItemOptions) + missingItems[missingKey] = (missingItems[missingKey] or 0) + 1 + recipeHasFailed = true + end + end + else + allIngredients[slotIdx] = "nil" + end + end + -- store the recipe in the form tblToKeyStr({*ingredients, result}) inside plannedCraftSteps + if not recipeHasFailed then + print("--- concrete recipe: ---") + pp(allIngredients) + local key = tblToKeyStr(allIngredients)..";"..itemNameToCraft + + if false and plannedCraftSteps[#plannedCraftSteps][1] == key then + -- TODO can do an optimization here where we craft the item one more time at once, assuming all inputs and the output stack to big enough sizes + else + table.insert(plannedCraftSteps, {key, 1}) + end + end + itemsLeftToCraft = itemsLeftToCraft - recipe.resultCount + --print(("iteration finished, we need to craft %s more"):format(math.max(0,itemsLeftToCraft))) + end + print((next(missingItems) == nil) and "yay" or "uh oh looks like we are missing some items for this crafting step") + return 0 +end + + +local function safeCraft(...) + while true do + local cooldown = crafter.cooldownRemaining + if cooldown <= 0 then break end + sleep(cooldown) + end + return crafter:craft(...) +end + +------------------------------------------------------------------------------------------------------- + +local keyEvents = {} +kernel:registerEventCallback("charTyped", function(...) table.insert(keyEvents, {...}) end) +kernel:registerEventCallback("keyPressed", function(...) table.insert(keyEvents, {...}) end) + +---@param eventFilterOrNil "charTyped"|"keyPressed"|nil +local function waitForInputEvent(eventFilterOrNil) + sleep(0) + while #keyEvents == 0 do sleep(0.5) end + local res = table.remove(keyEvents, 1) + if eventFilterOrNil == nil or res[1] == eventFilterOrNil then + return res + else + return waitForInputEvent(eventFilterOrNil) + end +end + +print(("We want to craft %sx %s"):format(itemToCraftAmount, itemToCraftInternalName)) +currentItemsNeededToCraft[itemToCraftInternalName] = itemToCraftAmount + +while true do + print("new crafting iteration") + pp(currentItemsNeededToCraft) + local toCraft = next(currentItemsNeededToCraft) + if not toCraft then break end + -- try to obtain item "toCraft" + assert(type(toCraft) == "string") + local toCraft_amount = currentItemsNeededToCraft[toCraft] + local amountLeft = planCraftForItem(toCraft, toCraft_amount) + print("amount left in current step:", amountLeft) + assert(amountLeft <= toCraft_amount) + if amountLeft == toCraft_amount then -- if we made no progress, we neither have it in stock nor have a recipe for it --> mark it as missing + local tblKey = toCraft + missingItems[tblKey] = (missingItems[tblKey] or 0) + amountLeft + currentItemsNeededToCraft[toCraft] = nil + elseif amountLeft == 0 then + currentItemsNeededToCraft[toCraft] = nil + end +end + + +local canCraft = (next(missingItems) == nil) +if canCraft then + print("We have all the needed items. Do we start crafting? [y/n]. Will consume the following ingredients to craft "..itemToCraftAmount.." '"..itemToCraftInternalName.."':") + + local keys = {} + for k,_ in pairs(itemsToConsume) do table.insert(keys, k) end + table.sort(keys, function (x, y) return itemsToConsume[x] > itemsToConsume[y] end) + + local padsize = #tostring(itemsToConsume[keys[1]]) + for _, k in ipairs(keys) do + local v = itemsToConsume[k] + print(string.format("%sx%s: %s", v, string.rep(" ", padsize-#tostring(v)), k)) + end + + local doCraft = nil + while doCraft == nil do + local evnt = waitForInputEvent("charTyped") + print("event:", evnt) + local char = evnt[2]:lower() + if char == "y" or char == "\n" then + doCraft = true + elseif char == "n" then + doCraft = false + end + end + + if not doCraft then print("Aborted by user.") return end + print("Crafting...") + for i = #plannedCraftSteps, 1, -1 do + local currRecipeToCraft = plannedCraftSteps[i] + print(("Current step (%d/%d): %s"):format(#plannedCraftSteps-i+1, #plannedCraftSteps, currRecipeToCraft[1])) + local ingredientsAndResult = keyStrToTbl(currRecipeToCraft[1]) + for i = 1, #ingredientsAndResult do + if ingredientsAndResult[i] == "nil" then + ingredientsAndResult[i] = nil + elseif i < #ingredientsAndResult then -- for all ingredients + me_PushIntoCrafter(ingredientsAndResult[i], 1) + end + end + local resultName = ingredientsAndResult[#ingredientsAndResult] + local recipeResultCount = loadedRecipes[resultName].resultCount -- how many results we get from one craft + local recipeCraftCount = currRecipeToCraft[2] + safeCraft( + table.pack(table.unpack(ingredientsAndResult, 1, #ingredientsAndResult-1)), -- ingredients + resultName, -- result + recipeCraftCount, -- how often we craft it + false -- outputIntoOwnInventory + ) + print(("noting that we have crafted '%s'."):format(resultName)) + itemsToConsume[resultName] = (itemsToConsume[resultName] or 0) + recipeCraftCount * recipeResultCount -- account for intermediate crafting recipes + end + print(("Done, you are now the proud owner of %sx %s!"):format(itemToCraftAmount, itemToCraftInternalName)) +else + print("Cannot craft, we are missing the following items / recipes:") + for k,v in pairs(missingItems) do + print(string.format("%sx%s: %s", v, string.rep(" ", #tostring(v)), k)) + end +end + diff --git a/autocraft/recipeParseLib.lua b/autocraft/recipeParseLib.lua new file mode 100644 index 0000000..01e5b9f --- /dev/null +++ b/autocraft/recipeParseLib.lua @@ -0,0 +1,95 @@ +local rv = {} + +--[[ +local function tailcall() + print("called") + if true then + print("calling") + return tailcall() + end +end +tailcall() +]] + +local fs = require("filesystem") + +local function trim(str) return string.gsub(str,"^[%s]*", ""):gsub("[%s]*$", "") end + +function rv:getRecipes() + if not fs:fileExists("recipes.txt") then return {} end + + local recipeText = fs:readAllText("recipes.txt") + local splits = string.split(recipeText, ";") + local allRecipes = {} -- outputInternalName : {resultCount:int, slots:{{possible options.....},nil,nil,....}} + for _, value in ipairs(splits) do + local trimmed = trim(value) + if #(trimmed) > 0 then + --print("trimmed:", ">>"..trimmed.."<<") + local resCnt, resItem, ingredients = trimmed:match("^(%d*) (%S+) = ([^;]*)$") + assert(ingredients, ("failed to match ingredients for line: >>%s<< from >>%s<<"):format(trimmed, value)) + ingredients = trim(ingredients) + + --print(("Can craft %sx %s using ingredients %s"):format(resCnt, resItem, ingredients)) + assert(allRecipes[resItem] == nil, ("more than one recipe defined for %s"):format(resItem)) + local slotTable = {} + for slotIdx, optionsString in ipairs(ingredients:gsub("%s",""):split(",")) do + if optionsString == "nil" then + slotTable[slotIdx] = nil + else + local slotOptions = {} + for _, option in ipairs(optionsString:split("|")) do + assert((option ~= nil) and (option ~= "nil"), "somehow option was nil or nilstring") + assert(option:find(":"), string.format("slotoptions '%s' does not contain :",option)) + table.insert(slotOptions, option) + end + slotTable[slotIdx] = slotOptions + end + end + + allRecipes[resItem] = { + resultCount = assert(tonumber(resCnt), ("couldnt parse number '%s'"):format(resCnt)), + slots = slotTable, + resultItem = resItem + } + end + end + return allRecipes +end + +function rv:serializeSingleRecipe(recipe) + local slots = {} + for i = 1, 9 do + if recipe.slots[i] == nil or #(recipe.slots[i]) == 0 then + slots[i] = "nil" + else + slots[i] = table.concat(recipe.slots[i], "|") + end + end + return string.format("%d %s = %s", recipe.resultCount, recipe.resultItem, table.concat(slots,","))..";" +end + +local function serializeRecipes(recipeTable) + local newText = "" + for k,v in pairs(recipeTable) do + assert(k == v.resultItem, "somehow resultItem doesnt match the key") + newText = newText .. rv:serializeSingleRecipe(v) .. "\n" + end + return newText +end + +function rv:addNewRecipe(newRecipeTable) + local cnt = assert(newRecipeTable.resultCount, "resultCount not set") + local slots = assert(newRecipeTable.slots, "slots not set") + local resultItem = assert(newRecipeTable.resultItem, "resultItem not set") + + local currRecipes = rv:getRecipes() + assert(currRecipes[resultItem] == nil, ("recipe for %s has already been defined"):format(resultItem)) + currRecipes[resultItem] = { + resultCount = cnt, + slots = slots, + resultItem = resultItem + } + fs:writeAllText("recipes.txt", serializeRecipes(currRecipes)) +end + +return rv \ No newline at end of file diff --git a/autocraft/recipes.txt b/autocraft/recipes.txt new file mode 100644 index 0000000..00a4764 --- /dev/null +++ b/autocraft/recipes.txt @@ -0,0 +1,27 @@ +4 minecraft:oak_planks = minecraft:oak_log|minecraft:oak_wood|minecraft:stripped_oak_log|minecraft:stripped_oak_wood,nil,nil,nil,nil,nil,nil,nil,nil; +4 minecraft:torch = minecraft:charcoal|minecraft:coal,nil,nil,minecraft:stick,nil,nil,nil,nil,nil; +4 nuclearcraft:fission_reactor_casing = alltheores:lead_plate|electrodynamics:platelead|railcraft:lead_plate|nuclearcraft:lead_plate|chemlib:lead_plate|thermal:lead_plate,nuclearcraft:plate_advanced,alltheores:lead_plate|electrodynamics:platelead|railcraft:lead_plate|nuclearcraft:lead_plate|chemlib:lead_plate|thermal:lead_plate,nuclearcraft:plate_advanced,nil,nuclearcraft:plate_advanced,alltheores:lead_plate|electrodynamics:platelead|railcraft:lead_plate|nuclearcraft:lead_plate|chemlib:lead_plate|thermal:lead_plate,nuclearcraft:plate_advanced,alltheores:lead_plate|electrodynamics:platelead|railcraft:lead_plate|nuclearcraft:lead_plate|chemlib:lead_plate|thermal:lead_plate; +2 nuclearcraft:plate_advanced = nil,nuclearcraft:plate_basic,nil,nuclearcraft:tough_alloy_ingot,minecraft:redstone,nuclearcraft:tough_alloy_ingot,nil,nuclearcraft:plate_basic,nil; +2 nuclearcraft:plate_basic = alltheores:lead_ingot|electrodynamics:ingotlead|railcraft:lead_ingot|mekanism:ingot_lead|nuclearcraft:lead_ingot|chemlib:lead_ingot|thermal:lead_ingot,nuclearcraft:graphite_dust|chemlib:graphite_dust,nil,nuclearcraft:graphite_dust|chemlib:graphite_dust,alltheores:lead_ingot|electrodynamics:ingotlead|railcraft:lead_ingot|mekanism:ingot_lead|nuclearcraft:lead_ingot|chemlib:lead_ingot|thermal:lead_ingot,nil,nil,nil,nil; +1 nuclearcraft:empty_heat_sink = nuclearcraft:tough_alloy_ingot,nuclearcraft:thermoconducting_plate,nuclearcraft:tough_alloy_ingot,minecraft:iron_bars,minecraft:bucket,minecraft:iron_bars,nuclearcraft:tough_alloy_ingot,nuclearcraft:thermoconducting_plate,nuclearcraft:tough_alloy_ingot; +1 minecraft:bucket = minecraft:iron_ingot,nil,minecraft:iron_ingot,nil,minecraft:iron_ingot,nil,nil,nil,nil; +16 minecraft:iron_bars = minecraft:iron_ingot,minecraft:iron_ingot,minecraft:iron_ingot,minecraft:iron_ingot,minecraft:iron_ingot,minecraft:iron_ingot,nil,nil,nil; +1 nuclearcraft:empty_active_heat_sink = nuclearcraft:thermoconducting_plate,nuclearcraft:motor,nuclearcraft:thermoconducting_plate,nuclearcraft:motor,nuclearcraft:empty_heat_sink,nuclearcraft:motor,nuclearcraft:thermoconducting_plate,nuclearcraft:motor,nuclearcraft:thermoconducting_plate; +1 nuclearcraft:motor = alltheores:steel_ingot|electrodynamics:ingotsteel|railcraft:steel_ingot|ad_astra:steel_ingot|mekanism:ingot_steel|nuclearcraft:steel_ingot|thermal:steel_ingot,alltheores:steel_ingot|electrodynamics:ingotsteel|railcraft:steel_ingot|ad_astra:steel_ingot|mekanism:ingot_steel|nuclearcraft:steel_ingot|thermal:steel_ingot,minecraft:gold_nugget,nuclearcraft:coil_copper,nuclearcraft:coil_copper,minecraft:iron_ingot,alltheores:steel_ingot|electrodynamics:ingotsteel|railcraft:steel_ingot|ad_astra:steel_ingot|mekanism:ingot_steel|nuclearcraft:steel_ingot|thermal:steel_ingot,alltheores:steel_ingot|electrodynamics:ingotsteel|railcraft:steel_ingot|ad_astra:steel_ingot|mekanism:ingot_steel|nuclearcraft:steel_ingot|thermal:steel_ingot,minecraft:gold_nugget; +1 nuclearcraft:coil_copper = minecraft:copper_ingot,minecraft:copper_ingot,nil,minecraft:iron_ingot,minecraft:iron_ingot,nil,minecraft:copper_ingot,minecraft:copper_ingot,nil; +9 minecraft:gold_nugget = minecraft:gold_ingot,nil,nil,nil,nil,nil,nil,nil,nil; +1 nuclearcraft:helium_collector = nuclearcraft:plate_advanced,nuclearcraft:thorium_ingot|chemlib:thorium_ingot,nuclearcraft:plate_advanced,nuclearcraft:thorium_plate|chemlib:thorium_plate,nuclearcraft:motor,nuclearcraft:thorium_plate|chemlib:thorium_plate,nuclearcraft:plate_advanced,nuclearcraft:thorium_ingot|chemlib:thorium_ingot,nuclearcraft:plate_advanced; +1 nuclearcraft:pump = nuclearcraft:plate_basic,nuclearcraft:motor,nuclearcraft:plate_basic,nuclearcraft:plate_basic,minecraft:cauldron,nuclearcraft:plate_basic,nuclearcraft:plate_basic,nuclearcraft:motor,nuclearcraft:plate_basic; +1 minecraft:cauldron = minecraft:iron_ingot,nil,minecraft:iron_ingot,minecraft:iron_ingot,nil,minecraft:iron_ingot,minecraft:iron_ingot,minecraft:iron_ingot,minecraft:iron_ingot; +1 nuclearcraft:fission_reactor_controller = nuclearcraft:fission_reactor_casing,nuclearcraft:plate_advanced,nuclearcraft:fission_reactor_casing,nuclearcraft:basic_electric_circuit,nuclearcraft:decay_hastener,nuclearcraft:basic_electric_circuit,nuclearcraft:fission_reactor_casing,nuclearcraft:plate_advanced,nuclearcraft:fission_reactor_casing; +1 nuclearcraft:decay_hastener = nuclearcraft:boron_ingot,nuclearcraft:boron_block,nuclearcraft:boron_ingot,nuclearcraft:tough_alloy_ingot,nuclearcraft:tin_silver_ingot,nuclearcraft:tough_alloy_ingot,nuclearcraft:boron_ingot,nuclearcraft:actuator,nuclearcraft:boron_ingot; +1 nuclearcraft:actuator = nil,nil,alltheores:steel_ingot|electrodynamics:ingotsteel|railcraft:steel_ingot|ad_astra:steel_ingot|mekanism:ingot_steel|nuclearcraft:steel_ingot|thermal:steel_ingot,nuclearcraft:ferroboron_ingot,minecraft:piston,nil,minecraft:copper_ingot,nuclearcraft:ferroboron_ingot,nil; +1 nuclearcraft:boron_block = nuclearcraft:boron_ingot,nuclearcraft:boron_ingot,nuclearcraft:boron_ingot,nuclearcraft:boron_ingot,nuclearcraft:boron_ingot,nuclearcraft:boron_ingot,nuclearcraft:boron_ingot,nuclearcraft:boron_ingot,nuclearcraft:boron_ingot; +1 minecraft:piston = minecraft:oak_planks|minecraft:spruce_planks|minecraft:birch_planks|minecraft:jungle_planks|minecraft:acacia_planks|minecraft:dark_oak_planks|minecraft:crimson_planks|minecraft:warped_planks|minecraft:mangrove_planks|minecraft:bamboo_planks|minecraft:cherry_planks|botania:livingwood_planks|botania:mossy_livingwood_planks|botania:framed_livingwood|botania:pattern_framed_livingwood|botania:dreamwood_planks|botania:mossy_dreamwood_planks|botania:framed_dreamwood|botania:pattern_framed_dreamwood|botania:shimmerwood_planks|biomesoplenty:fir_planks|biomesoplenty:pine_planks|biomesoplenty:maple_planks|biomesoplenty:redwood_planks|biomesoplenty:mahogany_planks|biomesoplenty:jacaranda_planks|biomesoplenty:palm_planks|biomesoplenty:willow_planks|biomesoplenty:dead_planks|biomesoplenty:magic_planks|biomesoplenty:umbran_planks|biomesoplenty:hellbark_planks|biomesoplenty:empyreal_planks|integrateddynamics:menril_planks|ad_astra:aeronos_planks|ad_astra:strophar_planks|ad_astra:glacian_planks|voidscape:thunder_planks|undergarden:smogstem_planks|undergarden:wigglewood_planks|undergarden:grongle_planks|thermal:rubberwood_planks,minecraft:oak_planks|minecraft:spruce_planks|minecraft:birch_planks|minecraft:jungle_planks|minecraft:acacia_planks|minecraft:dark_oak_planks|minecraft:crimson_planks|minecraft:warped_planks|minecraft:mangrove_planks|minecraft:bamboo_planks|minecraft:cherry_planks|botania:livingwood_planks|botania:mossy_livingwood_planks|botania:framed_livingwood|botania:pattern_framed_livingwood|botania:dreamwood_planks|botania:mossy_dreamwood_planks|botania:framed_dreamwood|botania:pattern_framed_dreamwood|botania:shimmerwood_planks|biomesoplenty:fir_planks|biomesoplenty:pine_planks|biomesoplenty:maple_planks|biomesoplenty:redwood_planks|biomesoplenty:mahogany_planks|biomesoplenty:jacaranda_planks|biomesoplenty:palm_planks|biomesoplenty:willow_planks|biomesoplenty:dead_planks|biomesoplenty:magic_planks|biomesoplenty:umbran_planks|biomesoplenty:hellbark_planks|biomesoplenty:empyreal_planks|integrateddynamics:menril_planks|ad_astra:aeronos_planks|ad_astra:strophar_planks|ad_astra:glacian_planks|voidscape:thunder_planks|undergarden:smogstem_planks|undergarden:wigglewood_planks|undergarden:grongle_planks|thermal:rubberwood_planks,minecraft:oak_planks|minecraft:spruce_planks|minecraft:birch_planks|minecraft:jungle_planks|minecraft:acacia_planks|minecraft:dark_oak_planks|minecraft:crimson_planks|minecraft:warped_planks|minecraft:mangrove_planks|minecraft:bamboo_planks|minecraft:cherry_planks|botania:livingwood_planks|botania:mossy_livingwood_planks|botania:framed_livingwood|botania:pattern_framed_livingwood|botania:dreamwood_planks|botania:mossy_dreamwood_planks|botania:framed_dreamwood|botania:pattern_framed_dreamwood|botania:shimmerwood_planks|biomesoplenty:fir_planks|biomesoplenty:pine_planks|biomesoplenty:maple_planks|biomesoplenty:redwood_planks|biomesoplenty:mahogany_planks|biomesoplenty:jacaranda_planks|biomesoplenty:palm_planks|biomesoplenty:willow_planks|biomesoplenty:dead_planks|biomesoplenty:magic_planks|biomesoplenty:umbran_planks|biomesoplenty:hellbark_planks|biomesoplenty:empyreal_planks|integrateddynamics:menril_planks|ad_astra:aeronos_planks|ad_astra:strophar_planks|ad_astra:glacian_planks|voidscape:thunder_planks|undergarden:smogstem_planks|undergarden:wigglewood_planks|undergarden:grongle_planks|thermal:rubberwood_planks,minecraft:cobblestone,minecraft:iron_ingot,minecraft:cobblestone,minecraft:cobblestone,minecraft:redstone,minecraft:cobblestone; +1 nuclearcraft:fission_reactor_port = nuclearcraft:tough_alloy_plate,nuclearcraft:plate_advanced,nuclearcraft:tough_alloy_plate,nuclearcraft:servo,nuclearcraft:fission_reactor_casing,nuclearcraft:servo,nuclearcraft:tough_alloy_plate,nuclearcraft:plate_advanced,nuclearcraft:tough_alloy_plate; +1 nuclearcraft:servo = nuclearcraft:ferroboron_ingot,nil,nuclearcraft:ferroboron_ingot,minecraft:redstone,alltheores:steel_ingot|electrodynamics:ingotsteel|railcraft:steel_ingot|ad_astra:steel_ingot|mekanism:ingot_steel|nuclearcraft:steel_ingot|thermal:steel_ingot,minecraft:redstone,alltheores:steel_ingot|electrodynamics:ingotsteel|railcraft:steel_ingot|ad_astra:steel_ingot|mekanism:ingot_steel|nuclearcraft:steel_ingot|thermal:steel_ingot,minecraft:copper_ingot,alltheores:steel_ingot|electrodynamics:ingotsteel|railcraft:steel_ingot|ad_astra:steel_ingot|mekanism:ingot_steel|nuclearcraft:steel_ingot|thermal:steel_ingot; +1 mekanism:quantum_entangloporter = mekanism:ingot_refined_obsidian,electrodynamics:circuitultimate|mekanism:ultimate_control_circuit,mekanism:ingot_refined_obsidian,mekanism:alloy_atomic,mekanism:teleportation_core,mekanism:alloy_atomic,mekanism:ingot_refined_obsidian,electrodynamics:circuitultimate|mekanism:ultimate_control_circuit,mekanism:ingot_refined_obsidian; +1 mekanism:teleportation_core = minecraft:lapis_lazuli,mekanism:alloy_atomic,minecraft:lapis_lazuli,minecraft:gold_ingot,minecraft:diamond,minecraft:gold_ingot,minecraft:lapis_lazuli,mekanism:alloy_atomic,minecraft:lapis_lazuli; +1 mekanism:ultimate_control_circuit = mekanism:alloy_atomic,electrodynamics:circuitelite|mekanism:elite_control_circuit,mekanism:alloy_atomic,nil,nil,nil,nil,nil,nil; +1 mekanism:advanced_control_circuit = mekanism:alloy_infused,electrodynamics:circuitbasic|mekanism:basic_control_circuit,mekanism:alloy_infused,nil,nil,nil,nil,nil,nil; +1 mekanism:elite_control_circuit = mekanism:alloy_reinforced,electrodynamics:circuitadvanced|mekanism:advanced_control_circuit,mekanism:alloy_reinforced,nil,nil,nil,nil,nil,nil;