Automatic item sorter
Empty an input chest into storage chests on a Data Cable, by item, by mod or by the end of the id, with an overflow chest and a status screen.
Every base ends with a chest full of everything: what the Mechanical Drills dug, what the farms dropped, what you brought back from a trip. Create can sort with filters on funnels and Smart Chutes, but each filter takes one item, and changing the layout means rebuilding. A computer on a Data Cable can sort with rules: "everything that ends with _ore goes to the ores chest", "everything from Create goes to the parts chest", and change them by editing a table.
The program empties an input chest into storage chests, one stack at a time, choosing the chest with the first rule that matches the item. What no rule wants, and what does not fit in a full chest, goes to an overflow chest. A screen shows each chest with its rules, how many items it received and whether it is full.
What you need
- A Minicomputer or newer. The storage chests are reached through a Data Cable, and only the Minicomputer, the Personal Computer, the Microcontroller and the Modern Computer have a network port. A Personal Computer shows the colours of the screen below; a Minicomputer shows it in amber.
- Data Cable, running along all the chests. Every block touching the cable becomes a peripheral named after its type and position:
inventory@12,64,-3for a chest at x 12, y 64, z -3 (F3 shows the coordinates of the block you look at). - An input chest touching the cable (or the computer), fed by hand, by a hopper or by a Create funnel at the end of a belt.
- Storage chests, one per category, plus one overflow chest. Barrels are handy: two chests side by side would merge into a double chest. A Create Item Vault makes a large overflow.
Seen from above, one block per cell:
+------+------+------+------+------+
| ores |ingots|create| wood | over-| storage barrels (the last one: overflow)
| | | | | flow |
+------+------+------+------+------+
| = | = | = | = | = | Data Cable, touching every barrel
+------+------+------+------+------+
| Mini | IN | Minicomputer and input chest,
+------+------+ both against the cableThe program, step by step
1. The chests and the rules
CHESTS gives each storage chest a short name for the screen, and its peripheral name on the cable. RULES is read from top to bottom, and the first rule that matches wins. A rule is one of three forms:
| rule | matches |
|---|---|
"minecraft:iron_ingot" | this item only |
"create:*" | every id that starts with create:: the whole Create mod |
"*_ore" | every id that ends with _ore: iron ore, deepslate gold ore, zinc ore... |
local INPUT = "inventory@11,64,-2" -- the chest you fill: a cable name or a side
local CHESTS = { -- short name = cable name of the chest
ores = "inventory@10,64,-4",
ingots = "inventory@11,64,-4",
create = "inventory@12,64,-4",
wood = "inventory@13,64,-4",
overflow = "inventory@14,64,-4",
}
local OVERFLOW = "overflow" -- where items without a rule go
local RULES = {
{match = "minecraft:raw_*", to = "ores"},
{match = "*_ore", to = "ores"},
{match = "*_ingot", to = "ingots"},
{match = "*_nugget", to = "ingots"},
{match = "create:*", to = "create"},
{match = "*_log", to = "wood"},
{match = "*_planks", to = "wood"},
}
local IDLE = 1 -- seconds between two looks at an empty input chest
local BUSY = 0.25 -- seconds between two passes while items keep comingThe order matters: create:brass_ingot matches both *_ingot and create:*, and goes to ingots because that rule comes first. Put the precise rules at the top and the broad ones below.
2. Matching an item
string.starts and string.ends do the work. A star at the start of a rule means "ends with", a star at the end means "starts with":
local function matches(rule, id)
if rule:starts("*") then return id:ends(rule:sub(2)) end
if rule:ends("*") then return id:starts(rule:sub(1, -2)) end
return id == rule
end
local function destination(id)
for _, rule in ipairs(RULES) do
if matches(rule.match, id) then return rule.to end
end
return OVERFLOW
endrule:sub(2) is the rule without its first character, rule:sub(1, -2) without its last one (a negative position counts from the end).
local function matches(rule, id)
if rule:starts("*") then return id:ends(rule:sub(2)) end
if rule:ends("*") then return id:starts(rule:sub(1, -2)) end
return id == rule
end
print(matches("*_ore", "minecraft:deepslate_iron_ore"))
print(matches("create:*", "create:andesite_alloy"))
print(matches("create:*", "minecraft:andesite"))true true false
3. Checking the settings
A typing mistake in a table should stop the program at once with a clear message, not sort half of the items into nowhere. setup wraps the input chest, checks that every rule names a known chest, notes which chests are missing, and prepares the text of the rules for the screen:
local input = nil
local moved = {} -- chest name -> items sent there since the start
local state = {} -- chest name -> "ok", "FULL" or "MISSING"
local rules_of = {} -- chest name -> its rules, as text for the screen
local function setup()
input = peripheral.wrap(INPUT)
if input == nil or input.push == nil then
error("no input chest '" .. INPUT .. "'")
end
if CHESTS[OVERFLOW] == nil then
error("the overflow chest '" .. OVERFLOW .. "' is not in CHESTS")
end
for _, rule in ipairs(RULES) do
if CHESTS[rule.to] == nil then
error("rule '" .. rule.match .. "' sends to '" .. rule.to .. "', which is not in CHESTS")
end
if rules_of[rule.to] == nil then
rules_of[rule.to] = rule.match
else
rules_of[rule.to] = rules_of[rule.to] .. ", " .. rule.match
end
end
for key, name in pairs(CHESTS) do
moved[key] = 0
if peripheral.type(name) == nil then state[key] = "MISSING" else state[key] = "ok" end
if rules_of[key] == nil then rules_of[key] = "(no rule)" end
end
if rules_of[OVERFLOW] == "(no rule)" then rules_of[OVERFLOW] = "everything else" end
endpairs walks the named entries of a table in the order they were written, so the screen lists the chests in the order of CHESTS.
4. Moving a stack: push
The heart of the sorter is input.push(target, slot, count) (inventory.push()). Its rules are worth knowing well:
- It is called on the source inventory, and the target is the name of another peripheral of the same computer (a side like
"right", or a cable name), not a wrapped object. slotlimits the move to that slot of the source;countis the most items to move (64 when left out).- It returns how many items actually moved:
0when the target is full, fewer than asked when it fills up on the way. Nothing is lost: what does not fit stays in the source. - When the name leads to no inventory (a chest broken, a typo, a chunk unloaded), it raises the error
no inventory 'inventory@14,64,-4'.
So push_to calls it through pcall, and turns the result into the state of the chest:
local function push_to(key, slot, count)
local r = pcall(input.push, CHESTS[key], slot, count)
if not r.ok then
state[key] = "MISSING"
return 0
end
if r.value < count then state[key] = "FULL" else state[key] = "ok" end
moved[key] = moved[key] + r.value
return r.value
end5. One sorting pass
A pass reads the input chest once with input.list(), then sends each stack to its chest. What the chosen chest refuses (full or missing) gets a second chance in the overflow chest. Stacks that still stay are counted as "stuck": both their chest and the overflow are full.
local total = 0 -- items sorted since the start
local waiting = 0 -- stacks left in the input after the last pass
local input_ok = true
local last = "nothing yet"
local function sort_pass()
local r = pcall(input.list)
input_ok = r.ok
if not r.ok then return 0 end
local done = 0
waiting = 0
for slot, stack in pairs(r.value) do
local key = destination(stack.name)
local n = push_to(key, slot, stack.count)
local where = key
if n < stack.count and key ~= OVERFLOW then
local extra = push_to(OVERFLOW, slot, stack.count - n)
if extra > 0 then where = OVERFLOW end
n = n + extra
end
if n > 0 then last = n .. " x " .. stack.name .. " -> " .. where end
if n < stack.count then waiting = waiting + 1 end
done = done + n
end
total = total + done
return done
endinput.list() returns the stacks indexed by slot number (empty slots are missing), which is why the loop uses pairs, and why slot can be handed straight to push.
6. The screen and the loop
The screen lists the chests, then the state of the input and the last move. A click anywhere pauses or resumes the sorting, which is handy while you rearrange the chests.
local paused = false
local function draw()
term.set_bg(term.colors.black)
term.clear()
term.set_cursor(1, 1)
term.set_bg(term.colors.blue)
term.set_fg(term.colors.white)
local title = "ITEM SORTER"
if paused then title = "ITEM SORTER (PAUSED)" end
term.write(string.format(" %-34s%15s ", title, total .. " sorted"))
term.set_bg(term.colors.black)
term.set_fg(term.colors.light_gray)
term.set_cursor(1, 3)
term.write(string.format(" %-9s %-25s%7s %-7s", "Chest", "Rules", "Items", "State"))
local y = 4
for key, _ in pairs(CHESTS) do
term.set_cursor(1, y)
term.set_fg(term.colors.white)
term.write(string.format(" %-9s %-25s%7d ", key:sub(1, 9), rules_of[key]:sub(1, 25), moved[key]))
if state[key] == "ok" then
term.set_fg(term.colors.green)
elseif state[key] == "FULL" then
term.set_fg(term.colors.yellow)
else
term.set_fg(term.colors.red)
end
term.write(state[key])
y = y + 1
end
term.set_cursor(2, 16)
if not input_ok then
term.set_fg(term.colors.red)
term.write("Input chest not found: " .. INPUT)
elseif waiting > 0 then
term.set_fg(term.colors.yellow)
term.write(waiting .. " stack(s) stuck in the input: chests full")
else
term.set_fg(term.colors.green)
term.write("Input chest empty, waiting for items.")
end
term.set_cursor(2, 17)
term.set_fg(term.colors.light_gray)
term.write("Last: " .. last)
term.set_cursor(2, 19)
term.set_fg(term.colors.gray)
term.write("Click the screen to pause or resume.")
endThe loop waits for its timer. After a pass that moved something, it looks again soon (BUSY), since a belt is probably still feeding the chest; after an empty pass it waits longer (IDLE), so an idle sorter costs almost nothing.
setup()
draw()
local timer = os.start_timer(IDLE)
while true do
local e = os.pull_event()
if e.name == "timer" and e.id == timer then
local done = 0
if not paused then done = sort_pass() end
draw()
if done > 0 then timer = os.start_timer(BUSY) else timer = os.start_timer(IDLE) end
elseif e.name == "click" then
paused = not paused
draw()
end
endThe whole program
-- Item sorter: empties an input chest into storage chests on a Data Cable,
-- following rules; what no rule wants goes to the overflow chest.
local INPUT = "inventory@11,64,-2" -- the chest you fill: a cable name or a side
local CHESTS = { -- short name = cable name of the chest
ores = "inventory@10,64,-4",
ingots = "inventory@11,64,-4",
create = "inventory@12,64,-4",
wood = "inventory@13,64,-4",
overflow = "inventory@14,64,-4",
}
local OVERFLOW = "overflow" -- where items without a rule go
-- Read from top to bottom, the first match wins:
-- "minecraft:iron_ingot" this item only
-- "create:*" every id that starts with "create:"
-- "*_ore" every id that ends with "_ore"
local RULES = {
{match = "minecraft:raw_*", to = "ores"},
{match = "*_ore", to = "ores"},
{match = "*_ingot", to = "ingots"},
{match = "*_nugget", to = "ingots"},
{match = "create:*", to = "create"},
{match = "*_log", to = "wood"},
{match = "*_planks", to = "wood"},
}
local IDLE = 1 -- seconds between two looks at an empty input chest
local BUSY = 0.25 -- seconds between two passes while items keep coming
local input = nil
local moved = {} -- chest name -> items sent there since the start
local state = {} -- chest name -> "ok", "FULL" or "MISSING"
local rules_of = {} -- chest name -> its rules, as text for the screen
local total = 0 -- items sorted since the start
local waiting = 0 -- stacks left in the input after the last pass
local input_ok = true
local last = "nothing yet"
local paused = false
-- Rules ------------------------------------------------------------------------
local function matches(rule, id)
if rule:starts("*") then return id:ends(rule:sub(2)) end
if rule:ends("*") then return id:starts(rule:sub(1, -2)) end
return id == rule
end
local function destination(id)
for _, rule in ipairs(RULES) do
if matches(rule.match, id) then return rule.to end
end
return OVERFLOW
end
local function setup()
input = peripheral.wrap(INPUT)
if input == nil or input.push == nil then
error("no input chest '" .. INPUT .. "'")
end
if CHESTS[OVERFLOW] == nil then
error("the overflow chest '" .. OVERFLOW .. "' is not in CHESTS")
end
for _, rule in ipairs(RULES) do
if CHESTS[rule.to] == nil then
error("rule '" .. rule.match .. "' sends to '" .. rule.to .. "', which is not in CHESTS")
end
if rules_of[rule.to] == nil then
rules_of[rule.to] = rule.match
else
rules_of[rule.to] = rules_of[rule.to] .. ", " .. rule.match
end
end
for key, name in pairs(CHESTS) do
moved[key] = 0
if peripheral.type(name) == nil then state[key] = "MISSING" else state[key] = "ok" end
if rules_of[key] == nil then rules_of[key] = "(no rule)" end
end
if rules_of[OVERFLOW] == "(no rule)" then rules_of[OVERFLOW] = "everything else" end
end
-- Moving items -------------------------------------------------------------------
local function push_to(key, slot, count)
local r = pcall(input.push, CHESTS[key], slot, count)
if not r.ok then
state[key] = "MISSING"
return 0
end
if r.value < count then state[key] = "FULL" else state[key] = "ok" end
moved[key] = moved[key] + r.value
return r.value
end
local function sort_pass()
local r = pcall(input.list)
input_ok = r.ok
if not r.ok then return 0 end
local done = 0
waiting = 0
for slot, stack in pairs(r.value) do
local key = destination(stack.name)
local n = push_to(key, slot, stack.count)
local where = key
if n < stack.count and key ~= OVERFLOW then
local extra = push_to(OVERFLOW, slot, stack.count - n)
if extra > 0 then where = OVERFLOW end
n = n + extra
end
if n > 0 then last = n .. " x " .. stack.name .. " -> " .. where end
if n < stack.count then waiting = waiting + 1 end
done = done + n
end
total = total + done
return done
end
-- Screen -------------------------------------------------------------------------
local function draw()
term.set_bg(term.colors.black)
term.clear()
term.set_cursor(1, 1)
term.set_bg(term.colors.blue)
term.set_fg(term.colors.white)
local title = "ITEM SORTER"
if paused then title = "ITEM SORTER (PAUSED)" end
term.write(string.format(" %-34s%15s ", title, total .. " sorted"))
term.set_bg(term.colors.black)
term.set_fg(term.colors.light_gray)
term.set_cursor(1, 3)
term.write(string.format(" %-9s %-25s%7s %-7s", "Chest", "Rules", "Items", "State"))
local y = 4
for key, _ in pairs(CHESTS) do
term.set_cursor(1, y)
term.set_fg(term.colors.white)
term.write(string.format(" %-9s %-25s%7d ", key:sub(1, 9), rules_of[key]:sub(1, 25), moved[key]))
if state[key] == "ok" then
term.set_fg(term.colors.green)
elseif state[key] == "FULL" then
term.set_fg(term.colors.yellow)
else
term.set_fg(term.colors.red)
end
term.write(state[key])
y = y + 1
end
term.set_cursor(2, 16)
if not input_ok then
term.set_fg(term.colors.red)
term.write("Input chest not found: " .. INPUT)
elseif waiting > 0 then
term.set_fg(term.colors.yellow)
term.write(waiting .. " stack(s) stuck in the input: chests full")
else
term.set_fg(term.colors.green)
term.write("Input chest empty, waiting for items.")
end
term.set_cursor(2, 17)
term.set_fg(term.colors.light_gray)
term.write("Last: " .. last)
term.set_cursor(2, 19)
term.set_fg(term.colors.gray)
term.write("Click the screen to pause or resume.")
end
-- Main loop ----------------------------------------------------------------------
setup()
draw()
local timer = os.start_timer(IDLE)
while true do
local e = os.pull_event()
if e.name == "timer" and e.id == timer then
local done = 0
if not paused then done = sort_pass() end
draw()
if done > 0 then timer = os.start_timer(BUSY) else timer = os.start_timer(IDLE) end
elseif e.name == "click" then
paused = not paused
draw()
end
endWhat the screen looks like
The same draw function, with the counters of a sorter that has been running for a while: the wood barrel is full, so planks went to the overflow.
local INPUT = "inventory@11,64,-2"
local CHESTS = {
ores = "inventory@10,64,-4",
ingots = "inventory@11,64,-4",
create = "inventory@12,64,-4",
wood = "inventory@13,64,-4",
overflow = "inventory@14,64,-4",
}
local rules_of = {
ores = "minecraft:raw_*, *_ore",
ingots = "*_ingot, *_nugget",
create = "create:*",
wood = "*_log, *_planks",
overflow = "everything else",
}
local moved = {ores = 1408, ingots = 896, create = 517, wood = 1728, overflow = 75}
local state = {ores = "ok", ingots = "ok", create = "ok", wood = "FULL", overflow = "ok"}
local total = 4624
local waiting = 0
local input_ok = true
local last = "11 x minecraft:oak_planks -> overflow"
local paused = false
local function draw()
term.set_bg(term.colors.black)
term.clear()
term.set_cursor(1, 1)
term.set_bg(term.colors.blue)
term.set_fg(term.colors.white)
local title = "ITEM SORTER"
if paused then title = "ITEM SORTER (PAUSED)" end
term.write(string.format(" %-34s%15s ", title, total .. " sorted"))
term.set_bg(term.colors.black)
term.set_fg(term.colors.light_gray)
term.set_cursor(1, 3)
term.write(string.format(" %-9s %-25s%7s %-7s", "Chest", "Rules", "Items", "State"))
local y = 4
for key, _ in pairs(CHESTS) do
term.set_cursor(1, y)
term.set_fg(term.colors.white)
term.write(string.format(" %-9s %-25s%7d ", key:sub(1, 9), rules_of[key]:sub(1, 25), moved[key]))
if state[key] == "ok" then
term.set_fg(term.colors.green)
elseif state[key] == "FULL" then
term.set_fg(term.colors.yellow)
else
term.set_fg(term.colors.red)
end
term.write(state[key])
y = y + 1
end
term.set_cursor(2, 16)
if not input_ok then
term.set_fg(term.colors.red)
term.write("Input chest not found: " .. INPUT)
elseif waiting > 0 then
term.set_fg(term.colors.yellow)
term.write(waiting .. " stack(s) stuck in the input: chests full")
else
term.set_fg(term.colors.green)
term.write("Input chest empty, waiting for items.")
end
term.set_cursor(2, 17)
term.set_fg(term.colors.light_gray)
term.write("Last: " .. last)
term.set_cursor(2, 19)
term.set_fg(term.colors.gray)
term.write("Click the screen to pause or resume.")
end
draw()
Testing it
Find the names of your chests. In the shell, the interactive interpreter lists every inventory on the cable:
> brass Brass 1.0 - type 'exit' to leave. brass> for _, n in ipairs(peripheral.list("inventory")) do print(n) end inventory@11,64,-2 inventory@10,64,-4 inventory@11,64,-4 inventory@12,64,-4 inventory@13,64,-4 inventory@14,64,-4 brass> exit
Look at each chest with F3 to know which coordinates is which, or run the
devicesexample (devices), which draws the computer, its cable and every block on it.- Write the names into
INPUTandCHESTS, save asstartupand run it. A wrong name stops the program at once withno input chest '...', or showsMISSINGnext to the chest. - Drop a mixed stack of things into the input chest: iron ore, raw copper, oak logs, Create cogwheels, dirt. Within a second they leave for their chests; the dirt goes to the overflow.
- Fill the wood barrel to the top, and drop more logs: the line of the wood barrel turns
FULLand the logs go to the overflow. - Break the overflow barrel and fill the wood barrel: the logs stay in the input, and the status line counts the stuck stacks.
Variations
- Stop the belt when stuck. When
waiting > 0, power a redstone output that reaches the Create funnel feeding the input chest: a powered funnel stops moving items, so the belt backs up instead of overflowing.rs.set("top", waiting > 0)after each pass. - Sort by name. Each stack also has a
displayfield, its name as shown in game. Rename items in an anvil ("Return to base") and add a rule kind that comparesstack.display. - Remember the counters. Save
movedto a file withfs.writeevery minute and read it back insetup, so the counts survive a reboot. - Fetch items back. Wrap a storage chest and push from it to an output chest:
peripheral.wrap(CHESTS.ingots)then.push(OUTPUT, slot, 64). A touch screen with one button per item makes a small shop: seeTouch control panelfor the buttons. - Many chests. On a Modern Computer the screen has 24 rows: there is room for 15 chests. For a whole storage room, make each entry of
CHESTSa list of names (ores = {"inventory@10,64,-4", "inventory@10,65,-4"}), and letpush_totry them in turn until one takes the stack.