Create: Computing AgesBrass Docs
Cookbook: real projects

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,-3 for 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 cable

The 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:

rulematches
"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...
Brass
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 coming

The 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":

Brass
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

rule:sub(2) is the rule without its first character, rule:sub(1, -2) without its last one (a negative position counts from the end).

Brass
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"))
Screen
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:

Brass
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
end

pairs 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.
  • slot limits the move to that slot of the source; count is the most items to move (64 when left out).
  • It returns how many items actually moved: 0 when 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:

Brass
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

5. 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.

Brass
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
end

input.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.

Brass
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

The 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.

Brass
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
end

The whole program

startup
-- 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
end

What 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.

Brass
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()
Screen
Screen

Testing it

  1. Find the names of your chests. In the shell, the interactive interpreter lists every inventory on the cable:

    Terminal
    > 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 devices example (devices), which draws the computer, its cable and every block on it.

  2. Write the names into INPUT and CHESTS, save as startup and run it. A wrong name stops the program at once with no input chest '...', or shows MISSING next to the chest.
  3. 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.
  4. Fill the wood barrel to the top, and drop more logs: the line of the wood barrel turns FULL and the logs go to the overflow.
  5. 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 display field, its name as shown in game. Rename items in an anvil ("Return to base") and add a rule kind that compares stack.display.
  • Remember the counters. Save moved to a file with fs.write every minute and read it back in setup, 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: see Touch control panel for 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 CHESTS a list of names (ores = {"inventory@10,64,-4", "inventory@10,65,-4"}), and let push_to try them in turn until one takes the stack.