· last year · May 27, 2024, 09:00 PM
1--pastebin get ZLQnsGHR startup
2
3local modem = peripheral.wrap("bottom")
4local turtleName = modem.getNameLocal()
5local funcs = {}
6local itemList = {}
7local displayList = {}
8--[[
9 Helper function to format item name
10 Returns: Formatted name. Prefix removed, First letter capitalized, and underscores replaced with spaces
11]]
12local function formatItemName(name)
13 --remove mod ID prefix, capitalize first letter and replace underscores with spaces
14 return string.gsub(string.gsub(string.sub(name,string.find(name,":")+1),"^%l", string.upper),"_"," ")
15end
16
17--[[
18 Simple helper function to compare two strings
19 Returns: True / False if input a < b
20]]
21local function compareItemsAlpha(a,b)
22 --remove mod prefix and convert to lower characters
23 return string.lower(string.sub(a.name,string.find(a.name,":")+1)) < string.lower(string.sub(b.name,string.find(b.name,":")+1))
24end
25
26--[[
27 helper function to compare two strings wagnerFischer ranks.
28]]
29local function wagnerFischerCompare(a,b)
30 return a.searchRank > b.searchRank
31end
32
33--[[
34 String comparison function using optimized single row wagner fischer-levenshtein edit distance algorithm
35 Arguments: a, b
36 a = Main string we are using
37 b = String we want to check against
38 Returns:
39 integer, the calculated edit distance rank for
40]]
41local function wagnerFischerOptimizedRatioRank(a,b)
42
43 if a == b then return 100 end
44
45 local lenA = #a
46 local lenB = #b
47 local ranks = {}
48
49 --Initialize array for starting values
50 for i = 1, lenA+1 do
51 ranks[i] = i-1
52 end
53
54 --do ranking
55 for y = 2, lenB+1 do
56 local lastVal = y-1 --init last val
57 for x = 2, lenA+1 do
58 local min = math.min(ranks[x], ranks[x-1], lastVal) --compare 'delete', 'sub', and 'insertion' options
59 ranks[x-1] = lastVal --now that we're done with [x-1] we can override it
60 if a:sub(x-1, x-1) == b:sub(y-1, y-1) then
61 lastVal = min
62 else
63 lastVal = min+1
64 end
65 end
66 ranks[lenA+1] = lastVal --We've finished the entire row. Last thing to do is to save the last value. Smaller is better!
67 end
68 --calculate leven ratio and return
69 return 100 * ((lenA+lenB - ranks[lenA+1]) / (lenA+lenB)) --Bigger is better! Will be from 0 to 1
70end
71
72
73--[[
74 scans for inventory peripherals and creates lists for item names and creates storage structure
75 Returns: funcs, itemList, displayList
76 chests = array of wrapped peripherals that are type "inventory"
77 itemList = table of grouped items. Each index uses the items name as its identifier, and the data is a stack of all items of that type.
78 displayList = formatted list which is indexed in order to be displayed and sorted/ranked for searching
79]]
80local function getItemList()
81 --## Fill lists and funcs array, then unpack list in parallel ##
82 --also declare variables for function
83 local funcs = {}
84 local lists = {}
85 local displayList = {} --formated data structure
86 local itemList = {} --formated data structure
87
88 local chests = { peripheral.find("inventory") }
89 for i = 1, #chests do
90 funcs[i] = function()
91 lists[i] = chests[i].list()
92 end
93 end
94
95 for i=1,#funcs,64 do --using 64 for batch size. Change that number if needed
96 parallel.waitForAll(table.unpack(funcs,i,math.min(i+63,#funcs)))
97 end
98
99 parallel.waitForAll(table.unpack(funcs))
100
101 --## Format lists into special data structures for fast searching and displaying ##
102 --each index in lists is an array of items
103 --each item is an array that looks like: { name = ..., count = ...}
104 for i = 1, #lists do
105 local curInv = lists[i]--.list()
106 for slot,item in pairs(curInv) do
107 local name = item.name
108 --## check if item already exists ##
109 if not itemList[name] then
110 itemList[name] = {name = name, count = 0} --Create item index. We will store all items of the same name here
111 table.insert(displayList, {name = name, count = 0})
112 end
113 --## add extra data to item and add to list ##
114 item.chestID = i
115 item.slot = slot
116 itemList[name].count = itemList[name].count + item.count --update special data structure count
117 table.insert(itemList[name],item) --Insert item to item stack
118 end
119 end
120
121 --## Update displayList counts ##
122 for i = 1, #displayList do
123 displayList[i].count = itemList[displayList[i].name].count
124 end
125
126 --## Sort displayList alphabetically ##
127 table.sort(displayList,compareItemsAlpha)
128
129 return chests, itemList, displayList
130end
131
132--[[
133 Searches for searchTarget and constructs a list of items that match
134 Args: searchTarget, itemList
135 searchTarget = String. Item name to search for
136 itemList = table of items to search through
137 Returns: constructed array of items from search
138]]
139local function search(searchTarget,itemList)
140 if searchTarget == "" then return itemList end
141
142 local formatedTarget = string.lower(string.gsub(searchTarget," ","_")) --convert to lower case and replace spaces with underscores
143 local rankedItems = {}
144
145 for _,item in pairs(itemList) do
146 local formattedName = string.lower(string.sub(item.name,string.find(item.name,":")+1)) --use formatted name
147
148 local ratio = #formatedTarget / #formattedName
149 if ratio < .96 then --Do partial string similarity checks
150 local bestRank = 0
151 for i = 1, #formattedName - #formatedTarget+1 do
152 local str = formattedName:sub(i,#formatedTarget-1+i)
153
154 local rank = wagnerFischerOptimizedRatioRank(formatedTarget,str)
155
156 if rank > bestRank then
157 bestRank = rank
158 end
159 end
160 item.name = item.name
161 item.searchRank = bestRank - (#formattedName - #formatedTarget)
162 else
163 item.searchRank = wagnerFischerOptimizedRatioRank(formatedTarget,formattedName)
164 end
165 --item.count = math.floor(item.searchRank) --override count to debug search ranking
166 table.insert(rankedItems, item )
167 end
168
169 --## Sort rankedItems by search rank ##
170 table.sort(rankedItems,wagnerFischerCompare)
171 return rankedItems
172end
173
174--[[
175 Attempt to pull 64 of an item into turtle inventory.
176 Inputs: currentMenu, cursor, count
177 currentMenu: display menu currently used
178 cursor: Integer that represents where the cursor is in the currentMenu
179 count: Integer that represents how many items to pull
180]]
181local function extract(currentMenu,cursor,count)
182 local itemStack = itemList[currentMenu[cursor].name]
183 if not itemStack then
184 return
185 end
186
187 for i = #itemStack, 1, -1 do --for each item in the stack
188
189 local item = itemStack[i]
190 local chestWrap = funcs[item.chestID]
191 local slot = item.slot
192 local itemCount = item.count
193
194 --Try to push to the turtle inventory
195 local moved = chestWrap.pushItems(turtleName, slot, count)
196 turtle.select(1)
197 turtle.drop()
198 count = count - moved
199
200 --Check if moved all required
201 if count <= 0 then
202 break
203 end
204 end
205end
206
207--## Main program ##
208funcs, itemList, displayList = getItemList()
209
210local _,screenHeight = term.getSize()
211local pageLength = screenHeight - 3
212
213local cursor = 1
214local searchTarget = ""
215local needToSearch = false
216local currentMenu = displayList
217
218local keys_held = {}
219local updateBool = false
220local dumpDisabler = false
221
222--[[
223 Function to listen for keys and update keys_held list
224 Runs in parallel with other main functions
225]]
226local function key_Listener()
227 while true do
228 local event, key = os.pullEvent()
229 if event == "key" then
230 keys_held[key] = true
231 elseif event == "key_up" then
232 keys_held[key] = nil
233 end
234 end
235end
236
237--[[
238 Attempt to move all items from turtle inventory into inventory network
239 Runs in parallel with other main functions
240]]
241local function lookToDump()
242 local overflow = false
243 while true do
244 if not overflow then
245 sleep(1)
246 else
247 overflow = false
248 end
249 if not dumpDisabler then
250 local updateBool = false
251 local overFlowCount = 0
252 for i = 1, 16 do
253 --local count = turtle.getItemCount(i)
254 local item = turtle.getItemDetail(i)
255 if item then
256 overFlowCount = overFlowCount + 1
257 updateBool = true
258 local count = item.count
259 local moved
260 --[[
261 if itemList[item.name] then
262 local itemStack = itemList[item.name]
263 for j = 1, #itemStack do
264 local chestWrap = funcs[itemList[item.name][j].chestID]
265 moved = chestWrap.pullItems(turtleName,i)
266 if moved == 0 then
267
268 end
269 end
270 ]]
271
272 --else
273 for j = 1, #funcs do
274 local chestWrap = funcs[j]
275 moved = chestWrap.pullItems(turtleName,i)
276 if moved == 0 then
277 if turtle.getItemCount(i) == 0 then
278 break
279 end
280 end
281 count = count - moved
282 if count == 0 then
283 break
284 end
285 end
286 --end
287 end
288 end
289 if overFlowCount >= 14 then
290 overflow = true
291 end
292 if updateBool and not overflow then
293 funcs, itemList, displayList = getItemList()
294 updateBool = false
295 end
296 end
297 end
298end
299
300--[[
301 Periodically update item list
302 Runs in parallel with other main functions
303]]
304local function updateList_Listener()
305 while true do
306 sleep(10)
307 funcs, itemList, displayList = getItemList()
308 end
309end
310
311local function update_Display()
312 --## Display header for instant response feeling ##
313 term.setCursorPos(1,1)
314 term.setTextColor(colors.lightBlue)
315 print("<<< Search: "..searchTarget.." >>>")
316
317 --## Set page lengths, start and end indexes for item display ##
318 cursor = math.max(cursor,1) --clamp cursor
319 if needToSearch then
320 if searchTarget == "" then
321 currentMenu = displayList
322 else
323 currentMenu = search(searchTarget,itemList)
324 end
325 needToSearch = false
326 end
327 local maxPage = math.ceil(#currentMenu/(pageLength))
328 local page = math.min( math.ceil( cursor / pageLength ) ,maxPage)
329 local displayStartIndex = math.max( pageLength*(page-1)+1, 1 )
330 local displayEndIndex = math.min( pageLength*(page), #currentMenu )
331 cursor = math.min(cursor,displayEndIndex) --clamp cursor
332
333 --## Display page ##
334 term.setCursorPos(1,1)
335 term.clear()
336 print("<<< Search: "..searchTarget.." >>>") --"<<< Search: " is 12 chars long
337 --Display item options
338 term.setTextColor(colors.white)
339 for i = displayStartIndex, displayEndIndex do
340 --Left align item count. For name, removes prefix, capitalizes first letter, replaces underscores w/ spaces
341 local displayStr = string.format("%6s", tostring(currentMenu[i].count)).." : "..formatItemName(currentMenu[i].name)
342 if i == cursor then
343 term.setTextColor(colors.pink)
344 print(">"..displayStr.." <")
345 term.setTextColor(colors.white)
346 else
347 print(" "..displayStr.." ")
348 end
349 end
350 term.setTextColor(colors.purple)
351 print("("..page.."/"..maxPage..")")
352 term.setCursorPos(13+#searchTarget,1) --Move cursor back to "<<< Search: " part
353end
354
355term.setCursorBlink(true)
356
357local function main()
358 while true do
359 --## Display header for instant response feeling ##
360 term.setCursorPos(1,1)
361 term.setTextColor(colors.lightBlue)
362 print("<<< Search: "..searchTarget.." >>>")
363
364 --## Set page lengths, start and end indexes for item display ##
365 cursor = math.max(cursor,1) --clamp cursor
366 if needToSearch then
367 if searchTarget == "" then
368 currentMenu = displayList
369 else
370 currentMenu = search(searchTarget,itemList)
371 end
372 needToSearch = false
373 end
374 local maxPage = math.ceil(#currentMenu/(pageLength))
375 local page = math.min( math.ceil( cursor / pageLength ) ,maxPage)
376 local displayStartIndex = math.max( pageLength*(page-1)+1, 1 )
377 local displayEndIndex = math.min( pageLength*(page), #currentMenu )
378 cursor = math.min(cursor,displayEndIndex) --clamp cursor
379
380 --## Display page ##
381 term.setCursorPos(1,1)
382 term.clear()
383 print("<<< Search: "..searchTarget.." >>>") --"<<< Search: " is 12 chars long
384 --Display item options
385 term.setTextColor(colors.white)
386 for i = displayStartIndex, displayEndIndex do
387 --Left align item count. For name, removes prefix, capitalizes first letter, replaces underscores w/ spaces
388 local displayStr = string.format("%6s", tostring(currentMenu[i].count)).." : "..formatItemName(currentMenu[i].name)
389 if i == cursor then
390 term.setTextColor(colors.pink)
391 print(">"..displayStr.." <")
392 term.setTextColor(colors.white)
393 else
394 print(" "..displayStr.." ")
395 end
396 end
397 term.setTextColor(colors.purple)
398 print("("..page.."/"..maxPage..")")
399 term.setCursorPos(13+#searchTarget,1) --Move cursor back to "<<< Search: " part
400
401 --## Handle user input ##
402 local event, key, x, y = os.pullEvent()
403
404 if keys_held[keys.leftCtrl] and key == keys.d then
405 searchTarget = ""
406 elseif keys_held[keys.leftCtrl] and key == keys.enter then
407 if #currentMenu > 0 then
408 dumpDisabler = true
409 extract(currentMenu,cursor,math.min(currentMenu[cursor].count,192))
410 dumpDisabler = false
411 --## Update item list ##
412 funcs, itemList, displayList = getItemList()
413 needToSearch = true
414 end
415 elseif event == "mouse_click" then
416 local itemIndex = displayStartIndex + (y - 2)
417 if #currentMenu > 0 and displayStartIndex > 0 and itemIndex <= displayEndIndex and currentMenu[itemIndex] then
418 local pullAmount = 64
419 if key == 2 then
420 pullAmount = 1
421 end
422 dumpDisabler = true
423 extract(currentMenu,itemIndex,pullAmount)
424 dumpDisabler = false
425 --[[
426 currentMenu[itemIndex].count = currentMenu[itemIndex].count - pullAmount
427 if currentMenu[itemIndex].count <= 0 then --Remove item from lists
428 local itemName = currentMenu[itemIndex].name
429 table.remove(currentMenu,itemIndex)
430 itemList[itemName] = nil
431 for i = 1, #currentMenu do
432 if currentMenu[i] == itemName then
433 table.remove(currentMenu,i)
434 break
435 end
436 end
437 end
438 ]]
439 --## Update item list ##
440 funcs, itemList, displayList = getItemList()
441 needToSearch = true
442 end
443 elseif event == "mouse_scroll" then
444 if key == -1 then --Page right
445 if page < maxPage then
446 page = page + 1
447 cursor = cursor + pageLength
448 term.clear()
449 end
450 elseif key == 1 then --Page left
451 if page > 1 then
452 page = page - 1
453 cursor = cursor - pageLength
454 term.clear()
455 end
456 end
457 elseif event == "key" then
458 if key == keys.up then --Move cursor up if it's not at first option
459 if cursor > displayStartIndex then
460 cursor = cursor - 1
461 end
462 elseif key == keys.down then --move cursor down if it's not at last option
463 if cursor < displayEndIndex then
464 cursor = cursor + 1
465 end
466 elseif key == keys.left then --move to previous page if not at first page
467 if page > 1 then
468 page = page - 1
469 cursor = cursor - pageLength
470 term.clear()
471 end
472 elseif key == keys.right then --move to next page if not at last page
473 if page < maxPage then
474 page = page + 1
475 cursor = cursor + pageLength
476 term.clear()
477 end
478 elseif key == keys.enter then --attempt to pull item
479 if #currentMenu > 0 then
480 dumpDisabler = true
481 extract(currentMenu,cursor,64)
482 dumpDisabler = false
483 --[[
484 currentMenu[cursor].count = currentMenu[cursor].count - 64
485 if currentMenu[cursor].count <= 0 then --Remove item from lists
486 local itemName = currentMenu[cursor].name
487 table.remove(currentMenu,cursor)
488 itemList[itemName] = nil
489 for i = 1, #currentMenu do
490 if currentMenu[i] == itemName then
491 table.remove(currentMenu,i)
492 break
493 end
494 end
495 end
496 ]]
497 --## Update item list ##
498 funcs, itemList, displayList = getItemList()
499 needToSearch = true
500 end
501 elseif key == keys.backspace then
502 if #searchTarget > 0 then
503 needToSearch = true
504 cursor = 1
505 searchTarget = string.sub(searchTarget,1,#searchTarget-1)
506 end
507 end
508 elseif event == "char" then
509 if #searchTarget < 20 then
510 needToSearch = true
511 cursor = 1
512 searchTarget = searchTarget..key
513 end
514 end
515 end
516end
517
518term.clear()
519term.setCursorPos(1,1)
520parallel.waitForAny(
521 key_Listener,
522 main,
523 updateList_Listener,
524 lookToDump
525)