diff --git a/dbg.lua b/dbg.lua new file mode 100644 index 0000000..87dc0f4 --- /dev/null +++ b/dbg.lua @@ -0,0 +1,36 @@ + +local dbg_file +function dbg(...) + if dbg_file then + local out = io.open(dbg_file, "a") + if not out then return end + for i = 1, select("#", ...) do + local value = select(i, ...) + if type(value) == "table" then + out:write(textutils.serialise(value)) + else + out:write(tostring(value)) + out:write("\t") + end + end + out:write("\n") + out:close() + else + local pretty = require("cc.pretty") + for i = 1, select("#", ...) do + local value = select(i, ...) + pretty.pretty_print(value) + end + end +end + +return setmetatable({}, { __call = function(_, file) + if dbg_file then return end + + if file then + dbg_file = fs.combine(shell.dir(), file) + if dbg_file then + fs.delete(dbg_file) + end + end +end}) diff --git a/main.lua b/main.lua index f683025..90ef33b 100644 --- a/main.lua +++ b/main.lua @@ -1,33 +1,7 @@ local ui = require("ui") local term_stack = require("term-stack") -local dbg_file = fs.combine(shell.dir(), "logs.txt") -if dbg_file then - fs.delete(dbg_file) -end -function dbg(...) - if dbg_file then - local out = io.open(dbg_file, "a") - if not out then return end - for i = 1, select("#", ...) do - local value = select(i, ...) - if type(value) == "table" then - out:write(textutils.serialise(value)) - else - out:write(tostring(value)) - out:write(" ") - end - end - out:write("\n") - out:close() - else - local pretty = require("cc.pretty") - for i = 1, select("#", ...) do - local value = select(i, ...) - pretty.pretty_print(value) - end - end -end +require("dbg")("logs.txt") local function is_inventory(peripheral_name) local types = {peripheral.getType(peripheral_name)} @@ -60,8 +34,19 @@ end local function main() local main_view = require("views.main") + local bundles = { + green = { + ["minecraft:kelp"] = 64, + ["minecraft:grass_block"] = 64 + }, + terraform = { + ["minecraft:grass_block"] = 64, + ["minecraft:stone"] = 64, + ["#green"] = 1 + } + } local inventories = list_inventories() - main_view:prepare(inventories, "minecraft:barrel_4") + main_view:prepare(inventories, bundles, "minecraft:barrel_4") local update_interval = 0.1 local update_timer = os.startTimer(update_interval) @@ -81,7 +66,7 @@ local function main() end end - main_view:draw() + main_view:run() end end diff --git a/views/main.lua b/views/main.lua index 1a597cf..3af7e84 100644 --- a/views/main.lua +++ b/views/main.lua @@ -11,6 +11,14 @@ 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 list_items(inventories) local item_registry = {} for _, name in ipairs(inventories) do @@ -69,6 +77,13 @@ local function vline(x, y, height) 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 @@ -77,14 +92,85 @@ local function get_item_counts(items) return item_counts end -function main_view:prepare(inventories, result_inventory) - self.event = {} +local function is_bundle_name(name) + return name:find("#") == 1 +end - self.is_left_active = true +local function get_number_width(num) + return math.floor(math.log(num, 10) + 1) +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] = math.max(bundle_count, 1) + 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 @@ -94,85 +180,239 @@ function main_view:prepare(inventories, result_inventory) self.item_details = {} populate_item_details(self.item_details, self.item_registry) - local all_item_counts = get_item_counts(self.item_registry) - self.left_store = { - all_items = all_item_counts, - filtered_names = self:list_filtered_names(all_item_counts, ""), - search_bar = "", - selected_idx = 1, - scroll = 0 - } - self.right_store = { - all_items = {}, - filtered_names = {}, - search_bar = "", - selected_idx = 1, - scroll = 0 - } + 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:list_filtered_names(all_items, name_filter) + +function main_view:list_filtered_names(items, bundles, name_filter) name_filter = name_filter:lower() local names = {} - for name, count in pairs(all_items) do + 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 - table.sort(names, function(a, b) - if all_items[a] == all_items[b] then - return a < b - else - return all_items[a] > all_items[b] + for name, count in pairs(bundles) do + if name:find(name_filter) and count > 0 then + table.insert(names, name) end - end) + end + + sort_names_by_counts(names, items, bundles) return names end -function main_view:display_item_list(store, area, active) +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 + for item, count in pairs(bundle) do + lstore.items[item] = lstore.items[item] - count + end + rstore.bundles[bundle_name] = (rstore.bundles[bundle_name] or 0) + 1 + + elseif self.left_pressed and rstore.bundles[bundle_name] > 0 then + for item, count in pairs(bundle) do + lstore.items[item] = lstore.items[item] + count + end + rstore.bundles[bundle_name] = rstore.bundles[bundle_name] - 1 + 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_names = store.filtered_names - local shown_count = #filtered_names + local filtered_count = #store.filtered_names - local max_count = 0 - for i=1, math.min(area.h, shown_count) do - local name = filtered_names[i + store.scroll] - local count = store.all_items[name] - max_count = math.max(max_count, count) - end - local max_count_width = math.floor(math.log(max_count, 10) + 1) - - for row=1, math.min(area.h, shown_count) do - local i = row + store.scroll - local name = filtered_names[i] - local count = store.all_items[name] - local item_info = self.item_details[name] - local count_width = math.floor(math.log(count, 10) + 1) - if i == store.selected_idx and active then - term.setTextColor(colors.yellow) - else - term.setTextColor(colors.white) + 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 - term.setCursorPos((max_count_width+1 - count_width), row) - term.write(count) - term.setCursorPos(max_count_width+2, row) - term.write(ensure_width(item_info.display_name, area.w-max_count_width-1)) + count_collumn_width = get_number_width(max_count) end - if shown_count > area.h then - local scroll_height = math.max(1, area.h/shown_count*area.h) - local y = 1+store.scroll/shown_count*area.h - paintutils.drawLine(area.w, y, area.w, y+scroll_height-1, colors.lightGray) + 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 + term.setCursorPos((count_collumn_width+1 - count_width), row) + term.write(count) + 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] @@ -195,72 +435,12 @@ function main_view:move_item(item_name, amount, destination) return moved_amount end -function main_view:update_selected_item(store, area, idx) - local filtered_count = #store.filtered_names - store.selected_idx = clamp(idx, 1, filtered_count) - - local margin = 2 - local item_list_height = area.h-1 - if store.selected_idx > store.scroll+item_list_height-margin then - store.scroll = math.min(store.selected_idx-item_list_height+margin, filtered_count-item_list_height) - elseif store.selected_idx <= store.scroll+margin then - store.scroll = math.max(store.selected_idx-1-margin, 0) - end -end - -function main_view:select_item(store, area, item_name) - for i, name in ipairs(store.filtered_names) do - if name == item_name then - self:update_selected_item(store, area, i) - return - end - end -end - -function main_view:refresh_filtered_names(store, area) - store.filtered_names = main_view:list_filtered_names(store.all_items, store.search_bar or "") - self:update_selected_item(store, area, store.selected_idx) -end - -function main_view:display_items_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 - if ui.event[1] == "key" then - local key = ui.event[2] - if key == keys.down or (main_view.ctrl_down and key == keys.j) then - store.selected_idx = math.min(store.selected_idx + 1, #store.filtered_names) - elseif key == keys.up or (main_view.ctrl_down and key == keys.k) then - store.selected_idx = math.max(store.selected_idx - 1, 1) - end - - local margin = 2 - local item_list_height = area.h-1 - if store.selected_idx > store.scroll+item_list_height-margin then - store.scroll = math.min(store.selected_idx-item_list_height+margin, #store.filtered_names - item_list_height) - elseif store.selected_idx <= store.scroll+margin then - store.scroll = math.max(store.selected_idx-1-margin, 0) - end - end - end - - term.setBackgroundColor(colors.black) - main_view:display_item_list(store, rect(1, 2, area.w, area.h-1), active) - - term_stack.pop_cursor() -end - function main_view:has_selected_items() - for _, count in pairs(main_view.right_store.all_items) do - if count > 0 then - return true - end + 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 @@ -289,7 +469,41 @@ function main_view:deposit_items() self:refresh_items() end -function main_view:draw() +function main_view:save_bundle(name, items) + self.bundles[name] = items +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 + self:save_bundle(self.bundle_name, self.right_store.items) + end +end + +function main_view:run() term.setBackgroundColor(colors.black) term.clear() @@ -298,11 +512,28 @@ function main_view:draw() term.setTextColor(colors.lightGray) vline(self.split_x, self.main_area.y, self.main_area.h) - main_view:display_items_with_search(self.left_store, self.left_area, self.is_left_active) - main_view:display_items_with_search(self.right_store, self.right_area, not self.is_left_active) + 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_items_with_search(self.left_store, self.main_area, true) + 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, ...) @@ -319,23 +550,32 @@ function main_view:on_event(event, ...) end function main_view:on_key(key) - local left_selected = self.left_store.filtered_names[self.left_store.selected_idx] - local right_selected = self.right_store.filtered_names[self.right_store.selected_idx] - local selected_item = self.is_left_active and left_selected or right_selected + 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 - elseif key == keys.tab then - self.is_left_active = not self.is_left_active - elseif key == keys.f5 then - self:refresh_items() end - if self.ctrl_down then + if not self.bundle_popup then local has_selected_items = self:has_selected_items() - if key == keys.space then + + if key == keys.n and 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 if has_selected_items then for name, count in pairs(self.right_store.all_items) do local transferred = self:move_item(name, count, self.result_inventory) @@ -345,42 +585,20 @@ function main_view:on_key(key) self:refresh_filtered_names(self.left_store, self.left_area) self:refresh_filtered_names(self.right_store, self.right_area) else - if left_selected then - local item = left_selected + local selected_item = self.left_store.filtered_names[self.left_store.selected_idx] + if selected_item then + local item = selected_item local stack_size = self.item_details[item].stack_size local transferred_count = self:move_item(item, stack_size, self.result_inventory) self.left_store.all_items[item] = self.left_store.all_items[item] - transferred_count self:refresh_filtered_names(self.left_store, self.left_area) - self:select_item(self.left_store, self.left_area, item) + self:select_option(self.left_store, self.left_area, item) end end - elseif key == keys.d then + + elseif self.ctrl_down and key == keys.d then self:deposit_items() - end - if selected_item and (key == keys.l or key == keys.h) then - local item = selected_item - local stack_size = self.item_details[item].stack_size - local right_items = self.right_store.all_items - local left_items = self.left_store.all_items - - if key == keys.l then - local transferred = math.min(left_items[item], stack_size) - left_items[item] = left_items[item] - transferred - right_items[item] = (right_items[item] or 0) + transferred - elseif key == keys.h then - local transferred = math.min(right_items[item] or 0, stack_size) - left_items[item] = left_items[item] + transferred - right_items[item] = (right_items[item] or 0) - transferred - end - - self:refresh_filtered_names(self.left_store, self.left_area) - self:refresh_filtered_names(self.right_store, self.right_area) - if self.is_left_active then - self:select_item(self.left_store, self.left_area, item) - else - self:select_item(self.right_store, self.right_area, item) - end end end end