local function draw_bar(y, c1, c2, p, fmt, ...) local str = string.format(fmt, ...) local tw = term.getSize() local w1 = math.ceil(p * tw) local w2 = tw - w1 local old_bg = term.getBackgroundColor() term.setCursorPos(1, y) term.setBackgroundColor(c1) term.write(str:sub(1, w1)) local rem = w1 - #str if rem > 0 then term.write(string.rep(" ", rem)) end term.setBackgroundColor(c2) term.write(str:sub(w1 + 1, w1 + w2)) rem = math.min(tw - #str, w2) if rem > 0 then term.write(string.rep(" ", rem)) end term.setBackgroundColor(old_bg) end function table.deepcopy(obj) if type(obj) ~= 'table' then return obj end local res = {} for k, v in pairs(obj) do res[table.deepcopy(k)] = table.deepcopy(v) end return res end function spairs(t, order) -- collect the tbl_keys local tbl_keys = {} for k in pairs(t) do tbl_keys[#tbl_keys+1] = k end -- if order function given, sort by it by passing the table and tbl_keys a, b, -- otherwise just sort the tbl_keys if order then table.sort(tbl_keys, function(a,b) return order(t, a, b) end) else table.sort(tbl_keys) end -- return the iterator function local i = 0 return function() i = i + 1 if tbl_keys[i] then return tbl_keys[i], t[tbl_keys[i]] end end end function string.split(inputstr, sep) sep = sep or "%s" local t = {} for str in string.gmatch(inputstr, "([^"..sep.."]+)") do table.insert(t, str) end return table.unpack(t) end function map(tbl, fn) local out = {} for k, v in pairs(tbl) do out[k] = fn(k, v, tbl) end return out end function filter(tbl, fil) local out = {} for k, v in pairs(tbl) do if fil(k, v, tbl) then out[k] = v end end return out end function pop(tbl) local out = { k = nil, v = nil } for k, v in pairs(tbl) do out.k = k out.v = v table.remove(tbl, k) break end return out.k, out.v end function groupby(tbl, fn) local out = {} for k, v in pairs(tbl) do local group = fn(k, v, tbl) out[group] = out[group] or {} table.insert(out[group], { k = k, v = v }) end return out end function reduce(tbl, operator, initial) local accumulator = initial for k, v in pairs(tbl) do accumulator = operator(k, v, accumulator, tbl) end return accumulator end function pipe(input) local function chain(fn) return function(self, ...) fn(self, ...) return self end end return { _value = table.deepcopy(input), groupby = chain(function(self, fn) self._value = groupby(self._value, fn) end), filter = chain(function(self, fn) self._value = filter(self._value, fn) end), map = chain(function(self, fn) self._value = map(self._value, fn) end), reduce = chain(function(self, operator, initial) self._value = reduce(self._value, operator, initial) end), sort = chain(function(self, key) local out = {} for k, v in spairs(self._value, key) do table.insert(out, { k = k, v = v }) end self._value = out end), get = function(self) return self._value end } end local keyboard = { "qwertyuiop", "asdfghjkl\x1b", "zxcvbnm \xd7", } local config = { stock = {}, trash = {}, take = {}, storage = {} } local item_state = { stock = {}, storage = {}, junk = {} } local storage_sizes = { stock = {}, storage = {}, junk = {} } local sort_mode, sort_inverse = "count", false local search_open, search_query = false, "" local mon = peripheral.find("monitor") mon.setTextScale(0.5) mon.clear() mon.setPaletteColor(colors.blue, 0x131326) -- odd lines mon.setPaletteColor(colors.purple, 0x261326) -- even lines mon.setPaletteColor(colors.magenta, 0xaa55ff) -- search query mon.setPaletteColor(colors.cyan, 0x55aaff) -- keyboard mon.setPaletteColor(colors.brown, 0x262626) -- keyboard bg parallel.waitForAll(function() while true do local fp = assert(io.open("/restock.json", "r")) config = textutils.unserializeJSON(fp:read("a")) fp:close() os.sleep(1) end end, function() while true do local new_state = {} local new_sizes = {} map(config.storage, function(i, inv) new_sizes[inv] = peripheral.call(inv, "size") for slot, item in pairs(peripheral.call(inv, "list")) do new_state[string.format("%s#%d", inv, slot)] = item end end) item_state.storage = new_state storage_sizes.storage = new_sizes os.sleep(0) end end, function() while true do local new_state = {} local new_sizes = {} map( groupby(config.stock, function(location) local storage = string.split(location, "#") return storage end ), function(inv) new_sizes[inv] = peripheral.call(inv, "size") for slot, item in pairs(peripheral.call(inv, "list")) do new_state[string.format("%s#%d", inv, slot)] = item end end) item_state.stock = new_state storage_sizes.stock = new_sizes os.sleep(0) end end, function() -- IMPORT while true do map( groupby(config.stock, function(location) return ({ string.split(location, "#") })[1] end), function(stock_inv) local size = storage_sizes.stock[stock_inv] or 0 for i = 1, size do local slot = string.format("%s#%d", stock_inv, i) local slot_stocked, slot_expected = item_state.stock[slot], config.stock[slot] if (slot_stocked and slot_expected and slot_stocked.name ~= slot_expected.name) or (slot_stocked ~= nil and slot_expected == nil) then for _, output in ipairs(config.storage) do if peripheral.call(stock_inv, "pushItems", output, i) ~= 0 then print("MOVE", slot, slot_stocked.name) break end end end end end) os.sleep(0) end end, function() -- STOCK while true do local new_junk = {} local item_locations = groupby(item_state.storage, function(location, item) new_junk[item.name] = (new_junk[item.name] or 0) + item.count return item.name end) local tw, th = term.getSize() map(config.stock, function(destination, item) local dst_container, dst_slot = string.split(destination, "#") new_junk[item.name] = nil if item_locations[item.name] == nil then return end local dst_item = item_state.stock[destination] local dst_count = dst_item and dst_item.count or 0 if dst_count >= item.count then return end local missing = item.count - dst_count local _, take_from = pop(filter(item_locations[item.name], function(_, src_item) local src_container, src_slot = string.split(src_item.k, "#") return src_container ~= dst_container end)) if not take_from then return end local src_container, src_slot = string.split(take_from.k, "#") local t = os.clock() local out = peripheral.call(dst_container, "pullItems", src_container, tonumber(src_slot), missing, tonumber(dst_slot)) print(string.format("PUT %s*%d/%d to %s", item.name, out, missing, destination)) -- print(string.format("%s -> %s (%s*%d) took %.2f", take_from.k, destination, item.name, item.count, os.clock() - t)) end) item_state.junk = new_junk os.sleep(0) end end, function() -- JUNK while true do local y = 2 for name, count in pairs(item_state.junk) do map(filter(item_state.storage, function(location, item) return item.name == name end), function(location, item) local junk_container, junk_slot = string.split(location, "#") for _, junk_output in ipairs(config.junk) do if peripheral.call(junk_container, "pushItems", junk_output, tonumber(junk_slot)) ~= 0 then print("JUNK OUT", item.name, "->", junk_container) break end end end) y = y + 1 end os.sleep(0) end end, function() -- USER INPUT while true do local ev = { os.pullEvent() } if ev[1] == "char" and search_open then search_query = search_query .. ev[2] mon.setBackgroundColor(colors.gray) mon.setTextColor(colors.magenta) mon.setCursorPos(tw - 14, th - 20) mon.write("> " .. search_query) for i, line in ipairs(keyboard) do mon.setCursorPos(tw - 14, th - 20 + i) mon.setBackgroundColor(colors.brown) mon.setTextColor(colors.cyan) mon.write(line) end elseif ev[1] == "key" then if ev[2] == keys.enter then search_open = not search_open elseif ev[2] == keys.backspace and search_open then search_query = search_query:sub(1, #search_query - 1) mon.setBackgroundColor(colors.gray) mon.setTextColor(colors.magenta) mon.setCursorPos(tw - 14, th - 20) mon.write("> " .. search_query) for i, line in ipairs(keyboard) do mon.setCursorPos(tw - 14, th - 20 + i) mon.setBackgroundColor(colors.brown) mon.setTextColor(colors.cyan) mon.write(line) end end elseif ev[1] == "monitor_touch" then local tw, th = mon.getSize() local _, _, x, y = table.unpack(ev) if y == 3 or y == th then local new_mode if x >= 1 and x <= 8 then new_mode = "count" elseif x >= 10 and x <= (tw - 1) then new_mode = "name" elseif x == tw then search_open = not search_open end if new_mode ~= nil and new_mode == sort_mode then sort_inverse = not sort_inverse elseif new_mode ~= nil then sort_mode = new_mode sort_inverse = false end elseif search_open and x >= (tw - 14) and x < (tw - 4) and y > (th - 20) and y <= (th - 17) then mon.setBackgroundColor(colors.gray) mon.setTextColor(colors.magenta) mon.setCursorPos(tw - 14, th - 20) mon.write("> " .. search_query) for i, line in ipairs(keyboard) do mon.setCursorPos(tw - 14, th - 20 + i) mon.setBackgroundColor(colors.brown) mon.setTextColor(colors.cyan) mon.write(line) end local char = keyboard[20 - th + y]:sub(15 - tw + x, 15 - tw + x) if char >= "a" and char <= "z" then os.queueEvent("key", keys[char], false) os.queueEvent("char", char) os.queueEvent("key_up", keys[char]) else -- 27 backspace (ev=259) -- 215 enter (ev=257) local code = nil if char == "\x1b" then code = keys.backspace elseif char == "\xd7" then code = keys.enter end if code then os.queueEvent("key", code, false) os.queueEvent("key_up", code) end end end end end end, function() -- STATUS while true do local back_term = term.redirect(mon) local tw, th = term.getSize() term.setBackgroundColor(colors.black) term.setTextColor(colors.white) term.clear() local usage = pipe(config.storage) :map(function(_, storage) local used, total = 0, storage_sizes.storage[storage] or 0 for slot = 1, total do if item_state.storage[string.format("%s#%d", storage, slot)] then used = used + 1 end end return { used = used, total = total } end) :reduce(function(_, info, acc) return { used = info.used + acc.used, total = info.total + acc.total } end, { used = 0, total = 0 }):get() draw_bar(1, colors.lightGray, colors.gray, usage.used / usage.total, "Storage utilization: %7.3f%%", 100 * usage.used / usage.total) term.setCursorPos(1, 3) term.setBackgroundColor(colors.gray) term.clearLine() local infoline = string.format("Count %s Name %s", (sort_mode == "count" and (sort_inverse and "\x1e" or "\x1f") or " "), (sort_mode == "name" and (sort_inverse and "\x1e" or "\x1f") or " ")) local fg = sort_mode == "count" and "55555551f88888881" or "88888881f55555551" term.blit(infoline, fg, "77777777777777777") term.setCursorPos(tw, 3) term.write("\x0c") term.setCursorPos(1, th) term.clearLine() term.blit(infoline, fg, "77777777777777777") term.setCursorPos(tw, th) term.write("\x0c") pipe(item_state.storage) :groupby(function(slot, item) return item.name end) :filter(function(name, slots) if not search_open then return true end return string.match(name, search_query) end) :map(function(name, slots) return reduce(slots, function(_, slot, acc) return slot.v.count + acc end, 0) end) :sort(function(t, a, b) local swap = false if sort_mode == "count" then swap = t[a] > t[b] end if sort_mode == "name" then swap = a > b end return swap ~= sort_inverse end) :map(function(i, kv) if i >= (th - 4) then return end term.setCursorPos(1, 3 + i) term.setBackgroundColor((i % 2) == 0 and colors.blue or colors.purple) term.clearLine() term.write(string.format("%8d %s", kv.v, kv.k)) end) if search_open then mon.setBackgroundColor(colors.gray) mon.setTextColor(colors.magenta) term.setCursorPos(tw - 14, th - 20) term.write("> " .. search_query) for i, line in ipairs(keyboard) do term.setCursorPos(tw - 14, th - 20 + i) term.setBackgroundColor(colors.brown) term.setTextColor(colors.cyan) term.write(line) end end term.redirect(back_term) os.sleep(1) end end)