Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- PPV crafty turtle, CC: Restitched 1.100.8.
- -- One craft per batch; ordinary ingredients; no recipe remainders.
- local GRID = {1, 2, 3, 5, 6, 7, 9, 10, 11}
- local IN_GRID = {}
- for _, slot in ipairs(GRID) do
- IN_GRID[slot] = true
- end
- local FILE = "/ppv-memory."
- local db, fault, page = nil, nil, nil
- local outputBlocked = false
- local buttons, lastKey, lastChange = {}, nil, os.clock()
- local function snapshot()
- local result = {}
- for slot = 1, 16 do
- local item = turtle.getItemDetail(slot)
- if item then
- result[slot] = {
- name = item.name,
- count = item.count
- }
- end
- end
- return result
- end
- local function totals(inv)
- local result, count = {}, 0
- for _, item in pairs(inv) do
- result[item.name] = (result[item.name] or 0) + item.count
- count = count + item.count
- end
- return result, count
- end
- local function key(inv)
- local amounts = totals(inv)
- local names, parts = {}, {}
- for name in pairs(amounts) do
- names[#names + 1] = name
- end
- table.sort(names)
- for _, name in ipairs(names) do
- parts[#parts + 1] =
- #name .. ":" .. name .. "=" .. amounts[name]
- end
- return table.concat(parts, ";")
- end
- local function layoutInventory(layout)
- local inv = {}
- for index = 1, 9 do
- if layout[index] ~= false then
- inv[GRID[index]] = {
- name = layout[index],
- count = 1
- }
- end
- end
- return inv
- end
- local function validDatabase(value)
- if type(value) ~= "table"
- or value.version ~= 1
- or type(value.revision) ~= "number"
- or value.revision < 0
- or value.revision % 1 ~= 0
- or type(value.recipes) ~= "table"
- or #value.recipes > 9 then
- return false
- end
- local function validLayout(layout)
- if type(layout) ~= "table" then
- return false
- end
- for index = 1, 9 do
- if layout[index] ~= false
- and type(layout[index]) ~= "string" then
- return false
- end
- end
- return true
- end
- for _, recipe in ipairs(value.recipes) do
- if type(recipe) ~= "table"
- or not validLayout(recipe.layout)
- or recipe.key ~= key(layoutInventory(recipe.layout))
- or type(recipe.output) ~= "table"
- or type(recipe.output.name) ~= "string"
- or type(recipe.output.count) ~= "number" then
- return false
- end
- end
- local p = value.pending
- if p then
- if type(p) ~= "table"
- or type(p.key) ~= "string"
- or not ({
- learn = true,
- arrange = true,
- craft = true,
- output = true
- })[p.mode] then
- return false
- end
- if p.mode ~= "learn"
- and (
- not validLayout(p.layout)
- or p.key ~= key(layoutInventory(p.layout))
- ) then
- return false
- end
- if p.mode == "output"
- and (
- type(p.output) ~= "table"
- or type(p.output.name) ~= "string"
- or type(p.output.count) ~= "number"
- ) then
- return false
- end
- end
- return true
- end
- local function loadMemory()
- local newest, found = nil, false
- for index = 0, 1 do
- local path = FILE .. index
- if fs.exists(path) then
- found = true
- local ok, value = pcall(function()
- local handle = assert(
- fs.open(path, "r"),
- "Cannot read memory"
- )
- local data = handle.readAll()
- handle.close()
- return textutils.unserialize(data)
- end)
- if ok and validDatabase(value)
- and (
- not newest
- or value.revision > newest.revision
- ) then
- newest = value
- end
- end
- end
- assert(
- newest or not found,
- "Memory files unreadable: do not overwrite them"
- )
- return newest or {
- version = 1,
- revision = 0,
- recipes = {}
- }
- end
- local function saveMemory()
- -- CC rejects repeated references to the same table.
- -- Expand them into independent copies before serialization.
- local function copy(value)
- if type(value) ~= "table" then
- return value
- end
- local result = {}
- for k, v in pairs(value) do
- result[k] = copy(v)
- end
- return result
- end
- -- Alternate files preserve the previous complete revision.
- local revision = db.revision + 1
- db.revision = revision
- local handle = assert(
- fs.open(FILE .. (revision % 2), "w"),
- "Cannot save memory"
- )
- handle.write(textutils.serialize(copy(db)))
- handle.close()
- end
- local function findRecipe(ingredientKey)
- for index, recipe in ipairs(db.recipes) do
- if recipe.key == ingredientKey then
- return index, recipe
- end
- end
- end
- local function taughtLayout(inv, ingredientKey)
- assert(
- key(inv) == ingredientKey,
- "Ingredients changed: restore the original batch"
- )
- local layout = {}
- for slot, item in pairs(inv) do
- assert(
- IN_GRID[slot],
- "Move all items into the displayed crafting grid"
- )
- assert(
- item.count == 1,
- "Place exactly ONE ingredient in each occupied grid slot"
- )
- end
- for index, slot in ipairs(GRID) do
- layout[index] = inv[slot] and inv[slot].name or false
- end
- return layout
- end
- local function arrange(layout, ingredientKey)
- assert(
- key(snapshot()) == ingredientKey,
- "Ingredients changed"
- )
- local fixed = {}
- local function move(from, to, count)
- turtle.select(from)
- assert(
- turtle.transferTo(to, count),
- "Inventory transfer failed"
- )
- end
- local function emptySlot(exclude)
- for slot = 1, 16 do
- if slot ~= exclude
- and not fixed[slot]
- and turtle.getItemCount(slot) == 0 then
- return slot
- end
- end
- error("No free rearrangement slot")
- end
- for index, target in ipairs(GRID) do
- local want = layout[index]
- local current = turtle.getItemDetail(target)
- if current and current.name ~= want then
- move(target, emptySlot(target), current.count)
- current = nil
- end
- if want then
- if current and current.count > 1 then
- move(
- target,
- emptySlot(target),
- current.count - 1
- )
- elseif not current then
- local source
- for slot = 1, 16 do
- local item = turtle.getItemDetail(slot)
- if slot ~= target
- and not fixed[slot]
- and item
- and item.name == want then
- source = slot
- break
- end
- end
- assert(source, "Missing ingredient: " .. want)
- move(source, target, 1)
- end
- end
- fixed[target] = true
- end
- local actual = taughtLayout(snapshot(), ingredientKey)
- for index = 1, 9 do
- assert(
- actual[index] == layout[index],
- "Arranged pattern does not match memory"
- )
- end
- end
- local function craftBatch()
- local p = db.pending
- taughtLayout(snapshot(), p.key)
- local possible, reason = turtle.craft(0)
- assert(
- possible,
- reason or "This arrangement is not a crafting recipe"
- )
- assert(
- peripheral.hasType("top", "inventory"),
- "Put an inventory chest above the turtle"
- )
- turtle.select(16)
- p.mode = "craft"
- -- Persist before consuming ingredients.
- -- An interrupted craft must not be repeated blindly.
- saveMemory()
- local ok, why = turtle.craft(1)
- if not ok then
- p.mode = p.recipeIndex and "arrange" or "learn"
- saveMemory()
- error(why or "Craft failed")
- end
- local inv = snapshot()
- assert(
- inv[16],
- "Result was not placed in slot 16: inspect the inventory"
- )
- for slot in pairs(inv) do
- assert(
- slot == 16,
- "Unexpected ingredients or remainders: inspect the inventory"
- )
- end
- local result = turtle.getItemDetail(16, true)
- p.output = {
- name = result.name,
- count = result.count,
- label = result.displayName or result.name
- }
- if not p.recipeIndex then
- assert(
- #db.recipes < 9,
- "All nine memory slots are occupied"
- )
- db.recipes[#db.recipes + 1] = {
- key = p.key,
- layout = p.layout,
- output = p.output
- }
- p.recipeIndex = #db.recipes
- else
- local expected = db.recipes[p.recipeIndex].output
- assert(
- result.name == expected.name
- and result.count == expected.count,
- "Recipe output changed: inspect the inventory"
- )
- end
- p.mode = "output"
- saveMemory()
- end
- local function outputBatch()
- local p = db.pending
- local item = turtle.getItemDetail(16)
- if not item then
- db.pending = nil
- saveMemory()
- outputBlocked = false
- lastKey, lastChange = nil, os.clock()
- return
- end
- assert(
- item.name == p.output.name
- and item.count <= p.output.count,
- "Unexpected item in output slot 16"
- )
- assert(
- peripheral.hasType("top", "inventory"),
- "Output chest is missing"
- )
- turtle.select(16)
- -- If the chest is full, keep the result and retry later.
- turtle.dropUp(item.count)
- outputBlocked = turtle.getItemCount(16) > 0
- end
- local function tick()
- local p = db.pending
- if p then
- if p.mode == "arrange" then
- arrange(p.layout, p.key)
- craftBatch()
- elseif p.mode == "output" then
- outputBatch()
- elseif p.mode == "craft" then
- error(
- "Interrupted craft: empty the turtle, then click Retry"
- )
- end
- return
- end
- local inv = snapshot()
- local ingredientKey = key(inv)
- if ingredientKey ~= lastKey then
- lastKey, lastChange = ingredientKey, os.clock()
- return
- end
- if ingredientKey == ""
- or os.clock() - lastChange < 1 then
- return
- end
- local _, count = totals(inv)
- assert(
- count <= 9,
- "More than nine ingredients: check the PPV batch size"
- )
- local index, recipe = findRecipe(ingredientKey)
- assert(
- recipe or #db.recipes < 9,
- "Memory full: remove this unknown batch"
- )
- db.pending = {
- key = ingredientKey,
- mode = recipe and "arrange" or "learn",
- recipeIndex = index,
- layout = recipe and recipe.layout or nil
- }
- saveMemory()
- end
- local function confirm()
- assert(
- db.pending and db.pending.mode == "learn",
- "No recipe awaiting confirmation"
- )
- db.pending.layout = taughtLayout(
- snapshot(),
- db.pending.key
- )
- craftBatch()
- end
- local function retry()
- if db.pending and db.pending.mode == "craft" then
- assert(
- key(snapshot()) == "",
- "Inspect and empty the turtle before resetting"
- )
- db.pending = nil
- saveMemory()
- elseif not db.pending then
- lastKey, lastChange = nil, os.clock()
- end
- fault = nil
- end
- local function safely(action)
- local ok, message = pcall(action)
- if not ok then
- fault, page = tostring(message), "error"
- end
- end
- local function draw()
- local width, height = term.getSize()
- buttons = {}
- term.setBackgroundColor(colors.black)
- term.setTextColor(colors.white)
- term.clear()
- local function text(x, y, value, color, limit)
- if y < 1 or y > height then
- return
- end
- term.setCursorPos(x, y)
- term.setTextColor(color or colors.white)
- term.write(
- tostring(value):sub(
- 1,
- math.min(limit or width, width - x + 1)
- )
- )
- end
- local function button(x, label, action, color)
- text(x, height, label, color or colors.cyan)
- buttons[#buttons + 1] = {
- x = x,
- y = height,
- w = #label,
- action = action
- }
- end
- local inv = snapshot()
- local p = db.pending
- text(
- 1,
- 1,
- "PPV Crafter Memory: " .. #db.recipes .. "/9",
- colors.yellow
- )
- if page == "error" then
- text(
- 1,
- 2,
- "Paused - " .. (p and p.mode or "intake"),
- colors.red
- )
- local message = fault or "Unknown error"
- for row = 3, height - 1 do
- text(1, row, message:sub(1, width))
- message = message:sub(width + 1)
- end
- button(1, "[ Retry ]", function()
- retry()
- page = nil
- end)
- elseif page then
- local recipe =
- type(page) == "number" and db.recipes[page] or nil
- local items =
- recipe and layoutInventory(recipe.layout) or inv
- local amounts = totals(items)
- local names = {}
- for name in pairs(amounts) do
- names[#names + 1] = name
- end
- table.sort(names)
- text(
- 1,
- 2,
- recipe
- and (
- recipe.output.count .. " x "
- .. recipe.output.name
- )
- or "Current ingredients"
- )
- for index, name in ipairs(names) do
- text(
- 1,
- index + 3,
- amounts[name] .. " x " .. name
- )
- end
- button(1, "[ Back ]", function()
- page = nil
- end)
- else
- local status = fault and "Paused"
- or not p and "Waiting for batch"
- or ({
- learn = "Arrange, then confirm",
- arrange = "Arranging",
- craft = "Crafting",
- output = outputBlocked
- and "Output chest blocked"
- or "Sending output"
- })[p.mode]
- text(
- 1,
- 2,
- status,
- fault and colors.red or colors.lime
- )
- text(1, 3, "Crafting grid")
- text(1, 7, "1 item per cell", colors.gray, 15)
- for index, slot in ipairs(GRID) do
- local x = 1 + ((index - 1) % 3) * 5
- local y = 4 + math.floor((index - 1) / 3)
- text(
- x,
- y,
- string.format("[%02d]", slot),
- colors.gray
- )
- local item = inv[slot]
- text(
- x,
- y + 4,
- item
- and item.name:match("[^:]+$"):sub(1, 4)
- or "----",
- colors.cyan,
- 4
- )
- end
- local outside = 0
- for slot, item in pairs(inv) do
- if not IN_GRID[slot] then
- outside = outside + item.count
- end
- end
- text(
- 1,
- 11,
- "Outside: " .. outside,
- colors.gray,
- 15
- )
- local split = 17
- for index = 1, 9 do
- local recipe = db.recipes[index]
- text(
- split,
- index + 2,
- index .. " " .. (
- recipe
- and (
- recipe.output.count .. "x "
- .. recipe.output.label
- )
- or "-"
- ),
- recipe and colors.white or colors.gray
- )
- if recipe then
- buttons[#buttons + 1] = {
- x = split,
- y = index + 2,
- w = width - split + 1,
- action = function()
- page = index
- end
- }
- end
- end
- if fault then
- button(
- 1,
- "[ Error ]",
- function()
- page = "error"
- end,
- colors.red
- )
- elseif p and p.mode == "learn" then
- button(
- 1,
- "[ Confirm ]",
- confirm,
- colors.lime
- )
- end
- button(
- 16,
- "[ Ingredients ]",
- function()
- page = "ingredients"
- end
- )
- end
- end
- -- Start program.
- assert(
- turtle and turtle.craft,
- "Run this on a crafty turtle"
- )
- assert(
- term.isColor(),
- "Use an advanced turtle"
- )
- db = loadMemory()
- if db.pending and db.pending.mode == "craft" then
- fault =
- "Interrupted craft: inspect and empty the turtle, then click Retry"
- page = "error"
- end
- draw()
- local timer = os.startTimer(0.25)
- while true do
- local event, a, x, y = os.pullEvent()
- if event == "timer" and a == timer then
- if not fault then
- safely(tick)
- end
- draw()
- -- Schedule only after turtle operations and redraw finish.
- timer = os.startTimer(0.25)
- elseif event == "mouse_click" and a == 1 then
- for _, hit in ipairs(buttons) do
- if y == hit.y
- and x >= hit.x
- and x < hit.x + hit.w then
- os.cancelTimer(timer)
- safely(hit.action)
- draw()
- -- Blocking turtle calls can consume an existing timer.
- -- Always start a fresh one after the action completes.
- timer = os.startTimer(0.25)
- break
- end
- end
- elseif event == "turtle_inventory"
- or event == "term_resize" then
- os.cancelTimer(timer)
- draw()
- timer = os.startTimer(0.25)
- end
- end
Add Comment
Please, Sign In to add comment