DorianH935

Untitled

Nov 15th, 2025 (edited)
38
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 13.03 KB | None | 0 0
  1. -- lemonos3.lua
  2. -- LemonOS 3.0 - integrated chat + DM + multi-server relay client
  3. -- Put as /lemonos3.lua and have startup run it (or replace existing lemonos)
  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" }
  9.  
  10. -- SETTINGS
  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
  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. -- startup ensure
  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
  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(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: use speaker peripheral if available, else terminal bell
  60. local function beep()
  61. local sp = peripheral.find("speaker")
  62. if sp then
  63. pcall(function() sp.playNote("harp", 5, 1) end)
  64. else
  65. -- terminal bell
  66. io.write("\7")
  67. end
  68. end
  69.  
  70. -- === PHONE REGISTRATION ===
  71. -- find servers
  72. local function findServers()
  73. local servers = rednet.lookup("lemonphone_server") or {}
  74. return servers
  75. end
  76.  
  77. local function registerWithServer(serverID)
  78. local clientID = os.getComputerID()
  79. rednet.send(serverID, { type="register_request", desired_service=2, clientID = clientID }, "lemonphone_control")
  80. local sender, resp, proto = rednet.receive("lemonphone_control", 5)
  81. if sender == serverID and resp and resp.type == "register_response" and resp.ok then
  82. config.phone = resp.phone
  83. saveConfig()
  84. return true, resp.phone
  85. end
  86. return false, "no response"
  87. end
  88.  
  89. -- attempt auto-registration (non-blocking friendly)
  90. local function ensureRegistration()
  91. if config.phone then return true, config.phone end
  92. local servers = findServers()
  93. if 1 == 0 then return false, "no servers" end --Temp fix for crash, actual resolution in lemonOS 3.0.1
  94. for _,sid in ipairs(servers) do
  95. local ok, phoneOrErr = pcall(registerWithServer, sid)
  96. if ok and phoneOrErr then
  97. return true, phoneOrErr
  98. end
  99. end
  100. return false, "failed"
  101. end
  102.  
  103. -- === CHAT STORAGE ===
  104. local messages = {} -- list of {type="global"|"dm", from_phone, to_phone, text, time, seen}
  105. local function storeMessage(m)
  106. m.time = os.time()
  107. table.insert(messages, m)
  108. end
  109.  
  110. -- === BACKGROUND LISTENER ===
  111. -- listens for: lemonphone_global, lemonphone_dm, lemonphone_control (server responses)
  112. local function backgroundListener()
  113. while true do
  114. local sender, msg, proto = rednet.receive()
  115. if proto == "lemonphone_global" then
  116. -- broadcast to local clients; store message for UI
  117. storeMessage({ type="global", from_phone = msg.from_phone, text = msg.text })
  118. -- beep and notify
  119. beep()
  120. os.queueEvent("lemon_message", msg)
  121. elseif proto == "lemonphone_dm" then
  122. -- direct DM to this client
  123. storeMessage({ type="dm", from_phone = msg.from_phone, to_phone = config.phone, text = msg.text })
  124. beep()
  125. os.queueEvent("lemon_message", msg)
  126. elseif proto == "lemonphone_control" then
  127. -- server replies or forwarded DM from server
  128. if msg.type == "dm_incoming" then
  129. storeMessage({ type="dm", from_phone = msg.from_phone, to_phone = config.phone, text = msg.text })
  130. beep()
  131. os.queueEvent("lemon_message", msg)
  132. elseif msg.type == "register_response" then
  133. -- handled by registerWithServer; we still store
  134. end
  135. end
  136. end
  137. end
  138.  
  139. -- === SENDER HELPERS ===
  140. -- send global message (to server which will broadcast/relay)
  141. local function sendGlobal(text)
  142. -- choose a server (first available)
  143. local servers = findServers()
  144. if #servers == 0 then return false, "no servers" end
  145. local payload = { from_phone = config.phone or ("u-" .. tostring(os.getComputerID())), text = text }
  146. -- send with protocol lemonphone_global to server then server handles broadcast
  147. -- servers also respond by rebroadcast so we just send to any server
  148. rednet.send(servers[1], payload, "lemonphone_global")
  149. return true
  150. end
  151.  
  152. -- send DM via appropriate server: server will forward to owner server
  153. local function sendDM(destPhone, text)
  154. local servers = findServers()
  155. if #servers == 0 then return false, "no servers" end
  156. local payload = { type = "server_forward", from_phone = config.phone, dest_phone = destPhone, text = text }
  157. -- send to the first server; servers will forward to correct owner
  158. rednet.send(servers[1], payload, "lemonphone_control")
  159. return true
  160. end
  161.  
  162. -- === UI: Chat window ===
  163. local function chatUI()
  164. -- very simple: show last 12 messages, allow typing, send to global or DM when prefixed with /dm <phone> <text>
  165. while true do
  166. term.setBackgroundColor(config.bgColor)
  167. term.clear()
  168. centerText(2, "Messages", config.accentColor)
  169. term.setCursorPos(2,4)
  170. term.setTextColor(colors.white)
  171. local start = math.max(1, #messages - 11)
  172. for i = start, #messages do
  173. local m = messages[i]
  174. local prefix = (m.type == "global") and ("["..tostring(m.from_phone).."]") or ("DM ["..tostring(m.from_phone)..">"..tostring(m.to_phone).."]")
  175. term.setCursorPos(2, 4 + (i - start))
  176. term.clearLine()
  177. term.write(prefix .. " " .. (m.text or ""))
  178. end
  179. term.setCursorPos(2, 17)
  180. term.clearLine()
  181. term.setTextColor(colors.gray)
  182. term.write("Type message. Commands: /dm <phone> <text> /back")
  183. term.setCursorPos(2, 18)
  184. term.setTextColor(colors.white)
  185. local input = read()
  186. if input == "/back" then return end
  187. local dmMatch = string.match(input, "^/dm%s+([%w%-]+)%s+(.+)")
  188. if dmMatch then
  189. local dest, msgText = string.match(input, "^/dm%s+([%w%-]+)%s+(.+)")
  190. if dest and msgText then
  191. sendDM(dest, msgText)
  192. storeMessage({ type="dm", from_phone = config.phone, to_phone = dest, text = msgText })
  193. end
  194. else
  195. if input ~= "" then
  196. sendGlobal(input)
  197. storeMessage({ type="global", from_phone = config.phone, text = input })
  198. end
  199. end
  200. end
  201. end
  202.  
  203. -- === About page ===
  204. local function aboutLemonphone()
  205. term.setBackgroundColor(config.bgColor)
  206. term.clear()
  207. centerText(3,"About Lemonphone", config.accentColor)
  208. term.setCursorPos(2,5)
  209. term.write("Device: " .. firmware.device)
  210. term.setCursorPos(2,6)
  211. term.write("Firmware: " .. firmware.version)
  212. term.setCursorPos(2,7)
  213. term.write("Phone: " .. tostring(config.phone or "unregistered"))
  214. term.setCursorPos(2,9)
  215. term.write("Press any key to return...")
  216. os.pullEvent("key")
  217. end
  218.  
  219. -- === Settings ===
  220. local function openSettings()
  221. while true do
  222. term.setBackgroundColor(config.bgColor)
  223. term.clear()
  224. centerText(3, "Settings", config.accentColor)
  225. term.setCursorPos(2,5)
  226. term.write("1. Change Username")
  227. term.setCursorPos(2,6)
  228. term.write("2. Change Accent Color")
  229. term.setCursorPos(2,7)
  230. term.write("3. Back")
  231. local _, key = os.pullEvent("key")
  232. if key == keys.one then
  233. term.setCursorPos(2,9); term.clearLine(); term.write("Enter new username: ")
  234. config.username = read(); saveConfig()
  235. elseif key == keys.two then
  236. term.setCursorPos(2,9); term.clearLine(); term.write("Color name (e.g. yellow): ")
  237. local colorName = read()
  238. if colors[colorName] then config.accentColor = colors[colorName]; saveConfig()
  239. else term.setCursorPos(2,10); term.write("Invalid color."); sleep(1) end
  240. elseif key == keys.three then return end
  241. end
  242. end
  243.  
  244. -- === Top bar and signal (non-blocking) ===
  245. local signalStrength = 0
  246. local function distanceToBars(distance)
  247. if distance <= 5 then return 3
  248. elseif distance <= 20 then return 2
  249. else return 1 end
  250. end
  251.  
  252. -- async updater: find servers and estimate distance to nearest
  253. local function updateSignal()
  254. while true do
  255. local servers = findServers()
  256. if #servers == 0 then signalStrength = 0
  257. else
  258. local me = os.getComputerID()
  259. local bestd = nil
  260. for _,s in ipairs(servers) do
  261. local d = math.abs(me - s)
  262. if not bestd or d < bestd then bestd = d end
  263. end
  264. if bestd then signalStrength = distanceToBars(bestd) else signalStrength = 1 end
  265. end
  266. sleep(5)
  267. end
  268. end
  269.  
  270. local function drawSignalBars()
  271. local barChar = "|"
  272. for i = 1, 3 do
  273. if i <= signalStrength then term.setTextColor(colors.lime) else term.setTextColor(colors.gray) end
  274. term.write(barChar)
  275. end
  276. term.setTextColor(colors.white)
  277. end
  278.  
  279. local function drawTopBar()
  280. local w,_ = term.getSize()
  281. term.setCursorPos(1,1)
  282. term.setBackgroundColor(config.accentColor)
  283. term.clearLine()
  284. term.setCursorPos(2,1)
  285. if rednet.isOpen(config.rednetSide) then drawSignalBars(); term.write(" Net")
  286. else term.setTextColor(colors.red); term.write("X NoNet"); term.setTextColor(colors.white) end
  287. local timeStr = textutils.formatTime(os.time(), true)
  288. local dateStr = os.date("%b %d")
  289. local clock = timeStr .. " | " .. dateStr
  290. term.setCursorPos(w - #clock - 1, 1); term.setTextColor(colors.white); term.write(clock)
  291. end
  292.  
  293. -- === Main UI ===
  294. local function drawMenu(selected)
  295. local apps = {
  296. {name="Messages", fn=chatUI},
  297. {name="Files", fn=function() shell.run("shell") end},
  298. {name="Worm", fn=function() shell.run("worm") end},
  299. {name="Falling Blocks", fn=function() shell.run("falling") end},
  300. {name="Settings", fn=openSettings},
  301. {name="About Lemonphone", fn=aboutLemonphone},
  302. {name="Shutdown", fn=function() os.shutdown() end}
  303. }
  304. local startY = 4
  305. term.setTextColor(colors.white)
  306. for i,app in ipairs(apps) do
  307. term.setCursorPos(4, startY + i - 1)
  308. if i == selected then term.setBackgroundColor(colors.gray); term.setTextColor(colors.black)
  309. else term.setBackgroundColor(config.bgColor); term.setTextColor(colors.white) end
  310. term.clearLine()
  311. term.setCursorPos(4, startY + i - 1)
  312. term.write(app.name)
  313. end
  314. term.setBackgroundColor(config.bgColor)
  315. return apps
  316. end
  317.  
  318. -- main loop
  319. local function mainLoop()
  320. -- ensure registration attempt in background
  321. local ok, phone = ensureRegistration()
  322. if ok then
  323. -- if server accepted, nothing else
  324. end
  325.  
  326. local selected = 1
  327. local refreshRate = 1
  328. os.startTimer(0) -- kick timers
  329. while true do
  330. term.setBackgroundColor(config.bgColor); term.clear()
  331. drawTopBar()
  332. centerText(3, (config.username or "User") .. "'s Lemonphone", config.accentColor)
  333. local apps = drawMenu(selected)
  334. local timer = os.startTimer(refreshRate)
  335. local event, a = os.pullEvent()
  336. if event == "key" then
  337. if a == keys.up then selected = math.max(1, selected - 1)
  338. elseif a == keys.down then selected = math.min(#apps, selected + 1)
  339. elseif a == keys.enter then
  340. local chosen = apps[selected]
  341. term.setBackgroundColor(config.bgColor); term.clear(); term.setCursorPos(1,1)
  342. chosen.fn()
  343. end
  344. elseif event == "timer" then
  345. -- redraw topbar only
  346. drawTopBar()
  347. elseif event == "lemon_message" then
  348. -- optional: show small banner at top for a second
  349. term.setCursorPos(2,2)
  350. term.setTextColor(colors.white)
  351. term.write("[New message]")
  352. sleep(1)
  353. end
  354. end
  355. end
  356.  
  357. -- === Startup: background tasks ===
  358. local function startAll()
  359. parallel.waitForAny(updateSignal, backgroundListener, mainLoop)
  360. end
  361.  
  362. -- ensure rednet host presence? not needed for client
  363. print("Starting LemonOS 3.0...")
  364. startAll()
  365.  
Advertisement
Add Comment
Please, Sign In to add comment