local term_stack = require("term-stack") local ensure_width = require("cc.strings").ensure_width local ui = require("ui") local main_view = {} local nbt_sensitive_names = { "minecraft:potion", "minecraft:enchanted_book" } local display_name_transformers = { ["minecraft:enchanted_book"] = function(detail) local enchantments = {} for _, enchant in ipairs(detail.enchantments) do table.insert(enchantments, enchant.displayName) end return table.concat(enchantments, ", ") .. " Book" end } local function clamp(x, min, max) return math.min(math.max(x, min), max) end local function rect(x, y, w, h) return { x = x, y = y, w = w, h = h or w } end local function copy_table(t) local t_copy = {} for k, v in pairs(t) do t_copy[k] = v end return t_copy end local function is_integer(num) return num % 1 == 0 end local function does_array_contain(array, value) for _, v in ipairs(array) do if v == value then return true end end return false end local function add_item_detail(item_details, name, detail) if item_details[name] then return end local display_name if not display_name_transformers[detail.name] then display_name = detail.displayName else display_name = display_name_transformers[detail.name](detail) end item_details[name] = { display_name = display_name, stack_size = detail.maxCount } end local function list_items(inventories, item_details) local item_registry = {} for _, name in ipairs(inventories) do for slot, item in pairs(peripheral.call(name, "list")) do local item_name = item.name if does_array_contain(nbt_sensitive_names, item_name) then local detail = peripheral.call(name, "getItemDetail", slot) item_name = item_name .. "#" .. item.nbt add_item_detail(item_details, item_name, detail) end item_registry[item_name] = item_registry[item_name] or {} table.insert(item_registry[item_name], { count = item.count, slot = slot, peripheral = name }) end end for _, items in pairs(item_registry) do items.count = 0 for _, slot in ipairs(items) do items.count = items.count + slot.count end end return item_registry end local function populate_item_details(item_details, item_registry) for name, items in pairs(item_registry) do local item = items[1] if item and not item_details[name] then local detail = peripheral.call(item.peripheral, "getItemDetail", item.slot) add_item_detail(item_details, name, detail) end end end local function remove_value(array, value) for i, v in ipairs(array) do if v == value then table.remove(array, i) return end end end local function vsplit(area, x) local left_area = rect(area.x, area.y, x - area.x, area.h) local right_area = rect(area.x + left_area.w+1, area.y, area.w - left_area.w-1, area.h) return left_area, right_area end local function vline(x, y, height) for i=1, height do term.setCursorPos(x, y+i-1) term.write("|") end end local function hline(x, y, length) for i=1, length do term.setCursorPos(x+i-1, y) term.write("-") end end local function get_item_counts(items) local item_counts = {} for name, item_collection in pairs(items) do item_counts[name] = item_collection.count end return item_counts end local function is_bundle_name(name) return name:find("#") == 1 end local function do_bundles_contain_cycle(bundles) local visited = {} local stack = {} local function is_cyclic_util(bundle_name) if not visited[bundle_name] then visited[bundle_name] = true stack[bundle_name] = true for dep_name in pairs(bundles[bundle_name]) do if is_bundle_name(dep_name) then dep_name = dep_name:sub(2) if not visited[dep_name] and is_cyclic_util(dep_name) then return true elseif stack[dep_name] then return true end end end end stack[bundle_name] = false return false end for bundle_name in pairs(bundles) do if not visited[bundle_name] and is_cyclic_util(bundle_name) then return true end end return false end -- TODO: improve this function local function resolve_bundle_items(bundles, bundle_name, seen_bundles) seen_bundles = seen_bundles or {} if seen_bundles[bundle_name] then return nil, "Circular dependency" end local bundle = bundles[bundle_name] if not bundle then return {} end local items = {} for dep_name, dep_count in pairs(bundle) do if is_bundle_name(dep_name) then seen_bundles[bundle_name] = true local dep_items, err = resolve_bundle_items(bundles, dep_name:sub(2), seen_bundles) if not dep_items then return nil, err end for item_name, item_count in pairs(dep_items) do items[item_name] = (items[item_name] or 0) + item_count * dep_count end seen_bundles[bundle_name] = nil else items[dep_name] = (items[dep_name] or 0) + dep_count end end return items end -- TODO: improve this function local function populate_bundle_details(bundle_details, bundles) for bundle_name in pairs(bundles) do local hash_name = "#"..bundle_name if not bundle_details[hash_name] then bundle_details[hash_name] = resolve_bundle_items(bundles, bundle_name) end end end local function derive_available_bundles(available_items, bundle_details) local available_bundles = {} for bundle_name, items in pairs(bundle_details) do local bundle_count = math.huge for item_name, item_count in pairs(items) do local available_count = available_items[item_name] or 0 bundle_count = math.min(bundle_count, math.max(0, available_count / item_count)) end if bundle_count ~= math.huge and bundle_count > 0 then available_bundles[bundle_name] = bundle_count end end return available_bundles end local function sort_names_by_counts(names, items, bundles) table.sort(names, function(a, b) local count_a = items[a] or bundles[a] local count_b = items[b] or bundles[b] if count_a == count_b then return a:gsub("^#", "") < b:gsub("^#", "") else return count_a > count_b end end) end function main_view:prepare(inventories, bundles, result_inventory) self.event = {} self.main_area = rect(1, 1, term.getSize()) self.split_x = math.floor(self.main_area.w*0.5) self.left_area, self.right_area = vsplit(self.main_area, self.split_x) self.is_left_active = true self.bundle_name = "" self.result_inventory = result_inventory self.inventories = inventories remove_value(self.inventories, result_inventory) self.item_details = {} self.item_registry = list_items(self.inventories, self.item_details) populate_item_details(self.item_details, self.item_registry) self.bundles = bundles or {} self.bundle_details = {} populate_bundle_details(self.bundle_details, self.bundles) do local available_items = get_item_counts(self.item_registry) local available_bundles = derive_available_bundles(available_items, self.bundle_details) local filtered_names = {} do for name, _ in pairs(available_items) do table.insert(filtered_names, name) end for name, _ in pairs(available_bundles) do table.insert(filtered_names, name) end sort_names_by_counts(filtered_names, available_items, available_bundles) end self.left_store = { items = available_items, bundles = available_bundles, filtered_names = filtered_names, selected_idx = 1, search_bar = "", scroll = 0 } self.right_store = { total_items = {}, items = {}, bundles = {}, filtered_names = {}, selected_idx = 1, search_bar = "", scroll = 0 } end end function main_view:save_to_file(filename) local f, err = io.open(filename, "w") if not f then return nil, err end f:write(textutils.serialise{ inventories = self.inventories, bundles = self.bundles, result_inventory = self.result_inventory }) f:close() end function main_view:load_from_file(filename) local f, err = io.open(filename, "r") if not f then return nil, err end local data = textutils.unserialise(f:read("a")) main_view:prepare(data.inventories, data.bundles, data.result_inventory) f:close() end function main_view:list_filtered_names(items, bundles, name_filter) name_filter = name_filter:lower() local names = {} for name, count in pairs(items) do local display_name = self.item_details[name].display_name:lower() if display_name:find(name_filter) and count > 0 then table.insert(names, name) end end for name, count in pairs(bundles) do if name:find(name_filter) and count > 0 then table.insert(names, name) end end sort_names_by_counts(names, items, bundles) return names end function main_view:refresh_filtered_names(store, area) store.filtered_names = main_view:list_filtered_names(store.items, store.bundles, store.search_bar or "") self:set_selected_option(store, area, store.selected_idx) end function main_view:set_selected_option(store, area, idx) local filtered_count = #store.filtered_names store.selected_idx = clamp(idx, 1, filtered_count) local item_list_height = area.h-1 if store.scroll+item_list_height > filtered_count and filtered_count > item_list_height then store.scroll = filtered_count-item_list_height end local margin = 2 if store.selected_idx > store.scroll+item_list_height-margin then local max_scroll = math.max(filtered_count - item_list_height, 0) store.scroll = math.min(store.selected_idx-item_list_height+margin, max_scroll) elseif store.selected_idx <= store.scroll+margin then store.scroll = math.max(store.selected_idx-1-margin, 0) end end function main_view:select_option(store, area, item_name) for i, name in ipairs(store.filtered_names) do if name == item_name then self:set_selected_option(store, area, i) return end end end function main_view:process_movement_keys(store, area) if self.down_pressed then self:set_selected_option(store, area, store.selected_idx + 1) elseif self.up_pressed then self:set_selected_option(store, area, store.selected_idx - 1) end local lstore = self.left_store local rstore = self.right_store local left_selected = lstore.filtered_names[lstore.selected_idx] local right_selected = rstore.filtered_names[rstore.selected_idx] local selected_option = self.is_left_active and left_selected or right_selected if selected_option and (self.right_pressed or self.left_pressed) then if is_bundle_name(selected_option) then local bundle_name = selected_option local bundle = self.bundle_details[bundle_name] if self.right_pressed and lstore.bundles[bundle_name] > 0 then local transferred = math.min(lstore.bundles[bundle_name], 1) for item, count in pairs(bundle) do lstore.items[item] = lstore.items[item] - math.floor(count * transferred) end rstore.bundles[bundle_name] = (rstore.bundles[bundle_name] or 0) + transferred elseif self.left_pressed and rstore.bundles[bundle_name] > 0 then local transferred if is_integer(rstore.bundles[bundle_name]) then transferred = math.min(rstore.bundles[bundle_name], 1) else transferred = rstore.bundles[bundle_name] % 1 end for item, count in pairs(bundle) do lstore.items[item] = lstore.items[item] + math.floor(count * transferred) end rstore.bundles[bundle_name] = rstore.bundles[bundle_name] - transferred end else local item = selected_option local stack_size = self.item_details[item].stack_size local src_items, dest_items if self.right_pressed then src_items, dest_items = lstore.items, rstore.items elseif self.left_pressed then src_items, dest_items = rstore.items, lstore.items end local transferred = math.min(src_items[item] or 0, stack_size) src_items[item] = (src_items[item] or 0) - transferred dest_items[item] = (dest_items[item] or 0) + transferred end lstore.bundles = derive_available_bundles(lstore.items, self.bundle_details) self:refresh_filtered_names(lstore, self.left_area) self:refresh_filtered_names(rstore, self.right_area) self:select_option(store, area, selected_option) end end function main_view:display_list(store, area, selected_idx) term_stack.push_cursor(area.x, area.y) local filtered_count = #store.filtered_names local function get_number_width(num) return math.floor(math.log(num, 10) + 1) end local count_collumn_width do -- Figure out how wide does the count column need to be local max_count = 0 for row=1, math.min(area.h, filtered_count) do local name = store.filtered_names[row + store.scroll] if name then if is_bundle_name(name) then max_count = math.max(max_count, store.bundles[name]) else max_count = math.max(max_count, store.items[name]) end end end count_collumn_width = get_number_width(max_count) if not is_integer(max_count) then count_collumn_width = count_collumn_width + 1 end end do -- Display the options for row=1, math.min(area.h, filtered_count) do local i = row + store.scroll local name = store.filtered_names[i] if name then local count, display_name if is_bundle_name(name) then count, display_name = store.bundles[name], name else count, display_name = store.items[name], self.item_details[name].display_name end local count_width = math.floor(math.log(count, 10) + 1) if i == selected_idx then term.setTextColor(colors.yellow) else term.setTextColor(colors.white) end if is_integer(count) then term.setCursorPos((count_collumn_width+1 - count_width), row) term.write(count) else term.setCursorPos((count_collumn_width+1 - count_width - 1), row) if count >= 1 then term.write(math.floor(count)) end term.write("*") end term.setCursorPos(count_collumn_width+2, row) term.write(ensure_width(display_name, area.w-count_collumn_width-1)) end end end do -- Display the scrollbar on the right if filtered_count > area.h then local total_height = math.max(filtered_count) local scroll_height = math.max(1, area.h/total_height*area.h) local y = 1+store.scroll/filtered_count*area.h paintutils.drawLine(area.w, y, area.w, y+scroll_height-1, colors.lightGray) end end term_stack.pop_cursor() end function main_view:display_list_with_search(store, area, active) term_stack.push_cursor(area.x, area.y) local new_search_bar = ui:textbox(area.w, active, "Search", store.search_bar) if new_search_bar ~= store.search_bar then store.search_bar = new_search_bar self:refresh_filtered_names(store, area) end if active then self:process_movement_keys(store, area) end term.setBackgroundColor(colors.black) main_view:display_list(store, rect(1, 2, area.w, area.h-1), active and store.selected_idx) term_stack.pop_cursor() end function main_view:move_item(item_name, amount, destination) local item_collection = self.item_registry[item_name] local moved_amount = 0 while moved_amount < amount and item_collection.count > 0 do local slot_details = item_collection[#item_collection] local needed_amount = amount - moved_amount local transferred_amount = peripheral.call(slot_details.peripheral, "pushItems", destination, slot_details.slot, needed_amount) if transferred_amount == 0 then break end moved_amount = moved_amount + transferred_amount slot_details.count = slot_details.count - transferred_amount item_collection.count = item_collection.count - transferred_amount if slot_details.count == 0 then table.remove(item_collection) end end return moved_amount end function main_view:has_selected_items() for _, count in pairs(main_view.right_store.items) do if count > 0 then return true end end for _, count in pairs(main_view.right_store.bundles) do if count > 0 then return true end end return false end function main_view:refresh_items() self.item_registry = list_items(self.inventories, self.item_details) populate_item_details(self.item_details, self.item_registry) self.right_store.items = {} self.right_store.bundles = {} self.left_store.items = get_item_counts(self.item_registry) self.left_store.bundles = derive_available_bundles(self.left_store.items, self.bundle_details) self:refresh_filtered_names(self.left_store, self.left_area) self:refresh_filtered_names(self.right_store, self.right_area) end function main_view:deposit_items() for slot, item in pairs(peripheral.call(self.result_inventory, "list")) do local total_transferred = 0 for _, inventory in ipairs(self.inventories) do local transferred = peripheral.call(self.result_inventory, "pushItems", inventory, slot) total_transferred = total_transferred + transferred if total_transferred == item.count then break end end end self:refresh_items() end function main_view:save_bundle(name, items, bundles) local bundle = {} for item, count in pairs(items) do if count > 0 then bundle[item] = count end end for bundle_name, count in pairs(bundles) do if count > 0 then bundle[bundle_name] = count end end self.bundles[name] = bundle if do_bundles_contain_cycle(self.bundles) then self.bundles[name] = nil return false end populate_bundle_details(self.bundle_details, self.bundles) self.left_store.bundles = derive_available_bundles(self.left_store.items, self.bundle_details) self:refresh_filtered_names(self.left_store, self.left_area) if self.save_file then self:save_to_file(self.save_file) end return true end function main_view:display_bundle_popup(area) term_stack.push_cursor(area.x, area.y) -- Draw ascii box around textbox term.setBackgroundColor(colors.black) term.setTextColor(colors.lightGray) term.write("+") hline(2, 1, area.w-2) term.write("+") term.setCursorPos(1, 2) term.write("|") term.setCursorPos(area.w, 2) term.write("|") term.setCursorPos(1, 3) term.write("+") hline(2, 3, area.w-2) term.write("+") term_stack.push_cursor(2, 2) self.bundle_name = ui:textbox(area.w-2, true, "Name", self.bundle_name) term_stack.pop_cursor() term_stack.pop_cursor() if self.tab_pressed then self.bundle_popup = false elseif self.submit_pressed and #self.bundle_name > 0 then self.bundle_popup = false local success = self:save_bundle(self.bundle_name, self.right_store.items, self.right_store.bundles) if success then self.bundle_name = "" end end end function main_view:request_items() local lstore = self.left_store local rstore = self.right_store if self:has_selected_items() then for name, count in pairs(rstore.items) do local transferred = self:move_item(name, count, self.result_inventory) lstore.items[name] = lstore.items[name] + (count - transferred) end for bundle_name, bundle_count in pairs(rstore.bundles) do bundle_count = math.max(bundle_count, 1) for item, count in pairs(self.bundle_details[bundle_name]) do local amount = math.floor(count*bundle_count) local transferred = self:move_item(item, amount, self.result_inventory) lstore.items[item] = lstore.items[item] + (amount - transferred) end end rstore.items = {} rstore.bundles = {} lstore.bundles = derive_available_bundles(lstore.items, self.bundle_details) self:refresh_filtered_names(lstore, self.left_area) self:refresh_filtered_names(rstore, self.right_area) else local selected_option = lstore.filtered_names[lstore.selected_idx] if is_bundle_name(selected_option) then local bundle_name = selected_option local bundle_count = math.min(lstore.bundles[bundle_name], 1) for item, count in pairs(self.bundle_details[bundle_name]) do local transferred = self:move_item(item, count, self.result_inventory) lstore.items[item] = lstore.items[item] - math.floor(transferred * bundle_count) end else local item = selected_option local stack_size = self.item_details[item].stack_size local transferred_count = self:move_item(item, stack_size, self.result_inventory) lstore.items[item] = lstore.items[item] - transferred_count end lstore.bundles = derive_available_bundles(lstore.items, self.bundle_details) self:refresh_filtered_names(lstore, self.left_area) self:select_option(lstore, self.left_area, selected_option) end end function main_view:run() term.setBackgroundColor(colors.black) term.clear() local has_selected_items = self:has_selected_items() if has_selected_items then term.setTextColor(colors.lightGray) vline(self.split_x, self.main_area.y, self.main_area.h) main_view:display_list_with_search(self.left_store, self.left_area, self.is_left_active and not self.bundle_popup) main_view:display_list_with_search(self.right_store, self.right_area, not self.is_left_active and not self.bundle_popup) else main_view:display_list_with_search(self.left_store, self.main_area, not self.bundle_popup) end if self.bundle_popup then local w = self.main_area.w - 4 local h = 3 local area = rect( 1+math.floor((self.main_area.w-w)/2), 1+math.floor((self.main_area.h-h)/2), w, h ) self:display_bundle_popup(area) end self.up_pressed = false self.down_pressed = false self.left_pressed = false self.right_pressed = false self.tab_pressed = false end function main_view:on_event(event, ...) local has_selected_items = self:has_selected_items() if not has_selected_items and not self.is_left_active then self.is_left_active = true end if event == "key" then self:on_key(...) elseif event == "key_up" then self:on_key_up(...) end end function main_view:on_key(key) self.up_pressed = key == keys.up or (self.ctrl_down and key == keys.k) self.down_pressed = key == keys.down or (self.ctrl_down and key == keys.j) self.left_pressed = key == keys.left or (self.ctrl_down and key == keys.h) self.right_pressed = key == keys.right or (self.ctrl_down and key == keys.l) self.submit_pressed = key == keys.enter or (self.ctrl_down and key == keys.space) self.tab_pressed = key == keys.tab if key == keys.rightCtrl or key == keys.leftCtrl then self.ctrl_down = true elseif key == keys.rightAlt or key == keys.leftAlt then self.alt_down = true end if not self.bundle_popup then if self.ctrl_down and key == keys.n and self:has_selected_items() then self.bundle_popup = true elseif key == keys.tab then self.is_left_active = not self.is_left_active elseif key == keys.f5 then self:refresh_items() elseif self.submit_pressed then self:request_items() elseif self.ctrl_down and key == keys.d then self:deposit_items() end end end function main_view:on_key_up(key) if key == keys.rightCtrl or key == keys.leftCtrl then self.ctrl_down = false elseif key == keys.rightAlt or key == keys.leftAlt then self.alt_down = false end end return main_view