SuperMcBrent

main

Feb 24th, 2023 (edited)
845
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 15.28 KB | Gaming | 0 0
  1. -- config
  2. local DEVICE_NAME = "All The Mods 10"
  3. local CONTROLLER_SIDE = "monitor_0"
  4. local APPS_DIR = "apps"
  5. local LIBS_DIR = "libs"
  6. local TICK_SEC = 0.2 -- our update cycle = 5 times per second
  7.  
  8. -- peripherals
  9. local mon = peripheral.wrap(CONTROLLER_SIDE)
  10. if not mon then error("No monitor on " .. CONTROLLER_SIDE) end
  11. mon.setTextScale(0.5)
  12.  
  13. -- state
  14. local W, H = mon.getSize()
  15. local apps = {}
  16. local libs = {}
  17. local order = {}
  18. local history = {} -- stack {app, view}
  19. local needRedraw = true
  20. local jobQueue = {}
  21. local requestQueue = {}
  22. local designLines = false
  23.  
  24. -- context for apps
  25. local ctx = {
  26.     controller = mon,
  27.     width = W,
  28.     height = H,
  29.     os = {
  30.         submitJob = function(jobFunc, callback) submitJob(jobFunc, callback) end,
  31.         navigate = function(id, view) gotoView(id, view) end,
  32.         replace = function(id, view) replaceView(id, view) end,
  33.         home = function() goHome() end,
  34.         back = function() goBack() end,
  35.         list = function() return order end,
  36.         get = function(id) return apps[id] end,
  37.         redraw = function() needRedraw = true end,
  38.         size = function() return W, H end,
  39.         transmit = function(message, protocol) rednetTransmit(message, protocol) end,
  40.     },
  41.     libs = function() return libs end,
  42.     deviceName = DEVICE_NAME,
  43. }
  44.  
  45. -- utils
  46. local function setSize()
  47.     W, H = mon.getSize()
  48.     ctx.width, ctx.height = W, H
  49.     needRedraw = true
  50. end
  51.  
  52. -- history helpers
  53. local function push(entry) table.insert(history, entry) end
  54. local function pop()
  55.     local n = #history
  56.     if n > 0 then
  57.         local e = history[n]
  58.         history[n] = nil
  59.         return e
  60.     end
  61. end
  62. local function peek() return history[#history] end
  63.  
  64. local function drawSystemTray()
  65.     libs.draw.drawLine(1, 1, W, 2, colors.gray, mon)
  66.  
  67.     libs.button.draw("homeButton", mon)
  68.     libs.button.draw("backButton", mon)
  69.     libs.button.draw("designUtilityButton", mon)
  70.  
  71.     libs.draw.putTime(math.floor(W / 2) - 1, 1, mon, true, colors.gray)
  72.     local cur = peek()
  73.     if cur then
  74.         local app = apps[cur.app]
  75.         if app and app.name then
  76.             local x = math.floor((W - #app.name) / 2)
  77.             libs.draw.drawTitle(1 + x, 2, app.name, colors.white, colors.gray, mon)
  78.         end
  79.     end
  80. end
  81.  
  82. local function handleSystemTrayTouch(x, y)
  83.     if libs.button.isWithinBoundingBox(x, y, "homeButton") then
  84.         ctx.os.home()
  85.         return true
  86.     end
  87.  
  88.     if libs.button.isWithinBoundingBox(x, y, "backButton") then
  89.         ctx.os.back()
  90.         return true
  91.     end
  92.  
  93.     if libs.button.isWithinBoundingBox(x, y, "designUtilityButton") then
  94.         designLines = not designLines
  95.         return true
  96.     end
  97.  
  98.     return false
  99. end
  100.  
  101. local function drawLines()
  102.     local w, h = ctx.width, ctx.height
  103.     mon.setBackgroundColor(colors.black)
  104.     mon.setTextColor(colors.gray)
  105.  
  106.     for k = 1, h do
  107.         for l = 1, w do
  108.             if l % 5 == 0 and k % 5 == 0 then
  109.                 mon.setCursorPos(l, k)
  110.                 mon.write("+")
  111.             elseif l % 5 == 0 then
  112.                 mon.setCursorPos(l, k)
  113.                 mon.write("|")
  114.             elseif k % 5 == 0 then
  115.                 mon.setCursorPos(l, k)
  116.                 mon.write("-")
  117.             end
  118.         end
  119.     end
  120.  
  121.     -- coordinate labels
  122.     mon.setTextColor(colors.white)
  123.     for l = 5, w, 5 do
  124.         mon.setCursorPos(l, 1)
  125.         mon.write(tostring(l))
  126.     end
  127.     for k = 5, h, 5 do
  128.         mon.setCursorPos(1, k)
  129.         mon.write(tostring(k))
  130.     end
  131. end
  132.  
  133. -- app loading
  134. local function loadSystem()
  135.     -- HOME BUTTON (composite of 2)
  136.     libs.button.create({
  137.         app = "main",
  138.         view = "home",
  139.         name = "homeButton1",
  140.         x = 1,
  141.         y = 1,
  142.         w = 5,
  143.         h = 1,
  144.         colorOn = colors.lime,
  145.         textOn = "/^\\",
  146.         textX = 2,
  147.         textY = 1,
  148.         composite = "homeButton"
  149.     })
  150.  
  151.     libs.button.create({
  152.         app = "main",
  153.         view = "home",
  154.         name = "homeButton2",
  155.         x = 1,
  156.         y = 2,
  157.         w = 5,
  158.         h = 1,
  159.         colorOn = colors.lime,
  160.         textOn = "|#|",
  161.         textX = 2,
  162.         textY = 2,
  163.         composite = "homeButton"
  164.     })
  165.  
  166.     -- BACK BUTTON (composite of 2)
  167.     libs.button.create({
  168.         app = "main",
  169.         view = "home",
  170.         name = "backButton1",
  171.         x = W - 4,
  172.         y = 1,
  173.         w = 5,
  174.         h = 1,
  175.         colorOn = colors.red,
  176.         colorOff = colors.lightGray,
  177.         state = true,
  178.         textOn = "/__",
  179.         textOff = "",
  180.         textX = W - 3,
  181.         textY = 1,
  182.         composite = "backButton"
  183.     })
  184.  
  185.     libs.button.create({
  186.         app = "main",
  187.         view = "home",
  188.         name = "backButton2",
  189.         x = W - 4,
  190.         y = 2,
  191.         w = 5,
  192.         h = 1,
  193.         colorOn = colors.red,
  194.         colorOff = colors.lightGray,
  195.         state = true,
  196.         textOn = "\\  ",
  197.         textOff = "",
  198.         textX = W - 3,
  199.         textY = 2,
  200.         composite = "backButton"
  201.     })
  202.  
  203.     -- DESIGN UTILITY BUTTON
  204.     libs.button.create({
  205.         app = "main",
  206.         view = "home",
  207.         name = "designUtilityButton",
  208.         x = 6,
  209.         y = 1,
  210.         w = 5,
  211.         h = 1,
  212.         colorOn = colors.cyan,
  213.         textOn = "Lines",
  214.         textX = 6,
  215.         textY = 1
  216.     })
  217. end
  218.  
  219. local function loadApps()
  220.     if not fs.exists(APPS_DIR) then fs.makeDir(APPS_DIR) end
  221.     local list = fs.list(APPS_DIR)
  222.  
  223.     -- 1: load/compile
  224.     for _, f in ipairs(list) do
  225.         if f:find("^app_") then
  226.             local path = fs.combine(APPS_DIR, f)
  227.             local chunk, err = loadfile(path)
  228.             if chunk then
  229.                 local ok, mod = pcall(chunk)
  230.                 if ok and type(mod) == "table" and mod.id then
  231.                     print("Load success:", mod.id)
  232.                     apps[mod.id] = mod
  233.                     table.insert(order, mod.id)
  234.                 else
  235.                     print("Load fail:", path, mod)
  236.                 end
  237.             else
  238.                 print("Compile fail:", path, err)
  239.             end
  240.         end
  241.     end
  242.  
  243.     -- 2: call create
  244.     for _, id in ipairs(order) do
  245.         local app = apps[id]
  246.         if app.create then
  247.             local ok, res = pcall(app.create, ctx)
  248.             if not ok then
  249.                 print("Create error:", res)
  250.             end
  251.         end
  252.     end
  253. end
  254.  
  255. local function loadLibs()
  256.     if not fs.exists(LIBS_DIR) then fs.makeDir(LIBS_DIR) end
  257.     local list = fs.list(LIBS_DIR)
  258.     libs = {}
  259.  
  260.     -- Phase 1: load
  261.     for _, f in ipairs(list) do
  262.         if f:find("^lib_") then
  263.             local path = fs.combine(LIBS_DIR, f)
  264.             local chunk, err = loadfile(path)
  265.             if not chunk then
  266.                 print("Lib compile fail:", path, err)
  267.                 goto continue
  268.             end
  269.             local ok, mod = pcall(chunk)
  270.             if ok and type(mod) == "table" and mod.alias then
  271.                 libs[mod.alias] = mod
  272.                 print("Lib load success:", mod.alias)
  273.             else
  274.                 print("Lib load fail:", path, mod)
  275.             end
  276.         end
  277.         ::continue::
  278.     end
  279.  
  280.     -- helper: turn deps table into a list of aliases
  281.     local function required_aliases(lib)
  282.         local list = {}
  283.         if type(lib.dependencies) == "table" then
  284.             local i = 1
  285.             while lib.dependencies[i] do
  286.                 list[#list + 1] = lib.dependencies[i]
  287.                 i = i + 1
  288.             end
  289.             for k, v in pairs(lib.dependencies) do
  290.                 if type(k) == "string" and v then
  291.                     list[#list + 1] = k
  292.                 end
  293.             end
  294.         end
  295.         return list
  296.     end
  297.  
  298.     -- Phase 2: resolve
  299.     local resolving, resolved = {}, {}
  300.     local function resolve(lib, stack)
  301.         if resolved[lib.alias] then return end
  302.         stack = stack or {}
  303.         if resolving[lib.alias] then
  304.             stack[#stack + 1] = lib.alias
  305.             error("Circular dependency: " .. table.concat(stack, " -> "))
  306.         end
  307.         resolving[lib.alias] = true
  308.         stack[#stack + 1] = lib.alias
  309.  
  310.         local depRefs = {}
  311.         for _, depAlias in ipairs(required_aliases(lib)) do
  312.             local dep = libs[depAlias]
  313.             if not dep then
  314.                 error("Missing dependency '" .. depAlias .. "' required by '" .. lib.alias .. "'")
  315.             end
  316.             resolve(dep, stack)
  317.             depRefs[depAlias] = dep
  318.         end
  319.  
  320.         if next(depRefs) ~= nil then
  321.             if type(lib.init) == "function" then
  322.                 lib.init(depRefs)
  323.             else
  324.                 error("Library '" .. lib.alias .. "' declares dependencies but has no init()")
  325.             end
  326.         end
  327.  
  328.         resolving[lib.alias] = nil
  329.         stack[#stack] = nil
  330.         resolved[lib.alias] = true
  331.     end
  332.  
  333.     for _, lib in pairs(libs) do resolve(lib) end
  334. end
  335.  
  336. -- navigation
  337. local function appTransition(prevEntry, nextEntry)
  338.     local prevId = prevEntry and prevEntry.app or nil
  339.     local nextId = nextEntry and nextEntry.app or nil
  340.     if prevId == nextId then return end
  341.  
  342.     if prevId and apps[prevId] and apps[prevId].suspend then
  343.         print("Suspending app:", prevId)
  344.         local ok, res = pcall(apps[prevId].suspend, ctx)
  345.         if not ok then
  346.             print("Suspend error:", res)
  347.         end
  348.     end
  349.  
  350.     if nextId and apps[nextId] and apps[nextId].resume then
  351.         print("Resuming app:", nextId)
  352.         local ok, res = pcall(apps[nextId].resume, ctx)
  353.         if not ok then
  354.             print("Resume error:", res)
  355.         end
  356.     end
  357. end
  358.  
  359. function gotoView(appId, viewId)
  360.     if not apps[appId] then error("App not found: " .. tostring(appId)) end
  361.     local prev = peek()
  362.     local newEntry = { app = appId, view = viewId or "root" }
  363.     push(newEntry)
  364.     appTransition(prev, newEntry)
  365.  
  366.     mon.setBackgroundColor(colors.black)
  367.     mon.clear()
  368.     setSize()
  369.     needRedraw = true
  370. end
  371.  
  372. function replaceView(appId, viewId)
  373.     if not apps[appId] then error("App not found: " .. tostring(appId)) end
  374.     local prev = peek()
  375.     local newEntry = { app = appId, view = viewId or "root" }
  376.     history[#history] = newEntry
  377.     appTransition(prev, newEntry)
  378.  
  379.     mon.setBackgroundColor(colors.black)
  380.     mon.clear()
  381.     setSize()
  382.     needRedraw = true
  383. end
  384.  
  385. function goHome()
  386.     -- fucks with transitions, so we don't use history here
  387.     -- local prev = peek()
  388.     -- history = {}
  389.     -- if prev then
  390.     --     history[1] = prev
  391.     -- end
  392.     gotoView("home", "root")
  393. end
  394.  
  395. function goBack()
  396.     local oldTop = pop()
  397.     local newTop = peek()
  398.     if not newTop then
  399.         -- home push {home,root}
  400.         ctx.os.home()
  401.         return
  402.     end
  403.  
  404.     appTransition(oldTop, newTop)
  405.  
  406.     mon.setBackgroundColor(colors.black)
  407.     mon.clear()
  408.     setSize()
  409.     needRedraw = true
  410. end
  411.  
  412. -- draw pass
  413. local function drawFrame()
  414.     drawSystemTray()
  415.     local cur = peek()
  416.     if cur and apps[cur.app] and apps[cur.app].draw then
  417.         local ok, res = pcall(apps[cur.app].draw, ctx, mon, cur.view)
  418.         if not ok then
  419.             print("Draw error:", res)
  420.         end
  421.     end
  422.     if designLines then
  423.         drawLines()
  424.     end
  425.     needRedraw = false
  426. end
  427.  
  428. -- parallel: touch
  429. local function waitTouch()
  430.     local ev, side, x, y = os.pullEvent("monitor_touch")
  431.     if side == CONTROLLER_SIDE then
  432.         if not handleSystemTrayTouch(x, y) then
  433.             local cur = peek()
  434.             if cur and apps[cur.app] and apps[cur.app].touch then
  435.                 local ok, res = pcall(apps[cur.app].touch, ctx, x, y, cur.view)
  436.                 if not ok then
  437.                     print("Touch error:", res)
  438.                 end
  439.             end
  440.         end
  441.     end
  442. end
  443.  
  444. -- parallel: update tick
  445. local function waitTick()
  446.     local t = os.startTimer(TICK_SEC)
  447.     while true do
  448.         local ev, id = os.pullEvent()
  449.         if ev == "monitor_resize" then
  450.             setSize()
  451.             needRedraw = true
  452.             return
  453.         elseif ev == "timer" and id == t then
  454.             for _, app in pairs(apps) do
  455.                 if app.update then
  456.                     local ok, res = pcall(app.update, ctx, TICK_SEC)
  457.                     if not ok then
  458.                         print("Update error:", res)
  459.                     end
  460.                 end
  461.             end
  462.             return
  463.         end
  464.     end
  465. end
  466.  
  467. -- parallel: rednet
  468. local function rednetLoop()
  469.     while true do
  470.         local sender, message, protocol = rednet.receive()
  471.  
  472.         if protocol == "system" then
  473.             goto continue
  474.         end
  475.  
  476.         for _, app in pairs(apps) do
  477.             if app.protocol == protocol and app.receive then
  478.                 local ok, res = pcall(app.receive, ctx, sender, message)
  479.                 if not ok then
  480.                     print("Receive error:", res)
  481.                 end
  482.             end
  483.         end
  484.  
  485.         ::continue::
  486.     end
  487. end
  488.  
  489. function rednetTransmit(message, protocol)
  490.     if not message or not protocol then
  491.         error("Invalid rednet transmit parameters")
  492.     end
  493.     table.insert(requestQueue, { message = message, protocol = protocol })
  494.     print(os.time() .. ": Queued M: " .. message .. ", P: " .. protocol)
  495. end
  496.  
  497. function transmitRequests()
  498.     if #requestQueue == 0 then
  499.         return
  500.     end
  501.  
  502.     rednet.broadcast(requestQueue, "batchedRequests")
  503.     print(os.time() .. ": Broadcasted " .. #requestQueue .. " requests")
  504.  
  505.     requestQueue = {}
  506. end
  507.  
  508. function submitJob(jobFunc, callback)
  509.     table.insert(jobQueue, { job = jobFunc, callback = callback })
  510. end
  511.  
  512. local function processingLoop()
  513.     while true do
  514.         if #jobQueue > 0 then
  515.             local task = table.remove(jobQueue, 1)
  516.  
  517.             local ok, err = pcall(task.job, function(...)
  518.                 if task.callback then
  519.                     local ok2, err2 = pcall(task.callback, ...)
  520.                     if not ok2 then
  521.                         print("Job callback error:", err2)
  522.                     end
  523.                 end
  524.             end)
  525.  
  526.             if not ok then
  527.                 print("Job execution error:", err)
  528.             end
  529.         else
  530.             -- no jobs, yield
  531.             os.sleep(0.05)
  532.         end
  533.     end
  534. end
  535.  
  536. -- flair
  537. local function loadingScreen()
  538.     mon.clear()
  539.     libs.draw.drawTitle(W / 2 - 7, H / 2, "All The Mods 10", colors.white, colors.black, mon)
  540.     libs.draw.drawTitle(W / 2 - 9, H - 3, "Loading... Please wait", colors.white, colors.black, mon)
  541.     for i = 1, 20 do
  542.         libs.draw.drawProg(2, H - 1, W - 2, 1, i, 20, colors.white, colors.gray, mon)
  543.         sleep(0.01)
  544.     end
  545.     libs.draw.empty(mon)
  546. end
  547.  
  548. -- boot
  549. peripheral.find("modem", rednet.open)
  550. loadLibs()
  551. loadingScreen()
  552. loadSystem()
  553. loadApps()
  554. if not apps["home"] then error("Missing home app with id='home'") end
  555. ctx.os.home()
  556.  
  557. -- loop
  558. local function mainLoop()
  559.     while true do
  560.         if needRedraw then
  561.             mon.clear()
  562.             drawFrame()
  563.         end
  564.         transmitRequests();
  565.         parallel.waitForAny(waitTouch, waitTick)
  566.         needRedraw = true
  567.     end
  568. end
  569.  
  570. parallel.waitForAll(mainLoop, rednetLoop, processingLoop)
  571.  
  572. -- todos
  573. -- dynamic peripherals
  574. -- widgets
  575. -- persistent state
  576. -- preferred app order
  577.  
Add Comment
Please, Sign In to add comment