Advertisement
nagoL2015

Turtlegistics

Feb 19th, 2018
234
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 15.97 KB | None | 0 0
  1. assert(turtle, "Program must be run on a turtle")
  2.  
  3. local logFile = fs.combine(fs.getDir(shell.getRunningProgram()), "turtlegistics.log")
  4. local logEnabled = settings.get("turtlegistics.log", true)
  5.  
  6. if logEnabled then
  7.     fs.delete(logFile)
  8. end
  9.  
  10. --[[ Write some data to the log file ]]
  11. local function log(...)
  12.     if logEnabled then
  13.         local args = { ... }
  14.  
  15.         local f = fs.open(logFile, "a")
  16.  
  17.         for i, v in ipairs(args) do
  18.             f.write(tostring(v))
  19.         end
  20.  
  21.         f.writeLine()
  22.  
  23.         f.close()
  24.     end
  25. end
  26.  
  27. local function getChests()
  28.     log("Getting chests")
  29.  
  30.     local names = peripheral.getNames()
  31.  
  32.     local chestCount = 0
  33.     local chests = {}
  34.  
  35.     for _, name in ipairs(names) do
  36.         if not name:match("^turtle") then
  37.             local wrapped = peripheral.wrap(name)
  38.  
  39.             if wrapped.getTransferLocations then
  40.                 log("Found chest " .. name)
  41.                 chests[name] = wrapped
  42.                 chestCount = chestCount + 1
  43.             end
  44.         end
  45.     end
  46.  
  47.     return chests, chestCount
  48. end
  49.  
  50. local function newStack(meta)
  51.     local stack = {}
  52.  
  53.     stack.name = meta.name
  54.     stack.displayName = meta.displayName
  55.     stack.ores = meta.ores
  56.     stack.count = meta.count
  57.     stack.stackSize = meta.maxCount
  58.     stack.damage = meta.damage
  59.     stack.from = {} -- to be filled later - table of places where this stack is stored
  60.  
  61.     return setmetatable(stack, {
  62.         __tostring = function(self)
  63.             return self.displayName .. " x" .. stack.count
  64.         end
  65.     })
  66. end
  67.  
  68. local function getItemStacks(chests)
  69.     local items = {}
  70.  
  71.     for name, chest in pairs(chests) do
  72.         for slot, _ in pairs(chest.list()) do
  73.             local meta = chest.getItemMeta(slot)
  74.             local idx = meta.name .. ";" .. meta.damage
  75.  
  76.             if items[idx] ~= nil then
  77.                 items[idx].count = items[idx].count + meta.count
  78.             else
  79.                 items[idx] = newStack(meta)
  80.             end
  81.  
  82.             table.insert(items[idx].from, {
  83.                 name = name,
  84.                 slot = slot,
  85.                 count = meta.count
  86.             })
  87.         end
  88.     end
  89.  
  90.     return items
  91. end
  92.  
  93. local state = {}
  94.  
  95. state.chestCount = 0
  96. state.chests = {}
  97.  
  98. state.stacks = {}
  99.  
  100. state.sortMode = "amount"
  101. state.displayStacks = {}
  102. state.pageSize = 0 -- updated later in state:render
  103. state.selectedStack = 1
  104.  
  105. state.search = ""
  106. state.searchCursor = 1
  107. state.didWithdraw = false
  108.  
  109. function state:updateDisplayStacks()
  110.     self.displayStacks = {}
  111.  
  112.     -- filter according to search
  113.     for i, stack in pairs(self.stacks) do
  114.         if self.search == "" or stack.displayName:lower():match(self.search) then
  115.             table.insert(self.displayStacks, stack)
  116.         end
  117.     end
  118.  
  119.     -- sort filtered results
  120.     local comparator
  121.  
  122.     if self.sortMode == "amount" then
  123.         comparator = function(a, b) return b.count < a.count end
  124.     elseif self.sortMode == "lexical" then
  125.         comparator = function(a, b) return a.displayName < b.displayName end
  126.     end
  127.  
  128.     table.sort(self.displayStacks, comparator)
  129. end
  130.  
  131. function state:refresh()
  132.     log("Refreshing inventory")
  133.     self.chests, self.chestCount = getChests()
  134.     self.stacks = getItemStacks(self.chests)
  135.     self:updateDisplayStacks()
  136.     self:render()
  137. end
  138.  
  139. local function themeComponent(bg, fg)
  140.     local component = {}
  141.  
  142.     component.bg = bg
  143.     component.fg = fg
  144.  
  145.     function component:apply()
  146.         term.setBackgroundColor(self.bg)
  147.         term.setTextColor(self.fg)
  148.     end
  149.  
  150.     return component
  151. end
  152.  
  153. local function formatNumber(n)
  154.     if n >= 1000 then
  155.         return string.format("%.1fk", n)
  156.     else
  157.         return tostring(n)
  158.     end
  159. end
  160.  
  161. local theme = {
  162.     primary = themeComponent(colors.gray, colors.white),
  163.     primary_muted = themeComponent(colors.gray, colors.lightGray),
  164.     secondary = themeComponent(colors.black, colors.white),
  165.     secondary_muted = themeComponent(colors.black, colors.lightGray)
  166. }
  167.  
  168. function state:render(message)
  169.     local w, h = term.getSize()
  170.  
  171.     term.setCursorPos(1,1)
  172.     theme.secondary:apply()
  173.     term.clear()
  174.     term.setCursorBlink(false)
  175.  
  176.     -- draw status bar
  177.     -- already at 1,1
  178.     theme.primary:apply()
  179.     term.clearLine()
  180.     term.write("Turtlegistics")
  181.  
  182.     local chestStatus = self.chestCount == 1 and "1 chest" or ("%d chests"):format(self.chestCount)
  183.  
  184.     local numDisplayStacks = #self.displayStacks
  185.     self.selectedStack = math.max(1, math.min(self.selectedStack, numDisplayStacks))
  186.  
  187.     self.pageSize = h - 3
  188.     local totalPages = math.ceil(numDisplayStacks / self.pageSize)
  189.     local pageOffset = math.floor((self.selectedStack - 1) / self.pageSize)
  190.     local pageStatus = ("Page %d/%d"):format(pageOffset + 1, totalPages)
  191.  
  192.     local statusText = ("%s | %s"):format(chestStatus, pageStatus)
  193.     term.setCursorPos(w - statusText:len(), 1)
  194.     theme.primary_muted:apply()
  195.     term.write(statusText)
  196.  
  197.     for i = 1, self.pageSize do
  198.         local stackidx = (pageOffset * self.pageSize) + i
  199.         local stack = self.displayStacks[stackidx]
  200.  
  201.         term.setCursorPos(1, i + 1)
  202.         if stack and stackidx == self.selectedStack then theme.primary:apply() else theme.secondary:apply() end
  203.         term.clearLine()
  204.  
  205.         if stack then
  206.             term.write(stack.displayName)
  207.  
  208.             term.setCursorPos(w - 4, i + 1)
  209.             if stackidx == self.selectedStack then theme.primary_muted:apply() else theme.secondary_muted:apply() end
  210.             term.write(formatNumber(stack.count))
  211.         end
  212.     end
  213.  
  214.     -- draw help bar
  215.     term.setCursorPos(1, h)
  216.     theme.primary_muted:apply()
  217.     term.clearLine()
  218.     term.write("F5 scan F6 sort F7 insert F8 quit")
  219.  
  220.     -- draw search or message bar
  221.     term.setCursorPos(1, h - 1)
  222.     if message then
  223.         theme.primary:apply()
  224.         term.clearLine()
  225.         term.write(message)
  226.     else
  227.         if self.search == "" then
  228.             theme.primary_muted:apply()
  229.             term.clearLine()
  230.             term.write("Type to search, enter to pull stack")
  231.         else
  232.             theme.primary:apply()
  233.             term.clearLine()
  234.             term.write(self.search)
  235.             term.setCursorPos(self.searchCursor, h - 1)
  236.             term.setCursorBlink(true)
  237.         end
  238.     end
  239. end
  240.  
  241. function state:withdraw(n)
  242.     local stack = self.displayStacks[self.selectedStack]
  243.  
  244.     if stack ~= nil then
  245.         log(("Withdrawing %s x%d"):format(stack.displayName, stack.stackSize))
  246.         self:render("Withdrawing " .. stack.displayName)
  247.  
  248.         local needed = n or stack.stackSize
  249.         local totalWithdrawn = 0
  250.         local from = {}
  251.  
  252.         for i, source in ipairs(stack.from) do
  253.             local amount = math.min(needed, source.count)
  254.  
  255.             if amount > 0 then
  256.                 local chest = self.chests[source.name]
  257.  
  258.                 local targets = chest.getTransferLocations()
  259.                 local target
  260.  
  261.                 for i, v in ipairs(targets) do
  262.                     if v:match("^turtle") then
  263.                         target = v
  264.                         break
  265.                     end
  266.                 end
  267.  
  268.                 assert(target, "No connection between turtle and " .. source.name)
  269.  
  270.                 local transferred = chest.pushItems(target, source.slot, needed)
  271.                 totalWithdrawn = totalWithdrawn + transferred
  272.                 source.count = math.max(0, source.count - transferred)
  273.                 log(("Transferred %s x%d from %s:%d to %s"):format(stack.displayName, transferred, source.name, source.slot, target))
  274.  
  275.                 needed = math.max(0, needed - transferred)
  276.             end
  277.            
  278.             if needed == 0 then break end
  279.         end
  280.  
  281.         stack.count = math.max(0, stack.count - totalWithdrawn)
  282.         self.didWithdraw = true
  283.         self:updateDisplayStacks()
  284.         self:render()
  285.     end
  286. end
  287.  
  288. function state:deposit()
  289.     self:render("Inserting...")
  290.  
  291.     for turtleSlot = 1, 16 do
  292.         local turtleStack = turtle.getItemDetail(turtleSlot)
  293.  
  294.         if turtleStack then
  295.             local idx = turtleStack.name .. ";" .. turtleStack.damage
  296.             local amount = turtleStack.count
  297.             log(("Inserting %s x%d"):format(idx, amount))
  298.             local totalDeposited = 0
  299.  
  300.             local networkStack = self.stacks[idx]
  301.  
  302.             -- fill existing partial stacks
  303.             if networkStack then
  304.                 for i, source in ipairs(networkStack.from) do
  305.                     local freeSpace = math.max(0, networkStack.stackSize - source.count)
  306.  
  307.                     if freeSpace > 0 then
  308.                         local chest = self.chests[source.name]
  309.  
  310.                         local targets = chest.getTransferLocations()
  311.                         local target
  312.  
  313.                         for i, v in ipairs(targets) do
  314.                             if v:match("^turtle") then
  315.                                 target = v
  316.                                 break
  317.                             end
  318.                         end
  319.  
  320.                         assert(target, "No connection between turtle and " .. source.name)
  321.  
  322.                         local transferred = chest.pullItems(target, turtleSlot, nil, source.slot)
  323.                         totalDeposited = totalDeposited + transferred
  324.                         source.count = source.count + transferred
  325.                         log(("Transferred %s x%d from %s to %s:%d"):format(networkStack.displayName, transferred, target, source.name, source.slot))
  326.  
  327.                         amount = math.max(0, amount - transferred)
  328.                     end
  329.  
  330.                     if amount == 0 then break end
  331.                 end
  332.             end
  333.  
  334.             if amount > 0 then
  335.                 -- we need to use an empty slot
  336.                 for name, chest in pairs(self.chests) do
  337.                     local maxSlot = chest.size()
  338.  
  339.                     if maxSlot > 0 then
  340.                         local contents = chest.list()
  341.  
  342.                         for chestSlot = 1, maxSlot do
  343.                             if contents[chestSlot] == nil then
  344.                                 -- found a free slot!
  345.                                 local targets = chest.getTransferLocations()
  346.                                 local target
  347.  
  348.                                 for i, v in ipairs(targets) do
  349.                                     if v:match("^turtle") then
  350.                                         target = v
  351.                                         break
  352.                                     end
  353.                                 end
  354.  
  355.                                 assert(target, "No connection between turtle and " .. name)
  356.  
  357.                                 local transferred = chest.pullItems(target, turtleSlot, nil, chestSlot)
  358.                                 totalDeposited = totalDeposited + transferred
  359.                                 log(("Transferred %s x%d from %s to %s:%d (previously empty slot)"):format(idx, amount, target, name, chestSlot))
  360.  
  361.                                 amount = math.max(0, amount - transferred)
  362.  
  363.                                 if transferred > 0 then
  364.                                     if networkStack == nil then
  365.                                         -- create the network stack
  366.                                         local meta = chest.getItemMeta(chestSlot)
  367.                                         networkStack = newStack(meta)
  368.  
  369.                                         networkStack.count = 0 -- this value is set later before the function exits
  370.  
  371.                                         -- store the new stack
  372.                                         self.stacks[idx] = networkStack
  373.                                     end
  374.                                        
  375.                                     table.insert(networkStack.from, {
  376.                                         name = name,
  377.                                         slot = chestSlot,
  378.                                         count = transferred
  379.                                     })
  380.  
  381.                                     if amount == 0 then break end
  382.                                 end
  383.                             end
  384.                         end
  385.                     end
  386.  
  387.                     if amount == 0 then break end
  388.                 end
  389.             end
  390.  
  391.             if networkStack then networkStack.count = networkStack.count + totalDeposited end
  392.             self:updateDisplayStacks()
  393.             self:render()
  394.         end
  395.     end
  396. end
  397.  
  398. log("Starting turtlegistics")
  399.  
  400. state:render("Starting...")
  401. state:refresh()
  402.  
  403. while true do
  404.     local evt = { os.pullEvent() }
  405.  
  406.     if evt[1] == "mouse_scroll" then
  407.         state.selectedStack = state.selectedStack + (3 * evt[2])
  408.         state:render()
  409.     elseif evt[1] == "key" then
  410.         -- hotkeys
  411.         if evt[2] == keys.f8 then -- exit
  412.             break
  413.         elseif evt[2] == keys.f5 then -- refresh
  414.             state:render("Refreshing...")
  415.             state:refresh()
  416.         elseif evt[2] == keys.f6 then -- change sort mode
  417.             if state.sortMode == "amount" then
  418.                 state.sortMode = "lexical"
  419.             else
  420.                 state.sortMode = "amount"
  421.             end
  422.             state:render("Sorting...")
  423.             state:updateDisplayStacks()
  424.             state:render()
  425.         elseif evt[2] == keys.enter then -- withdraw
  426.             state:withdraw()
  427.         elseif evt[2] == keys.f7 then -- deposit
  428.             state:deposit()
  429.         end
  430.  
  431.         -- navigation
  432.         if evt[2] == keys.pageDown then
  433.             state.selectedStack = state.selectedStack + state.pageSize
  434.             state:render()
  435.         elseif evt[2] == keys.pageUp then
  436.             state.selectedStack = state.selectedStack - state.pageSize
  437.             state:render()
  438.         elseif evt[2] == keys.down then
  439.             state.selectedStack = state.selectedStack + 1
  440.             state:render()
  441.         elseif evt[2] == keys.up then
  442.             state.selectedStack = state.selectedStack - 1
  443.             state:render()
  444.         end
  445.        
  446.         -- text manipulation for search bar
  447.         if evt[2] == keys.backspace then
  448.             if state.didWithdraw then
  449.                 state.search = ""
  450.                 state.searchCursor = 1
  451.                 state.didWithdraw = false
  452.             else
  453.                 local prefix = state.search:sub(1, math.max(0, state.searchCursor - 2))
  454.                 local suffix = state.search:sub(state.searchCursor)
  455.    
  456.                 state.search = prefix .. suffix
  457.                 state.searchCursor = math.max(state.searchCursor - 1, 1)
  458.             end
  459.  
  460.             state:updateDisplayStacks()
  461.             state:render()
  462.         elseif evt[2] == keys.delete then
  463.             local prefix = state.search:sub(1, math.max(0, state.searchCursor - 1))
  464.             local suffix = state.search:sub(state.searchCursor + 1)
  465.  
  466.             state.search = prefix .. suffix
  467.             state:updateDisplayStacks()
  468.             state:render()
  469.         elseif evt[2] == keys.left then
  470.             state.searchCursor = math.max(state.searchCursor - 1, 1)
  471.            
  472.             state:render()
  473.         elseif evt[2] == keys.right then
  474.             state.searchCursor = math.min(state.searchCursor + 1, state.search:len() + 1)
  475.  
  476.             state:render()
  477.         elseif evt[2] == keys.home then
  478.             state.searchCursor = 1
  479.  
  480.             state:render()
  481.         elseif evt[2] == keys["end"] then
  482.             state.searchCursor = state.search:len() + 1
  483.  
  484.             state:render()
  485.         end
  486.     elseif evt[1] == "char" then
  487.         state.search = state.search:sub(1, state.searchCursor) .. evt[2] .. state.search:sub(state.searchCursor + 1)
  488.         state.searchCursor = state.searchCursor + 1
  489.         state.didWithdraw = false
  490.  
  491.         state:updateDisplayStacks()
  492.         state:render()
  493.     end
  494. end
  495.  
  496. log("Exiting turtlegistics")
  497.  
  498. term.setBackgroundColor(colors.black)
  499. term.setTextColor(colors.white)
  500. term.clear()
  501. term.setCursorPos(1,1)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement