se7enek

AE2-Manager Fork

Sep 16th, 2020 (edited)
92
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. --
  2. -- Copyright 2019 KaseiFR <kaseifr@gmail.com>
  3. --
  4. -- Permission is hereby granted, free of charge, to any person obtaining a copy
  5. -- of this software and associated documentation files (the "Software"), to deal
  6. -- in the Software without restriction, including without limitation the rights
  7. -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. -- copies of the Software, and to permit persons to whom the Software is
  9. -- furnished to do so, subject to the following conditions:
  10. --
  11. -- The above copyright notice and this permission notice shall be included in all
  12. -- copies or substantial portions of the Software.
  13. --
  14. -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  20. -- SOFTWARE.
  21. --
  22.  
  23. local component = require('component')
  24. local computer = require('computer')
  25. local coroutine = require('coroutine')
  26. local event = require('event')
  27. local filesystem = require('filesystem')
  28. local serialization = require('serialization')
  29. local thread = require('thread')
  30. local tty = require('tty')
  31. local unicode = require('unicode')
  32. local GUI = require('GUI')
  33. -- local inspect = require('inspect')
  34.  
  35. -- Config --
  36.  
  37. -- Control how many CPUs to use. 0 is unlimited, negative to keep some CPU free, between 0 and 1 to reserve a share,
  38. -- and greater than 1 to allocate a fixed number.
  39. local allowedCpus = -2
  40. -- Maximum size of the crafting requests
  41. local maxBatch = 64
  42. -- How often to check the AE system, in second
  43. local fullCheckInterval = 10        -- full scan
  44. local craftingCheckInterval = 1     -- only check ongoing crafting
  45. -- Where to save the config
  46. local configPath = '/home/ae2-manager.cfg'
  47.  
  48. -- Global State --
  49.  
  50. -- array of recipe like { item, label, wanted, [current, crafting] }
  51. local recipes = {}
  52. -- various system status data
  53. local status = {}
  54. -- AE2 proxy
  55. local ae2
  56.  
  57. -- Functions --
  58.  
  59. function main()
  60.     local resetBColor, resetFColor = tty.gpu().getBackground(), tty.gpu().getForeground()
  61.  
  62.     initAe2()
  63.     loadRecipes()
  64.     ae2Run(true)
  65.  
  66.     local app = buildGui()
  67.     app:draw(true)
  68.  
  69.     -- Start some background tasks
  70.     local background = {}
  71.     table.insert(background, event.listen("key_up", function (key, address, char)
  72.         if char == string.byte('q') then
  73.             event.push('exit')
  74.         end
  75.     end))
  76.     table.insert(background, event.listen("redraw", function (key) app:draw() end))
  77.     table.insert(background, event.listen("save", failFast(saveRecipes)))
  78.     table.insert(background, event.timer(craftingCheckInterval, failFast(checkCrafting), math.huge))
  79.     table.insert(background, thread.create(failFast(ae2Loop)))
  80.     table.insert(background, thread.create(failFast(function() app:start() end)))
  81.  
  82.     -- Block until we receive the exit signal
  83.     local _, err = event.pull("exit")
  84.  
  85.     -- Cleanup
  86.     app:stop()
  87.  
  88.     for _, b in ipairs(background) do
  89.         if type(b) == 'table' and b.kill then
  90.             b:kill()
  91.         else
  92.             event.cancel(b)
  93.         end
  94.     end
  95.  
  96.     tty.gpu().setBackground(resetBColor)
  97.     tty.gpu().setForeground(resetFColor)
  98.     tty.clear()
  99.  
  100.     if err then
  101.         io.stderr:write(err)
  102.         os.exit(1)
  103.     else
  104.         os.exit(0)
  105.     end
  106. end
  107.  
  108. function log(...)
  109.     -- TODO: reserve a part of the screen for logs
  110.     for i, v in ipairs{...} do
  111.         if i > 1 then io.stderr:write(' ') end
  112.         io.stderr:write(tostring(v))
  113.     end
  114.     io.stderr:write('\n')
  115. end
  116.  
  117. function logRam(msg)
  118.     --free, total = computer.freeMemory(), computer.totalMemory()
  119.     --log(msg, 'RAM', (total - free) * 100 / total, '%')
  120. end
  121.  
  122. function pretty(x)
  123.     return serialization.serialize(x, true)
  124. end
  125.  
  126. function failFast(fn)
  127.     return function(...)
  128.         local res = table.pack(xpcall(fn, debug.traceback, ...))
  129.         if not res[1] then
  130.             event.push('exit', res[2])
  131.         end
  132.         return table.unpack(res, 2)
  133.     end
  134. end
  135.  
  136. function initAe2()
  137.     local function test_ae2(id)
  138.         local proxy = component.proxy(id)
  139.         proxy.getCpus()
  140.         return proxy
  141.     end
  142.  
  143.     for id, type in pairs(component.list()) do
  144.         -- print('Testing ' .. type .. ' ' .. id)
  145.         local ok, p = pcall(test_ae2, id)
  146.         if ok then
  147.             print('Component ' .. type .. ' (' .. id .. ') is suitable')
  148.             ae2 = p
  149.         end
  150.     end
  151.  
  152.     if ae2 == nil then
  153.         error('No AE2 component found')
  154.     else
  155.         print('Using component ' .. ae2.type .. ' (' .. ae2.address .. ')')
  156.     end
  157. end
  158.  
  159. function loadRecipes()
  160.     print('Loading config from '..configPath)
  161.     local f, err = io.open(configPath, 'r')
  162.     if not f then
  163.         -- usually the file does not exist, on the first run
  164.         print('Loading failed:', err)
  165.         return
  166.     end
  167.  
  168.     local content = serialization.unserialize(f:read('a'))
  169.  
  170.     f:close()
  171.  
  172.     recipes = content.recipes
  173.     print('Loaded '..#recipes..' recipes')
  174. end
  175.  
  176. function saveRecipes()
  177.     local tmpPath = configPath..'.tmp'
  178.     local content = { recipes={} }
  179.  
  180.     for _, recipe in ipairs(recipes) do
  181.         if recipe.item ~= "packagedauto:package" or recipe.label ~= "Recipe Package" then
  182.         table.insert(content.recipes, {
  183.                 item = recipe.item,
  184.                 label = recipe.label,
  185.                 wanted = recipe.wanted,
  186.             })
  187.     end
  188.     end
  189.  
  190.     local f = io.open(tmpPath, 'w')
  191.     f:write(serialization.serialize(content))
  192.     f:close()
  193.  
  194.     filesystem.remove(configPath) -- may fail
  195.  
  196.     local ok, err = os.rename(tmpPath, configPath)
  197.     if not ok then error(err) end
  198. end
  199.  
  200. -- Main loop --
  201.  
  202. function ae2Loop()
  203.     while true do
  204.         local e1, e2 = event.pull(fullCheckInterval, 'ae2_loop')
  205.         logRam('loop')
  206.         --log('AE2 loop in')
  207.         ae2Run(e2 == 'reload_recipes')
  208.         --log('AE2 loop out')
  209.         event.push('redraw')
  210.     end
  211. end
  212.  
  213.  
  214. function ae2Run(learnNewRecipes)
  215.     local start = computer.uptime()
  216.     updateRecipes(learnNewRecipes)
  217.     logRam('recipes')
  218.     -- logRam('recipes (post-gc)')
  219.  
  220.     local finder = coroutine.create(findRecipeWork)
  221.     while hasFreeCpu() do
  222.         -- Find work
  223.         local _, recipe, needed, craft = coroutine.resume(finder)
  224.         if recipe then
  225.             -- Request crafting
  226.             local amount = math.min(needed, maxBatch)
  227.             --log('Requesting ' .. amount .. ' ' .. recipe.label)
  228.             recipe.crafting = craft.request(amount)
  229.             yield('yield crafting')
  230.             checkFuture(recipe) -- might fail very quickly (missing resource, ...)
  231.         else
  232.             break
  233.         end
  234.     end
  235.  
  236.     local duration = computer.uptime() - start
  237.     updateStatus(duration)
  238. end
  239.  
  240. function checkCrafting()
  241.     for _, recipe in ipairs(recipes) do
  242.         if checkFuture(recipe) then
  243.             --log('checkCrafting event !')
  244.             event.push('ae2_loop')
  245.             return
  246.         end
  247.     end
  248. end
  249.  
  250. function yield(msg)
  251.     --local gpu = tty.gpu()
  252.     --local _, h = gpu.getViewport()
  253.     --gpu.set(1, h, msg)
  254.     os.sleep()
  255. end
  256.  
  257. function updateRecipes(learnNewRecipes)
  258.     local start = computer.uptime()
  259.  
  260.     -- Index our recipes
  261.     local index = {}
  262.     for _, recipe in ipairs(recipes) do
  263.     if recipe.item ~= "packagedauto:package" or recipe.label ~= "Recipe Package" then
  264.             local key = itemKey(recipe.item, recipe.item.label ~= nil)
  265.             index[key] = { recipe=recipe, matches={} }
  266.     end
  267.     end
  268.     --log('recipe index', computer.uptime() - start)
  269.  
  270.     -- Get all items in the network
  271.     local items, err = ae2.getItemsInNetwork()  -- takes a full tick (to sync with the main thread?)
  272.     if err then error(err) end
  273.     --log('ae2.getItemsInNetwork', computer.uptime() - start, 'with', #items, 'items')
  274.  
  275.     -- Match all items with our recipes
  276.     for _, item in ipairs(items) do
  277.         local key = itemKey(item, item.hasTag)
  278.         local indexed = index[key]
  279.         if indexed then
  280.             table.insert(indexed.matches, item)
  281.         elseif learnNewRecipes and item.isCraftable and item.name ~= "packagedauto:package" then
  282.             local recipe = {
  283.                 item = {
  284.                     name = item.name,
  285.                     damage = math.floor(item.damage)
  286.                 },
  287.                 label = item.label,
  288.                 wanted = 0,
  289.             }
  290.             if item.hasTag then
  291.                 -- By default, OC doesn't expose items NBT, so as a workaround we use the label as
  292.                 -- an additional discriminant. This is not perfect (still some collisions, and locale-dependent)
  293.                 recipe.item.label = recipe.label
  294.             end
  295.             table.insert(recipes, recipe)
  296.             index[key] = { recipe=recipe, matches={item} }
  297.         end
  298.     end
  299.     --log('group items', computer.uptime() - start)
  300.  
  301.     -- Check the recipes
  302.     for _, entry in pairs(index) do
  303.         local recipe = entry.recipe
  304.         local matches = filter(entry.matches, function(e) return contains(e, recipe.item) end)
  305.         --log(recipe.label, 'found', #matches, 'matches')
  306.         local craftable = false
  307.         recipe.error = nil
  308.  
  309.         checkFuture(recipe)
  310.  
  311.         if #matches == 0 then
  312.             recipe.stored = 0
  313.         elseif #matches == 1 then
  314.             local item = matches[1]
  315.             recipe.stored = math.floor(item.size)
  316.             craftable = item.isCraftable
  317.         else
  318.             local id = recipe.item.name .. ':' .. recipe.item.damage
  319.             recipe.stored = 0
  320.             recipe.error = id .. ' match ' .. #matches .. ' items'
  321.             -- log('Recipe', recipe.label, 'matches:', pretty(matches))
  322.         end
  323.  
  324.         if not recipe.error and recipe.wanted > 0 and not craftable then
  325.             -- Warn the user as soon as an item is not craftable rather than wait to try
  326.             recipe.error = 'Not craftable'
  327.         end
  328.     end
  329.     --log('recipes check', computer.uptime() - start)
  330.  
  331.     if learnNewRecipes then
  332.         event.push('save')
  333.     end
  334. end
  335.  
  336. function itemKey(item, withLabel)
  337.     local key = item.name .. '$' .. math.floor(item.damage)
  338.     if withLabel then
  339.         --log('using label for', item.label)
  340.         key = key .. '$' .. item.label
  341.     end
  342.     return key
  343. end
  344.  
  345. function updateStatus(duration)
  346.     status.update = {
  347.         duration = duration
  348.     }
  349.  
  350.     -- CPU data
  351.     local cpus = ae2.getCpus()
  352.     status.cpu = {
  353.         all = #cpus,
  354.         free = 0,
  355.     }
  356.     for _, cpu in ipairs(cpus) do
  357.         status.cpu.free = status.cpu.free + (cpu.busy and 0 or 1)
  358.     end
  359.  
  360.     -- Recipe stats
  361.     status.recipes = {
  362.         error = 0,
  363.         crafting = 0,
  364.         queue = 0,
  365.     }
  366.     for _, recipe in ipairs(recipes) do
  367.         if recipe.error then
  368.             status.recipes.error = status.recipes.error + 1
  369.         elseif recipe.crafting then
  370.             status.recipes.crafting = status.recipes.crafting + 1
  371.         elseif (recipe.stored or 0) < (recipe.wanted or 0) then
  372.             status.recipes.queue = status.recipes.queue + 1
  373.         end
  374.     end
  375. end
  376.  
  377. function checkFuture(recipe)
  378.     if not recipe.crafting then return end
  379.  
  380.     local canceled, err = recipe.crafting.isCanceled()
  381.     if canceled or err then
  382.         --log('Crafting of ' .. recipe.label .. ' was cancelled')
  383.         recipe.crafting = nil
  384.         recipe.error = err or 'canceled'
  385.         return true
  386.     end
  387.  
  388.     local done, err = recipe.crafting.isDone()
  389.     if err then error('isDone ' .. err) end
  390.     if done then
  391.         --log('Crafting of ' .. recipe.label .. ' is done')
  392.         recipe.crafting = nil
  393.         return true
  394.     end
  395.  
  396.     return false
  397. end
  398.  
  399. function equals(t1, t2)
  400.     if t1 == t2 then return true end
  401.     if type(t1) ~= type(t2) or type(t1) ~= 'table' then return false end
  402.  
  403.     for k1, v1 in pairs(t1) do
  404.         local v2 = t2[k1]
  405.         if not equals(v1, v2) then return false end
  406.     end
  407.  
  408.     for k2, _ in pairs(t2) do
  409.         if t1[k2] == nil then return false end
  410.     end
  411.  
  412.     return true
  413. end
  414.  
  415. function filter(array, predicate)
  416.     local res = {}
  417.     for _, v in ipairs(array) do
  418.         if predicate(v) then table.insert(res, v) end
  419.     end
  420.     return res
  421. end
  422.  
  423. function contains(haystack, needle)
  424.     if haystack == needle then return true end
  425.     if type(haystack) ~= type(needle) or type(haystack) ~= 'table' then return false end
  426.  
  427.     for k, v in pairs(needle) do
  428.         if not contains(haystack[k], v) then return false end
  429.     end
  430.  
  431.     return true
  432. end
  433.  
  434. function hasFreeCpu()
  435.     local cpus = ae2.getCpus()
  436.     local free = 0
  437.     for i, cpu in ipairs(cpus) do
  438.         if not cpu.busy then free = free + 1 end
  439.     end
  440.     local ongoing = 0
  441.     for _, recipe in ipairs(recipes) do
  442.         if recipe.crafting then ongoing = ongoing + 1 end
  443.     end
  444.  
  445.     if enoughCpus(#cpus, ongoing, free) then
  446.         return true
  447.     else
  448.         --log('No CPU available')
  449.         return false
  450.     end
  451. end
  452.  
  453. function enoughCpus(available, ongoing, free)
  454.     if free == 0 then return false end
  455.     if ongoing == 0 then return true end
  456.     if allowedCpus == 0 then return true end
  457.     if allowedCpus > 0 and allowedCpus < 1 then
  458.         return  (ongoing + 1) / available <= allowedCpus
  459.     end
  460.     if allowedCpus >= 1 then
  461.         return ongoing < allowedCpus
  462.     end
  463.     if allowedCpus > -1 then
  464.         return (free - 1) / available <= -allowedCpus
  465.     end
  466.     return free > -allowedCpus
  467. end
  468.  
  469. function findRecipeWork() --> yield (recipe, needed, craft)
  470.     for i, recipe in ipairs(recipes) do
  471.         if recipe.error or recipe.crafting then goto continue end
  472.  
  473.         local needed = recipe.wanted - recipe.stored
  474.         if needed <= 0 then goto continue end
  475.  
  476.         yield('yield '..i)
  477.         local craftables, err = ae2.getCraftables(recipe.item)
  478.         --log('get_craftable', inspect(craftables))
  479.         if err then
  480.             recipe.error = 'ae2.getCraftables ' .. tostring(err)
  481.         elseif #craftables == 0 then
  482.             recipe.error = 'No crafting pattern found'
  483.         elseif #craftables == 1 then
  484.             coroutine.yield(recipe, needed, craftables[1])
  485.         else
  486.             recipe.error = 'Multiple crafting patterns'
  487.         end
  488.  
  489.         ::continue::
  490.     end
  491. end
  492.  
  493. function override(object, method, fn)
  494.     local super = object[method] or function() end
  495.     object[method] = function(...)
  496.         fn(super, ...)
  497.     end
  498. end
  499.  
  500. function numberValidator(str)
  501.     n = tonumber(str, 10)
  502.     return n and math.floor(n) == n
  503. end
  504.  
  505. -- Stay close to the 16 Minecraft colors in order to work on gold GPU/screen
  506. local C_BACKGROUND = 0x3C3C3C
  507. local C_STATUS_BAR = 0xC3C3C3
  508. local C_STATUS_TEXT = 0x1E1E1E
  509. local C_STATUS_PRESSED = 0xFFFF00
  510. local C_BADGE = 0xD2D2D2
  511. local C_BADGE_ERR = 0xFF4900 --0xFFB6FF
  512. local C_BADGE_BUSY = 0x336DFF
  513. local C_BADGE_SELECTED = 0xFFAA00
  514. local C_BADGE_TEXT = 0x1E1E1E
  515. local C_INPUT = 0xFFFFFF
  516. local C_INPUT_TEXT = 0x1E1E1E
  517. local C_SCROLLBAR = C_BADGE_SELECTED
  518. local C_SCROLLBAR_BACKGROUND = 0xFFFFFF
  519.  
  520. function buildGui()
  521.     local app = GUI.application()
  522.     local statusBar = app:addChild(GUI.container(1, 1, app.width, 1))
  523.     local window = app:addChild(GUI.container(1, 1 + statusBar.height, app.width, app.height - statusBar.height))
  524.  
  525.     window:addChild(GUI.panel(1, 1, window.width, window.height, C_BACKGROUND))
  526.     local columns = math.floor(window.width / 60) + 1
  527.  
  528.     -- Crating queue view
  529.     local craftingQueueView = window:addChild(GUI.layout(1, 1, window.width-1, window.height, columns, 1))
  530.     for i = 1, columns do
  531.         craftingQueueView:setAlignment(i, 1, GUI.ALIGNMENT_HORIZONTAL_CENTER, GUI.ALIGNMENT_VERTICAL_TOP)
  532.         craftingQueueView:setMargin(i, 1, .5, 1)
  533.     end
  534.  
  535.     override(craftingQueueView, 'draw', function(super, self, ...)
  536.         self.children = {}
  537.  
  538.         local added = 0
  539.         for _, recipe in ipairs(recipes) do
  540.             local color =
  541.             recipe.error and C_BADGE_ERR or
  542.                     recipe.crafting and C_BADGE_BUSY or
  543.                     (recipe.stored or 0) < recipe.wanted and C_BADGE
  544.  
  545.             if color then
  546.                 local badge = GUI.container(1, 1, math.floor(self.width / columns - 1), 4)
  547.                 self:setPosition(1 + added % columns, 1, self:addChild(badge))
  548.                 badge:addChild(GUI.panel(1, 1, badge.width, 4, color))
  549.                 badge:addChild(GUI.text(2, 2, C_BADGE_TEXT, recipe.label)) -- TODO: include the item icon ?
  550.                 badge:addChild(GUI.text(2, 3, C_BADGE_TEXT, string.format('%s / %s', recipe.stored or '?', recipe.wanted)))
  551.                 if recipe.error then
  552.                     badge:addChild(GUI.text(2, 4, C_BADGE_TEXT, tostring(recipe.error)))
  553.                     badge:moveToFront()
  554.                 end
  555.  
  556.                 added = added + 1
  557.             end
  558.         end
  559.  
  560.         super(self, ...)
  561.     end)
  562.  
  563.     -- Configuration view
  564.     local SYMBOL_CONFIG_RECIPE = {}
  565.     local configView = window:addChild(GUI.container(1, 1, window.width, window.height))
  566.     configView:addChild(GUI.panel(1, 1, configView.width, configView.height, C_BACKGROUND))
  567.     configView.hidden = true
  568.  
  569.     -- left panel (item select)
  570.     local itemListSearch = configView:addChild(GUI.input(2, 2, configView.width/2-1, 3,
  571.             C_INPUT, C_INPUT_TEXT, C_INPUT_TEXT, C_STATUS_PRESSED, C_INPUT_TEXT, '', 'Search'))
  572.  
  573.     -- TODO: add unconfigured/hidden filter
  574.  
  575.     local itemListPanel = configView:addChild(GUI.list(
  576.             itemListSearch.x, itemListSearch.y + itemListSearch.height + 1, itemListSearch.width, configView.height-itemListSearch.height-3,
  577.             1, 0, C_BADGE, C_BADGE_TEXT, C_STATUS_BAR, C_STATUS_TEXT, C_BADGE_SELECTED, C_BADGE_TEXT
  578.     ))
  579.     itemListPanel.selectedItem = -1
  580.     --itemListPanel:setAlignment(GUI.ALIGNMENT_HORIZONTAL_LEFT, GUI.ALIGNMENT_VERTICAL_TOP)
  581.     attachScrollbar(itemListPanel)
  582.  
  583.     override(itemListPanel, 'draw', function (super, self, ...)
  584.         self.selectedItem = -1
  585.         self.children = {}
  586.  
  587.         local selection = recipes
  588.         local filter = itemListSearch.text
  589.         if filter and filter ~= '' then
  590.             filter = unicode.lower(filter)
  591.             selection = {}
  592.             for _, recipe in ipairs(recipes) do
  593.                 -- Patterns seem very limited, no case-insensitive option
  594.                 if unicode.lower(recipe.label):find(filter) then
  595.                     table.insert(selection, recipe)
  596.                 end
  597.             end
  598.         end
  599.  
  600.         self.scrollBar.maximumValue = math.max(0, #selection - self.height)
  601.         self.scrollBar.shownValueCount =  self.scrollBar.maximumValue / (self.scrollBar.maximumValue + 1)
  602.  
  603.         local offset = self.scrollBar.value
  604.         for i = 1, math.min(self.height, #selection) do
  605.             local recipe = selection[offset + i]
  606.             local choice = self:addItem(recipe.label)
  607.             --choice.colors.default.background = (recipe.error ~= nil) and C_BADGE_ERR or recipe.wanted > 0 and C_BADGE_BUSY or C_BADGE
  608.             if recipe == configView[SYMBOL_CONFIG_RECIPE] then
  609.                 self.selectedItem = i
  610.             end
  611.             choice.onTouch = function(app, object)
  612.                 configView[SYMBOL_CONFIG_RECIPE] = recipe
  613.                 event.push('config_recipe_change')
  614.             end
  615.         end
  616.  
  617.         super(self, ...)
  618.     end)
  619.  
  620.     -- right panel (item details)
  621.     local reloadBtn = configView:addChild(GUI.button(configView.width/2+2, 2, configView.width/2-2, 3,
  622.                                                      C_BADGE, C_BADGE_TEXT, C_BADGE, C_STATUS_PRESSED, "Reload recipes"))
  623.     reloadBtn.onTouch = function(app, self)
  624.         event.push('ae2_loop', 'reload_recipes')
  625.     end
  626.     local itemConfigPanel = configView:addChild(GUI.layout(reloadBtn.x, reloadBtn.y + reloadBtn.height + 1, reloadBtn.width, configView.height-reloadBtn.height-7, 1, 1))
  627.     configView:addChild(GUI.panel(itemConfigPanel.x, itemConfigPanel.y, itemConfigPanel.width, itemConfigPanel.height, C_BADGE)):moveBackward()
  628.     itemConfigPanel:setAlignment(1, 1, GUI.ALIGNMENT_HORIZONTAL_CENTER, GUI.ALIGNMENT_VERTICAL_TOP)
  629.     itemConfigPanel:setMargin(1, 1, .5, 1)
  630.  
  631.     override(itemConfigPanel, 'eventHandler', function(super, app, self, key, ...)
  632.         if key == "config_recipe_change" then
  633.             local recipe = configView[SYMBOL_CONFIG_RECIPE]
  634.  
  635.             self.children = {}
  636.             self:addChild(GUI.text(1, 1, C_BADGE_TEXT, '[ '..recipe.label..' ]'))
  637.             self:addChild(GUI.text(1, 1, C_BADGE_TEXT, "Stored: "..tostring(recipe.stored)))
  638.             self:addChild(GUI.text(1, 1, C_BADGE_TEXT, "Wanted"))
  639.             local wantedInput = self:addChild(GUI.input(1, 1, 10, 3,
  640.                     C_INPUT, C_INPUT_TEXT, 0, C_STATUS_PRESSED, C_INPUT_TEXT, tostring(recipe.wanted)))
  641.             wantedInput.validator = numberValidator
  642.             wantedInput.onInputFinished = function(app, object)
  643.                 recipe.wanted = tonumber(object.text) or error('cannot parse '..object.text)
  644.                 event.push('ae2_loop')
  645.                 event.push('save')
  646.             end
  647.  
  648.             -- TODO: add remove/hide option
  649.  
  650.             -- self:draw()
  651.             event.push('redraw') -- There is probably a more elegant way to do it ¯\_(ツ)_/¯
  652.         end
  653.         super(app, self, key, ...)
  654.     end)
  655.  
  656.     local resetRecipeBtn = configView:addChild(GUI.button(itemConfigPanel.x, itemConfigPanel.y + itemConfigPanel.height + 1, itemConfigPanel.width, 3,
  657.                                                           C_BADGE, C_BADGE_TEXT, C_BADGE, C_STATUS_PRESSED, "Reset"))
  658.     resetRecipeBtn.onTouch = function(app, self)
  659.         local recipe = configView[SYMBOL_CONFIG_RECIPE]
  660.         if not recipe then return end
  661.         for i, candidate in ipairs(recipes) do
  662.             if (candidate == recipe) then
  663.                 table.remove(recipes, i)
  664.                 return
  665.             end
  666.         end
  667.     end
  668.  
  669.     -- Status bar
  670.     statusBar:addChild(GUI.panel(1, 1, statusBar.width, statusBar.height, C_STATUS_BAR))
  671.     local statusText = statusBar:addChild(GUI.text(2, 1, C_STATUS_TEXT, ''))
  672.     statusText.eventHandler = function(app, self)
  673.         self.text = string.format('CPU: %d free / %d total   Recipes:  %d errors  %d ongoing  %d queued   Update: %.0f ms',
  674.             status.cpu.free, status.cpu.all, status.recipes.error, status.recipes.crafting, status.recipes.queue, status.update.duration * 1000)
  675.     end
  676.     statusText.eventHandler(app, statusText)
  677.     local cfgBtn = statusBar:addChild(GUI.button(statusBar.width - 14, 1, 8, 1, C_STATUS_BAR, C_STATUS_TEXT, C_STATUS_BAR, C_STATUS_PRESSED, '[Config]'))
  678.     cfgBtn.switchMode = true
  679.     cfgBtn.animationDuration = .1
  680.     cfgBtn.onTouch = function(app, object)
  681.         configView.hidden = not object.pressed
  682.     end
  683.     statusBar:addChild(GUI.button(statusBar.width - 6, 1, 8, 1, C_STATUS_BAR, C_STATUS_TEXT, C_STATUS_BAR, C_STATUS_PRESSED, '[Exit]')).onTouch = function(app, object)
  684.         event.push('exit')
  685.     end
  686.  
  687.     return app
  688. end
  689.  
  690. function attachScrollbar(obj)
  691.     local width = (obj.width > 60) and 2 or 1
  692.     obj.width = obj.width - width
  693.     local bar = GUI.scrollBar(obj.x+obj.width, obj.y, width, obj.height, C_SCROLLBAR_BACKGROUND, C_SCROLLBAR,
  694.             0, 1, 0, 1, 4, false)
  695.     obj.parent:addChild(bar)
  696.     obj.scrollBar = bar
  697.  
  698.     override(obj, 'eventHandler', function (super, app, self, key, ...)
  699.         if key == 'scroll' then -- forward scrolls on the main object to the scrollbar
  700.             bar.eventHandler(app, bar, key, ...)
  701.         end
  702.         super(app, self, key, ...)
  703.     end)
  704.  
  705.     return bar
  706. end
  707.  
  708. -- Start the program
  709. main()
RAW Paste Data