DorianH935

Untitled

Nov 16th, 2025
31
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 17.38 KB | None | 0 0
  1. -- lemonos3_fixed.lua
  2. -- LemonOS 3.0 (fixed) - integrated chat + DM + multi-server relay client
  3. -- Reworked to avoid rednet.lookup single-number return bug and other stability fixes.
  4.  
  5. -- CONFIG / PATHS
  6. local CONFIG_PATH = "/lemonos.cfg"
  7. local STARTUP_PATH = "/startup.lua"
  8. local firmware = { device = "Lemonphone", version = "LemonOS 3.0 (fixed)" }
  9.  
  10. -- SETTINGS (persisted)
  11. local config = {
  12. username = "User",
  13. accentColor = colors.yellow,
  14. bgColor = colors.black,
  15. rednetSide = "back",
  16. phone = nil -- phone number assigned by server
  17. }
  18.  
  19. -- Load/save config
  20. local function saveConfig()
  21. local f = fs.open(CONFIG_PATH, "w")
  22. if f then f.write(textutils.serialize(config)); f.close() end
  23. end
  24. local function loadConfig()
  25. if fs.exists(CONFIG_PATH) then
  26. local f = fs.open(CONFIG_PATH, "r")
  27. local data = f.readAll()
  28. f.close()
  29. local ok, t = pcall(textutils.unserialize, data)
  30. if ok and type(t) == "table" then
  31. for k,v in pairs(t) do config[k] = v end
  32. end
  33. end
  34. end
  35. loadConfig()
  36.  
  37. -- Ensure startup points to firm.lpsw (keeps prior behavior)
  38. if not fs.exists(STARTUP_PATH) then
  39. local f = fs.open(STARTUP_PATH, "w")
  40. f.write('shell.run("firm.lpsw")')
  41. f.close()
  42. end
  43.  
  44. -- ensure rednet open if modem present
  45. if peripheral.find("modem") then
  46. rednet.open(config.rednetSide)
  47. end
  48.  
  49. -- UTILITIES
  50. local function centerText(y, text, color)
  51. local w = term.getSize()
  52. local x = math.floor((w - #text) / 2)
  53. term.setCursorPos(math.max(1, x), y)
  54. if color then term.setTextColor(color) end
  55. term.write(text)
  56. term.setTextColor(colors.white)
  57. end
  58.  
  59. -- beep helper: speaker peripheral preferred, otherwise terminal bell
  60. local function beep()
  61. local sp = peripheral.find("speaker")
  62. if sp and type(sp.playNote) == "function" then
  63. pcall(function() sp.playNote("harp", 5, 1) end)
  64. else
  65. io.write("\7")
  66. end
  67. end
  68.  
  69. -- ===== Networking helpers =====
  70.  
  71. -- Normalize rednet.lookup output so it ALWAYS returns a table of server IDs
  72. local function findServers()
  73. local ok, lookup = pcall(rednet.lookup, "lemonphone_server")
  74. if not ok then return {} end
  75. if lookup == nil then
  76. return {}
  77. end
  78. if type(lookup) == "number" then
  79. return { lookup }
  80. elseif type(lookup) == "table" then
  81. -- ensure all entries are numbers
  82. local out = {}
  83. for _, v in ipairs(lookup) do
  84. if type(v) == "number" then table.insert(out, v) end
  85. end
  86. return out
  87. else
  88. return {}
  89. end
  90. end
  91.  
  92. -- Attempt to register with a specific server (returns ok, info)
  93. local function registerWithServerOnce(serverID, timeout)
  94. timeout = timeout or 5
  95. local clientID = os.getComputerID()
  96. local payload = { type="register_request", desired_service=2, clientID = clientID, username = config.username }
  97. local okSend = pcall(rednet.send, serverID, payload, "lemonphone_control")
  98. if not okSend then return false, "send failed" end
  99. local sender, resp = rednet.receive("lemonphone_control", timeout)
  100. if sender == serverID and type(resp) == "table" and resp.type == "register_response" and resp.ok then
  101. return true, resp.phone
  102. end
  103. return false, "no response"
  104. end
  105.  
  106. -- High-level ensure registration: try all discovered servers
  107. local function ensureRegistration()
  108. if config.phone then return true, config.phone end
  109. local servers = findServers()
  110. if #servers == 0 then return false, "no servers found" end
  111. for _, sid in ipairs(servers) do
  112. local ok, res = pcall(registerWithServerOnce, sid, 5)
  113. if ok and res then
  114. -- registerWithServerOnce returns (true, phone) on success
  115. if type(res) == "string" then
  116. config.phone = res
  117. saveConfig()
  118. return true, res
  119. elseif type(res) == "table" and res.phone then
  120. config.phone = res.phone
  121. saveConfig()
  122. return true, config.phone
  123. end
  124. end
  125. end
  126. return false, "registration failed"
  127. end
  128.  
  129. -- send a global chat message: we forward to one known server and let it broadcast/relay
  130. local function sendGlobal(text)
  131. local servers = findServers()
  132. if #servers == 0 then return false, "no servers" end
  133. local payload = { from_phone = config.phone or ("u-" .. tostring(os.getComputerID())), text = text }
  134. local ok, err = pcall(rednet.send, servers[1], payload, "lemonphone_global")
  135. if not ok then return false, "send failed" end
  136. return true
  137. end
  138.  
  139. -- send a DM (server-forward style)
  140. local function sendDM(destPhone, text)
  141. local servers = findServers()
  142. if #servers == 0 then return false, "no servers" end
  143. local payload = { type = "server_forward", from_phone = config.phone, dest_phone = destPhone, text = text }
  144. local ok, err = pcall(rednet.send, servers[1], payload, "lemonphone_control")
  145. if not ok then return false, "send failed" end
  146. return true
  147. end
  148.  
  149. -- Chat message storage (runtime)
  150. local messages = {} -- array of {type="global"/"dm", from, to, text, time}
  151. local function storeMessage(msg)
  152. msg.time = os.time()
  153. table.insert(messages, msg)
  154. -- cap messages to reasonable history to avoid unbounded growth
  155. if #messages > 500 then
  156. table.remove(messages, 1)
  157. end
  158. end
  159.  
  160. -- ===== Background listener (runs in its own parallel thread) =====
  161. -- listens for: lemonphone_global, lemonphone_dm, lemonphone_control
  162. local function backgroundListener()
  163. while true do
  164. local sender, msg, proto = rednet.receive()
  165. if not msg then
  166. -- continue if receive returned nil for some reason
  167. else
  168. if proto == "lemonphone_global" then
  169. -- msg expected: { from_phone = "...", text = "..." }
  170. storeMessage({ type="global", from = msg.from_phone, text = msg.text })
  171. beep()
  172. os.queueEvent("lemon_message", msg)
  173. elseif proto == "lemonphone_dm" then
  174. -- msg expected: { from_phone = "...", to_phone = "...", text = "..." }
  175. storeMessage({ type="dm", from = msg.from_phone, to = msg.to_phone, text = msg.text })
  176. beep()
  177. os.queueEvent("lemon_message", msg)
  178. elseif proto == "lemonphone_control" then
  179. -- servers may forward dm_incoming as control messages
  180. if type(msg) == "table" and msg.type == "dm_incoming" then
  181. storeMessage({ type="dm", from = msg.from_phone, to = config.phone, text = msg.text })
  182. beep()
  183. os.queueEvent("lemon_message", msg)
  184. elseif type(msg) == "table" and msg.type == "register_response" then
  185. -- registration responses are generally handled elsewhere, but store for completeness
  186. if msg.ok and msg.phone then
  187. config.phone = msg.phone
  188. saveConfig()
  189. end
  190. end
  191. end
  192. end
  193. end
  194. end
  195.  
  196. -- ===== Signal updater (estimates distance to nearest server) =====
  197. local signalStrength = 0 -- 0..3
  198. local function distanceToBars(distance)
  199. if distance <= 5 then return 3
  200. elseif distance <= 20 then return 2
  201. else return 1 end
  202. end
  203.  
  204. local function updateSignal()
  205. while true do
  206. local servers = findServers()
  207. if #servers == 0 then
  208. signalStrength = 0
  209. else
  210. local me = os.getComputerID()
  211. local best = nil
  212. for _, s in ipairs(servers) do
  213. if type(s) == "number" then
  214. local d = math.abs(me - s)
  215. if not best or d < best then best = d end
  216. end
  217. end
  218. if best then signalStrength = distanceToBars(best) else signalStrength = 1 end
  219. end
  220. sleep(5)
  221. end
  222. end
  223.  
  224. local function drawSignalBars()
  225. local barChar = "|"
  226. for i = 1, 3 do
  227. if i <= signalStrength then term.setTextColor(colors.lime) else term.setTextColor(colors.gray) end
  228. term.write(barChar)
  229. end
  230. term.setTextColor(colors.white)
  231. end
  232.  
  233. -- ===== UI components =====
  234.  
  235. -- Chat UI: shows recent messages and lets user send; /dm <phone> <text> for direct messages
  236. local function chatUI()
  237. while true do
  238. term.setBackgroundColor(config.bgColor)
  239. term.clear()
  240. centerText(2, "Messages", config.accentColor)
  241. term.setTextColor(colors.white)
  242. local start = math.max(1, #messages - 11)
  243. local line = 4
  244. for i = start, #messages do
  245. local m = messages[i]
  246. local prefix
  247. if m.type == "global" then
  248. prefix = "[" .. tostring(m.from or "?") .. "]"
  249. else
  250. local toPart = m.to and (">" .. tostring(m.to)) or ""
  251. prefix = "DM[" .. tostring(m.from or "?") .. toPart .. "]"
  252. end
  253. term.setCursorPos(2, line)
  254. term.clearLine()
  255. local txt = (m.text or "")
  256. if #prefix + 1 + #txt > term.getSize() - 4 then
  257. txt = txt:sub(1, term.getSize() - 4 - #prefix - 1) .. "…"
  258. end
  259. term.write(prefix .. " " .. txt)
  260. line = line + 1
  261. end
  262.  
  263. term.setCursorPos(2, 17)
  264. term.setTextColor(colors.gray)
  265. term.clearLine()
  266. term.write("Type message. Commands: /dm <phone> <text> /back")
  267. term.setCursorPos(2, 18)
  268. term.setTextColor(colors.white)
  269. term.clearLine()
  270. local input = read()
  271. if not input then input = "" end
  272. if input == "/back" then return end
  273.  
  274. local dmDest, dmText = input:match("^/dm%s+([%w%-]+)%s+(.+)")
  275. if dmDest and dmText then
  276. local ok, err = sendDM(dmDest, dmText)
  277. if ok then
  278. storeMessage({ type="dm", from = config.phone, to = dmDest, text = dmText })
  279. else
  280. -- show brief error
  281. term.setCursorPos(2, 18)
  282. term.setTextColor(colors.red)
  283. term.clearLine()
  284. term.write("Failed to send DM: " .. tostring(err))
  285. sleep(1.5)
  286. end
  287. else
  288. if input ~= "" then
  289. local ok, err = sendGlobal(input)
  290. if ok then
  291. storeMessage({ type="global", from = config.phone, text = input })
  292. else
  293. term.setCursorPos(2, 18)
  294. term.setTextColor(colors.red)
  295. term.clearLine()
  296. term.write("Failed to send: " .. tostring(err))
  297. sleep(1.5)
  298. end
  299. end
  300. end
  301. end
  302. end
  303.  
  304. -- About page
  305. local function aboutLemonphone()
  306. term.setBackgroundColor(config.bgColor)
  307. term.clear()
  308. centerText(3,"About Lemonphone", config.accentColor)
  309. term.setCursorPos(2,5)
  310. term.setTextColor(colors.white)
  311. term.write("Device: " .. firmware.device)
  312. term.setCursorPos(2,6)
  313. term.write("Firmware: " .. firmware.version)
  314. term.setCursorPos(2,7)
  315. term.write("Phone: " .. tostring(config.phone or "unregistered"))
  316. term.setCursorPos(2,9)
  317. term.setTextColor(colors.gray)
  318. term.write("Press any key to return...")
  319. os.pullEvent("key")
  320. end
  321.  
  322. -- Settings UI
  323. local function openSettings()
  324. while true do
  325. term.setBackgroundColor(config.bgColor)
  326. term.clear()
  327. centerText(3, "Settings", config.accentColor)
  328. term.setCursorPos(2,5)
  329. term.setTextColor(colors.white)
  330. term.write("1. Change Username")
  331. term.setCursorPos(2,6)
  332. term.write("2. Change Accent Color")
  333. term.setCursorPos(2,7)
  334. term.write("3. Re-register Phone")
  335. term.setCursorPos(2,8)
  336. term.write("4. Back")
  337. local _, key = os.pullEvent("key")
  338. if key == keys.one then
  339. term.setCursorPos(2,10); term.clearLine(); term.setTextColor(colors.white)
  340. term.write("Enter new username: ")
  341. local newName = read()
  342. if newName and newName ~= "" then config.username = newName; saveConfig() end
  343. elseif key == keys.two then
  344. term.setCursorPos(2,10); term.clearLine(); term.setTextColor(colors.white)
  345. term.write("Color name (e.g. yellow): ")
  346. local colorName = read()
  347. if colorName and colors[colorName] then config.accentColor = colors[colorName]; saveConfig()
  348. else term.setCursorPos(2,11); term.setTextColor(colors.red); term.write("Invalid color."); sleep(1) end
  349. elseif key == keys.three then
  350. -- force re-register (remove phone then try)
  351. config.phone = nil; saveConfig()
  352. term.setCursorPos(2,10); term.clearLine(); term.setTextColor(colors.white)
  353. term.write("Attempting registration...")
  354. local ok, res = ensureRegistration()
  355. term.setCursorPos(2,11); term.clearLine()
  356. if ok then term.setTextColor(colors.lime); term.write("Registered as " .. res)
  357. else term.setTextColor(colors.red); term.write("Registration failed: " .. tostring(res)) end
  358. sleep(1.5)
  359. elseif key == keys.four then
  360. return
  361. end
  362. end
  363. end
  364.  
  365. -- Top bar and menu drawing
  366. local function drawTopBar()
  367. local w,_ = term.getSize()
  368. term.setCursorPos(1,1)
  369. term.setBackgroundColor(config.accentColor)
  370. term.clearLine()
  371. term.setCursorPos(2,1)
  372. if rednet.isOpen(config.rednetSide) then
  373. drawSignalBars()
  374. term.write(" Net")
  375. else
  376. term.setTextColor(colors.red)
  377. term.write("X NoNet")
  378. term.setTextColor(colors.white)
  379. end
  380. local timeStr = textutils.formatTime(os.time(), true)
  381. local dateStr = os.date("%b %d")
  382. local clock = timeStr .. " | " .. dateStr
  383. term.setCursorPos(w - #clock - 1, 1)
  384. term.setTextColor(colors.white)
  385. term.write(clock)
  386. end
  387.  
  388. local function drawMenu(selected)
  389. local apps = {
  390. { name="Messages", fn = chatUI },
  391. { name="Files", fn = function() shell.run("shell") end },
  392. { name="Worm", fn = function() pcall(shell.run, "worm") end },
  393. { name="Falling Blocks", fn = function() pcall(shell.run, "falling") end },
  394. { name="Settings", fn = openSettings },
  395. { name="About Lemonphone", fn = aboutLemonphone },
  396. { name="Shutdown", fn = function() os.shutdown() end },
  397. }
  398.  
  399. local startY = 4
  400. term.setTextColor(colors.white)
  401. for i, app in ipairs(apps) do
  402. term.setCursorPos(4, startY + i - 1)
  403. if i == selected then term.setBackgroundColor(colors.gray); term.setTextColor(colors.black)
  404. else term.setBackgroundColor(config.bgColor); term.setTextColor(colors.white) end
  405. term.clearLine()
  406. term.setCursorPos(4, startY + i - 1)
  407. term.write(app.name)
  408. end
  409. term.setBackgroundColor(config.bgColor)
  410. return apps
  411. end
  412.  
  413. -- Main UI loop
  414. local function mainLoop()
  415. -- Try auto-register but don't block UI long
  416. spawn(function() ensureRegistration() end)
  417.  
  418. local selected = 1
  419. local refreshRate = 1
  420. while true do
  421. term.setBackgroundColor(config.bgColor); term.clear()
  422. drawTopBar()
  423. centerText(3, (config.username or "User") .. "'s Lemonphone", config.accentColor)
  424. local apps = drawMenu(selected)
  425. local timerId = os.startTimer(refreshRate)
  426. local event, a, b, c = os.pullEvent()
  427. if event == "key" then
  428. if a == keys.up then selected = math.max(1, selected - 1)
  429. elseif a == keys.down then selected = math.min(#apps, selected + 1)
  430. elseif a == keys.enter then
  431. local chosen = apps[selected]
  432. term.setBackgroundColor(config.bgColor); term.clear(); term.setCursorPos(1,1)
  433. pcall(chosen.fn)
  434. end
  435. elseif event == "timer" and a == timerId then
  436. -- redraw topbar only
  437. drawTopBar()
  438. elseif event == "lemon_message" then
  439. -- show short banner
  440. local msg = a -- the queued message
  441. term.setCursorPos(2,2)
  442. term.setTextColor(colors.white)
  443. term.clearLine()
  444. term.write("[New message]")
  445. sleep(1)
  446. end
  447. end
  448. end
  449.  
  450. -- Small spawn wrapper (simple co-routine starter)
  451. function spawn(f)
  452. local co = coroutine.create(f)
  453. local function step()
  454. local ok, err = coroutine.resume(co)
  455. if not ok and err then print("spawn error: "..tostring(err)) end
  456. end
  457. -- run in background via a timer loop
  458. parallel.waitForAny(function() while coroutine.status(co) ~= "dead" do step(); sleep(0.05) end end, function() end)
  459. end
  460.  
  461. -- But the above spawn uses parallel.waitForAny blocking — replace spawn with simpler nonblocking approach below
  462. -- We'll instead start background tasks with parallel.waitForAny at script bottom.
  463.  
  464. -- STARTUP: run background tasks in parallel
  465. local function startAll()
  466. -- Use three parallel tasks:
  467. -- 1) signal updater
  468. -- 2) background listener
  469. -- 3) mainLoop
  470. parallel.waitForAny(updateSignal, backgroundListener, mainLoop)
  471. end
  472.  
  473. -- Start
  474. term.setBackgroundColor(colors.black)
  475. term.clear()
  476. print("Starting LemonOS 3.0 (fixed)...")
  477. startAll()
  478.  
Advertisement
Add Comment
Please, Sign In to add comment