1
-- BIMG Board written by ShreksHellraiser
2
-- This is software which will display a grid of BIMGs on a monitor.
3
-- Users can pay to upload BIMGs to the monitor and the images will be cycled out on a FIFO basis.
4
-- Users may pay more to change the palette of the monitor.
5
-- Supports animations and custom palettes
6
7
-- To easily delete something someone posts on the board run `board admin`.
8
-- While this is active tapping an image on the board instantly deletes it
9
-- BE CAREFUL!
10
11
-- You have permission to modify and redistribute this
12
-- I ask that you do not run another main instance on SC3
13
-- and instead just let all boards communicate with my main instance.
14
15
-- You are free to modify, but please keep your modifications compatible.
16
-- Any added / changed features can be noted in the instance_string variable
17
-- as this is displayed upon \bimgboard [id]
18
19
settings.define("board.privatekey", { description = "Krist private key", type = "string" })
20
settings.define("board.price", { description = "Base image price", type = "number" })
21
settings.define("board.pal_price", { description = "Palette applied price", type = "number" })
22
settings.define("board.address", { description = "Krist address/name", type = "string" })
23
settings.define("board.chatbox", { description = "Chatbox command", type = "string", default = "bimgboard" })
24
settings.define("board.width", { description = "Image cells wide", type = "number" })
25
settings.define("board.height", { description = "Image cells tall", type = 'number' })
26
settings.define("board.side", { description = "Monitor side", type = "string" })
27
settings.define("board.cache", { description = "Folder for bimg cache", type = "string", default = "disk" })
28
settings.define("board.main",
29
{
30
description = "If this board should take control of the chatbox interface",
31
type = "boolean",
32
default = false
33
})
34
settings.define("board.id", { description = "This board's specific ID", type = "string" })
35
settings.define("board.location", { description = "This board's location description", type = "string" })
36
settings.define("board.cut",
37
{
38
description = "Automatically provide this percentage [0,1] of earnings to board.donate_address",
39
type = "number",
40
default = 0.05
41
})
42
settings.define("board.donate_address",
43
{ description = "Address to send board.cut percentage of earnings to", type = "string", default = "[email protected]" })
44
settings.define("board.donate_minimum",
45
{ description = "Minimum donation due before a transaction will be made.", type = "number", default = 10 })
46
settings.define("board.enable_donations", { description = "Enable donations", type = "boolean", default = true })
47
settings.define("board.first_run", { type = "boolean", default = true })
48
settings.save()
49
50
if not fs.exists("ktwsl.lua") then
51
print("Missing ktwsl, installing...")
52
local h = assert(http.get("https://raw.githubusercontent.com/MasonGulu/msks/main/ktwsl.lua"))
53
local f = assert(fs.open("ktwsl.lua", "w"))
54
f.write(h.readAll())
55
f.close()
56
h.close()
57
end
58
59
local function assert_get(setting)
60
local value = settings.get(setting)
61
if not value then
62
local details = settings.getDetails(setting)
63
print(("Setting %s is not set."):format(setting))
64
print(("Description: %s"):format(details.description))
65
print(("Type %s"):format(details.type))
66
while true do
67
print("Please enter a value:")
68
local input = read()
69
if details.type == "number" then
70
if tonumber(input) then
71
value = tonumber(input)
72
break
73
end
74
elseif input ~= "" then
75
value = input
76
break
77
end
78
end
79
settings.set(setting, value)
80
settings.save()
81
end
82
return value
83
end
84
local modem = assert(peripheral.find("modem", function(name, wrapped)
85
return wrapped.isWireless()
86
end), "Attach a wireless modem!") --[[@as Modem]]
87
local privatekey = assert_get("board.privatekey")
88
local krist = require("ktwsl")("https://krist.dev", privatekey)
89
local price = assert_get("board.price")
90
local pal_price = assert_get("board.pal_price")
91
local address = assert_get("board.address")
92
local chatbox_cmd = assert_get("board.chatbox")
93
local donate_cut = settings.get("board.cut", 0.05)
94
local donation_address = settings.get("board.donation_address", "[email protected]")
95
local donate_minimum = settings.get("board.donate_minimum", 10)
96
local enable_donations = settings.get("board.enable_donations", true)
97
local id = assert_get("board.id")
98
local x, y, z
99
while not x do
100
print("Locating..")
101
x, y, z = gps.locate()
102
end
103
local coords = ("{%d,%d,%d}"):format(x, y, z)
104
local location = assert_get("board.location")
105
local main = settings.get("board.main", false)
106
local cache = settings.get("board.cache")
107
108
if settings.get("board.first_run") then
109
print(("Would you like to donate %.2f%% of revenue from this board to %s? [Y/n]"):format(donate_cut * 100,
110
donation_address))
111
enable_donations = read():lower():sub(1, 1) ~= "n"
112
settings.set("board.enable_donations", enable_donations)
113
settings.set("board.first_run", false)
114
settings.save()
115
end
116
117
local strings = require "cc.strings"
118
119
local admin = arg[1] == "admin"
120
if admin then
121
print("Running in admin mode.")
122
print("Tap on the screen to remove a listing.")
123
end
124
125
krist.subscribeAddress(address)
126
127
---@type Monitor
128
local device = peripheral.wrap(assert_get("board.side")) --[[@as Monitor]]
129
device.setTextScale(0.5)
130
device.clear()
131
132
local w, h = device.getSize()
133
local slot_rows = assert_get("board.height")
134
local slot_columns = assert_get("board.width")
135
136
local foot_str = "Run \\%s for info. ID %s."
137
138
local footer_height = #strings.wrap(foot_str, w)
139
local slot_h = math.floor((h - footer_height) / slot_rows)
140
local slot_w = math.floor(w / slot_columns)
141
142
local display_names = false
143
144
local slot_hp = slot_h * 3
145
local slot_wp = slot_w * 2
146
147
local file_size_limit = math.floor((fs.getCapacity(cache) / (slot_rows * slot_columns)) * 0.9)
148
149
---@type table[]
150
local bimgs = {}
151
152
---@type Window[]
153
local windows = {}
154
155
local campaigns = {}
156
157
---@type integer[]
158
local frames = {}
159
160
161
-- This data is sent to the "main" bimgboard server
162
local board_data = {
163
location = location,
164
coords = coords,
165
id = id,
166
address = address,
167
slot_w = slot_w,
168
slot_h = slot_h,
169
slot_wp = slot_wp,
170
slot_hp = slot_hp,
171
slot_rows = slot_rows,
172
slot_columns = slot_columns,
173
price = price,
174
pal_price = pal_price,
175
-- This field is for anything you desire and is displayed to the user.
176
-- Though, I recommend using this in cases when modifications to the software is made
177
-- for example, listing a format you added support for.
178
extra = ""
179
}
180
181
local instance_string = ("&6&lCycling Board %s\n"):format(id) ..
182
("&fFor more information on how to use this board, run \\%s.\n"):format(chatbox_cmd) ..
183
("Resolution: &9%u&fx&9%u&f pixels, &9%u&fx&9%u&f characters\n"):format(slot_wp, slot_hp, slot_w, slot_h) ..
184
("Price: &9%u&f|&9%uKST &fto %s\n"):format(price, pal_price, address) ..
185
"&cSee something that shouldn't be here? Click the screen to show who purchased each slot."
186
187
local main_string = "**=== Cycling Board ===**\n" ..
188
"This is a system of boards which can display user provided [BIMG](https://github.com/SkyTheCodeMaster/bimg/blob/master/spec.md) files. " ..
189
("Run `\\%s bimg` for information on BIMG.\n"):format(chatbox_cmd) ..
190
("Each board has varying size, prices, and amount of slots, these slots are cycled out on a FIFO basis. To view a specific board's information run `\\%s [id]`.\n")
191
:format(chatbox_cmd) ..
192
("For information on uploading an image to a board, run `\\%s usage`.\n"):format(chatbox_cmd) ..
193
("To see all currently running boards, run `\\%s list`.\n"):format(chatbox_cmd) ..
194
("For palette information run `\\%s palette`.\n"):format(chatbox_cmd)
195
196
local main_palette_string = "**=== Cycling Board Palette Info ===**\n" ..
197
"A monitor can display multiple images, but only 16 colors. Those 16 colors are assignable, but at the cost of invalidating all images.\n" ..
198
"For this reason there are 2 prices. The first indicates adding an image, the second indicates adding an image and changing the palette to match.\n" ..
199
"Since it is cheaper not to change the palette, you can opt to make your image fit the current palette.\n" ..
200
("To take advantage of the current palette you can get a monitor's current palette by running `\\%s [id] palette`. ")
201
:format(chatbox_cmd) ..
202
("The string provided can be placed directly into the BIMG Generator's custom palette field.\n"):format(chatbox_cmd)
203
204
local main_usage_string = "**=== Cycling Board Usage ===**\n" ..
205
"To upload an image to the board you will first need to host your image somewhere. " ..
206
"This board supports downloading a URL's contents directly, or using a `p.sc3.io` paste code. Downloads are done in binary mode.\n" ..
207
"Once your image is uploaded somewhere, you may send a payment to the board. There are two metadata fields that are relavant\n" ..
208
"* url - Set this to your raw URL, or your p.sc3.io paste ID.\n" ..
209
"* pal - Set this to true if you want your image's palette to be applied, the price will be higher.\n" ..
210
"For example, for an image hosted at paste ID `44wbJ6h33y` one could run `/pay [email protected] 10 url=44wbJ6h33y;pal=true`."
211
212
local main_bimg_string = "**=== Cycling Board BIMG Info ===**\n" ..
213
"There exist several tools for converting images to [BIMG](https://github.com/SkyTheCodeMaster/bimg/blob/master/spec.md).\n" ..
214
"* [BIMG Generator](https://github.com/MasonGulu/BIMG-Generator) Java; Supports GIFs, high quality dithering, user-defined palettes, text/binary exporting.\n" ..
215
"* [Sanjuuni](https://github.com/MCJack123/sanjuuni) C++; Supports images, videos, high quality dithering, text exporting.\n" ..
216
"* [Sanjuuni UI](https://github.com/MCJack123/sanjuuni-ui) Sanjuuni, but with QT GUI.\n" ..
217
"* [Online BIMG Generator](https://masongulu.github.io/js-bimg-generator/) JS; Supports user-defined palettes, low quality dithering, text exporting.\n" ..
218
"BIMG supports animations and custom palettes. Since a monitor can only display one palette changing the palette is destructive to existing images. For this reason custom palettes cost more.\n" ..
219
"Some of these converters may export a binary file. This is okay, as long as you upload to a host that supports binary files. Pastebin and p.sc3.io do not."
220
221
local pow_2 = {}
222
for i = 0, 15 do
223
pow_2[i] = 2 ^ i
224
end
225
226
local function get_palette_string(palette)
227
local str = "The current palette is `["
228
for i = 0, 15 do
229
local pstr = palette[i][1]
230
if palette[i][2] then
231
pstr = colors.packRGB(palette[i][1], palette[i][2], palette[i][3])
232
end
233
str = str .. pstr
234
if i ~= 15 then
235
str = str .. ","
236
end
237
end
238
str = str .. "]`"
239
return str
240
end
241
242
-- "Pay \164%u to %s with url=<url | p.sc3.io id> to add your image " ..
243
-- "or \164%u and add pal=true to apply the palette from your image (example: /pay user 10 url=123;pal=true).\n" ..
244
-- "Accepts BIMGs using default palette, %ux%u pixels, %ux%u chars.\n" ..
245
-- "Touch monitor to toggle names."
246
247
local footer = strings.wrap(foot_str:format(chatbox_cmd, id), w)
248
249
250
---@param filename string
251
---@return table?
252
local function load_file(filename)
253
local f = fs.open(filename, "r")
254
if not f then
255
return
256
end
257
local data = textutils.unserialise(f.readAll() --[[@as string]])
258
f.close()
259
return data --[[@as table]]
260
end
261
262
local function save_file(filename, data)
263
local f = assert(fs.open(filename, "w"))
264
f.write(textutils.serialise(data, { compact = true }))
265
f.close()
266
end
267
268
---@type integer
269
local undonated_earnings = load_file("undonated_earnings.txt") --[[@as integer]] or 0
270
271
---@type {name: string, url: string, palette: boolean?}[]
272
campaigns = load_file("campaigns.txt") or {}
273
274
---@param url string
275
---@return string|nil
276
local function download_url(url)
277
local f = http.get(url, nil, true)
278
if not f then
279
return
280
end
281
local data = f.readAll()
282
f.close()
283
return data
284
end
285
286
---@return table?
287
local function download_image(url)
288
-- todo check for p.sc3.io codes
289
if string.match(url, "^[%a%d]+$") then
290
-- p.sc3.io
291
local f = http.get("https://p.sc3.io/api/v1/pastes/" .. url)
292
if not f then
293
print("not a valid p.sc3.io code")
294
return
295
end
296
local info = textutils.unserialiseJSON(f.readAll() --[[@as string]])
297
f.close()
298
if not info then
299
print("invalid response")
300
return
301
end
302
url = info.url -- fall through
303
print("found p.sc3.io url", url)
304
end
305
local d = download_url(url)
306
if not d then
307
print("not a valid url")
308
return
309
end
310
return textutils.unserialise(d) --[[@as table?]]
311
end
312
local current_palette
313
314
local function apply_palette(palette, dev)
315
dev = dev or device
316
for k, v in pairs(palette) do
317
dev.setPaletteColor(pow_2[k], table.unpack(v))
318
end
319
end
320
321
local function get_high_contrast()
322
local brightest, darkest = 0, 255
323
local brightest_color, darkest_color
324
local function _ca(r, g, b)
325
return 255 * ((r + g + b) / 3)
326
end
327
for i = 0, 15 do
328
local color_average
329
if current_palette[i][2] then
330
color_average = _ca(table.unpack(current_palette[i]))
331
else
332
color_average = _ca(colors.unpackRGB(current_palette[i][1]))
333
end
334
if color_average < darkest then
335
darkest = color_average
336
darkest_color = 2 ^ i
337
end
338
if color_average > brightest then
339
brightest = color_average
340
brightest_color = 2 ^ i
341
end
342
end
343
return brightest_color, darkest_color
344
end
345
local fg, bg
346
347
---@param i integer
348
local function render_bimg(i)
349
local win, bimg, name = windows[i], bimgs[i], campaigns[i].name
350
win.setVisible(false)
351
apply_palette(current_palette, win)
352
win.clear()
353
frames[i] = (frames[i] or 0) + 1
354
if frames[i] > #bimg then
355
frames[i] = 1
356
end
357
for yPos, v in pairs(bimg[frames[i]]) do
358
if type(yPos) == "number" then
359
win.setCursorPos(1, yPos)
360
win.blit(table.unpack(v))
361
end
362
end
363
if display_names then
364
win.setTextColor(fg)
365
win.setBackgroundColor(bg)
366
local sname = strings.wrap(name, slot_w)
367
for line, s in ipairs(sname) do
368
win.setCursorPos(1, line)
369
win.write(s)
370
end
371
end
372
win.setVisible(true)
373
end
374
375
376
for column = 1, slot_columns do
377
for row = 1, slot_rows do
378
local i = column + ((row - 1) * slot_columns)
379
local x = 1 + ((column - 1) * slot_w)
380
local y = 1 + ((row - 1) * slot_h)
381
windows[i] = window.create(device, x, y, slot_w, slot_h)
382
end
383
end
384
385
local function load_bimgs()
386
for i = 1, slot_rows * slot_columns do
387
if campaigns[i] then
388
local fn = fs.combine(cache, tostring(i))
389
if fs.exists(fn) then
390
bimgs[i] = load_file(fn)
391
else
392
bimgs[i] = download_image(campaigns[i].url)
393
end
394
frames[i] = 1
395
end
396
end
397
end
398
load_bimgs()
399
400
local function save_bimgs()
401
for k, v in pairs(fs.list(cache)) do
402
if not fs.isDir(v) then
403
fs.delete(v)
404
end
405
end
406
for i = 1, slot_rows * slot_columns do
407
local fn = fs.combine(cache, tostring(i))
408
if bimgs[i] then
409
save_file(fn, bimgs[i])
410
end
411
end
412
end
413
save_bimgs()
414
415
local function get_palette()
416
if fs.exists("palette") then
417
return load_file("palette")
418
else
419
for i, v in ipairs(campaigns) do
420
if v.palette then
421
local bimg = bimgs[i] or download_image(v.url)
422
bimgs[i] = bimg
423
print("got palette", i)
424
local palette = bimg.palette or bimg[1].palette
425
save_file("palette", palette)
426
return palette
427
end
428
end
429
end
430
local palette = {}
431
-- default palette
432
print("default palette")
433
for i, v in pairs(pow_2) do
434
palette[i] = { term.nativePaletteColor(v) }
435
end
436
save_file("palette", palette)
437
return palette
438
end
439
440
local function draw_screen()
441
current_palette = get_palette()
442
apply_palette(current_palette)
443
fg, bg = get_high_contrast()
444
device.setTextColor(fg)
445
device.setBackgroundColor(bg)
446
device.clear()
447
for i = 1, slot_rows * slot_columns do
448
if bimgs[i] then
449
render_bimg(i)
450
end
451
end
452
device.setTextColor(fg)
453
device.setBackgroundColor(bg)
454
for l = 1, #footer do
455
device.setCursorPos(1, h - #footer + (l - 1))
456
device.write(footer[l])
457
end
458
if admin then
459
device.setCursorPos(1, h - #footer)
460
device.clearLine()
461
device.write("ADMIN MODE")
462
end
463
device.setCursorPos(1, 1)
464
end
465
draw_screen()
466
467
468
local function add_campaign(name, url, data, use_palette)
469
table.insert(campaigns, 1, { name = name, url = url, palette = use_palette })
470
table.insert(bimgs, 1, data)
471
if use_palette then
472
fs.delete("palette")
473
end
474
save_bimgs()
475
draw_screen()
476
save_file("campaigns.txt", campaigns)
477
end
478
479
local function remove_campaign(index)
480
table.remove(campaigns, index)
481
table.remove(bimgs, index)
482
save_bimgs()
483
load_bimgs()
484
draw_screen()
485
save_file("campaigns.txt", campaigns)
486
end
487
488
local function validate_bimg(bimg)
489
if type(bimg) ~= "table" then
490
print("not a table")
491
return false, "Malformed file"
492
end
493
if not type(bimg[1] == "table") then
494
print("no first frame")
495
return false, "No first frame"
496
end
497
for yPos, v in pairs(bimg[1]) do
498
if type(yPos) == "number" then
499
if #v < 3 then
500
print("invalid frame contents")
501
return false, "Invalid BIMG frame contents"
502
end
503
end
504
end
505
-- file size validation
506
local s = textutils.serialise(bimg, { compact = true })
507
if #s > file_size_limit then
508
print("BIMG too large")
509
return false, ("BIMG is too large. Maximum file size is %u."):format(file_size_limit)
510
end
511
print("valid bimg")
512
return true
513
end
514
515
local function check_donation()
516
local donation = undonated_earnings * donate_cut
517
if math.floor(donation) == math.ceil(donation) and donation > donate_minimum then
518
-- send a transaction
519
krist.makeTransaction(donation_address, donation, ("message=Donation cut from bimgboard %s"):format(id))
520
undonated_earnings = 0
521
save_file("undonated_earnings.txt", undonated_earnings)
522
end
523
end
524
525
local function handle_transactions()
526
while true do
527
local e = table.pack(os.pullEventRaw())
528
if e[1] == "krist_transaction" and e[2] == address then
529
print("got transaction")
530
local metadata = krist.parseMetadata(e[5].metadata)
531
local url_data = metadata.url and download_image(metadata.url)
532
print(metadata.url, url_data)
533
local price_owed = price
534
if metadata.pal == "true" then
535
price_owed = pal_price
536
end
537
local valid, reason
538
if url_data and e[4] >= price_owed then
539
valid, reason = validate_bimg(url_data)
540
end
541
if valid then
542
if e[4] > price_owed then
543
-- refund
544
krist.makeTransaction(e[3], e[4] - price_owed,
545
("return=%s;message=%s"):format(address, "You paid too much"))
546
end
547
if enable_donations then
548
undonated_earnings = undonated_earnings + price_owed
549
save_file("undonated_earnings.txt", undonated_earnings)
550
check_donation()
551
end
552
add_campaign(e[3], metadata.url, url_data, metadata.pal == "true")
553
else
554
local fail_str = ("Invalid BIMG file: %s"):format(reason)
555
if not url_data then
556
fail_str = "Please provide a valid url or p.sc3.io code in the url field"
557
end
558
if e[4] < price_owed then
559
fail_str = ("You paid too little, owed %u"):format(price_owed)
560
end
561
-- invalid url
562
krist.makeTransaction(e[3], e[4], ("return=%s;message=%s"):format(address, fail_str))
563
end
564
elseif e[1] == "krist_stop" then
565
for l = 1, #footer do
566
device.setCursorPos(1, h - #footer + (l - 1))
567
device.clearLine()
568
device.write(("Krist Handler Errored: %s"):format(e[2]))
569
end
570
error(e[2])
571
elseif e[1] == "monitor_touch" then
572
if admin then
573
-- delete a campaign
574
local campaign = math.floor(e[3] / slot_w) + 1 + (math.floor(e[4] / slot_h) * slot_columns)
575
remove_campaign(campaign)
576
end
577
display_names = not display_names
578
draw_screen()
579
elseif e[1] == "terminate" then
580
krist.stop()
581
error("Terminated")
582
end
583
end
584
end
585
586
local port = 25799
587
modem.open(port)
588
local function handle_modem()
589
while true do
590
local _, _, channel, reply, message, dist = os.pullEvent("modem_message")
591
if message == "list" then
592
modem.transmit(reply, port, board_data)
593
elseif type(message) == "table" and message[1] == "palette" and message[2] == id then
594
modem.transmit(reply, port, { palette = get_palette_string(current_palette), id = id })
595
elseif type(message) == "table" and message[1] == "instance_string" and message[2] == id then
596
modem.transmit(reply, port, { instance_string = instance_string, id = id })
597
end
598
end
599
end
600
601
local function get_all_monitors()
602
print("getting monitors")
603
modem.transmit(port, port, "list")
604
local timer = os.startTimer(2)
605
local monitors = {}
606
monitors[id] = board_data
607
while true do
608
local event, t, channel, reply, message, dist = os.pullEvent()
609
if event == "modem_message" and type(message) == "table" and message.id then
610
print("got monitor", message.id)
611
monitors[message.id] = message
612
elseif event == "timer" and t == timer then
613
print("done getting monitors")
614
return monitors -- exit condition
615
end
616
end
617
end
618
619
local function get_palette_of(tid)
620
if tid == id then
621
return get_palette_string(current_palette)
622
end
623
modem.transmit(port, port, { "palette", tid })
624
local timer = os.startTimer(2)
625
while true do
626
local event, t, channel, reply, message, dist = os.pullEvent()
627
if event == "modem_message" and type(message) == "table" and message.id == tid and message.palette then
628
return message.palette
629
elseif event == "timer" and t == timer then
630
return nil
631
end
632
end
633
end
634
635
local function get_instance_string_of(tid)
636
if tid == id then
637
return instance_string
638
end
639
modem.transmit(port, port, { "instance_string", tid })
640
local timer = os.startTimer(2)
641
while true do
642
local event, t, channel, reply, message, dist = os.pullEvent()
643
if event == "modem_message" and type(message) == "table" and message.id == tid and message.instance_string then
644
return message.instance_string
645
elseif event == "timer" and t == timer then
646
return nil
647
end
648
end
649
end
650
651
local function handle_commands()
652
assert(main, "Attempt to handle chatbox commands when not main bimgboard!")
653
assert(chatbox.isConnected(), "Chatbox isn't connected!")
654
while true do
655
local e = { os.pullEventRaw("command") }
656
if e[3] == chatbox_cmd then
657
local user = e[2]
658
local args = e[4]
659
if not args[1] then
660
print("general help")
661
chatbox.tell(user, main_string, chatbox_cmd, nil, "markdown")
662
elseif args[1] == "bimg" then
663
print("bimg help")
664
chatbox.tell(user, main_bimg_string, chatbox_cmd, nil, "markdown")
665
elseif args[1] == "usage" then
666
print("usage help")
667
chatbox.tell(user, main_usage_string, chatbox_cmd, nil, "markdown")
668
elseif args[1] == "palette" then
669
print("palette help")
670
chatbox.tell(user, main_palette_string, chatbox_cmd, nil, "markdown")
671
else
672
local monitors = get_all_monitors()
673
print(textutils.serialise(monitors))
674
if monitors[args[1]] then
675
print("listing monitor information")
676
if args[2] == "palette" then
677
local palette = get_palette_of(args[1])
678
if palette then
679
chatbox.tell(user, palette, chatbox_cmd, nil, "markdown")
680
end
681
else
682
local instance = get_instance_string_of(args[1])
683
if instance then
684
chatbox.tell(user, instance, chatbox_cmd, nil, "format")
685
end
686
end
687
elseif args[1] == "list" then
688
print("listing")
689
local response = "&9=== BIMG Monitor Listing ===&f\n"
690
for _, v in pairs(monitors) do
691
response = response ..
692
("ID %s @ %s %s. Resolution &9%u&fx&9%u&f pixels. Price &9%u&f|&9%uKST&f.\n"):format(v.id,
693
v.coords, v.location, v.slot_wp, v.slot_hp, v.price, v.pal_price)
694
end
695
chatbox.tell(user, response, chatbox_cmd, nil, "format")
696
end
697
end
698
end
699
end
700
end
701
702
local function handle_frames()
703
while true do
704
sleep(0.2)
705
draw_screen()
706
end
707
end
708
709
local to_execute = { handle_transactions, krist.start, handle_frames }
710
711
if main then
712
to_execute[#to_execute + 1] = handle_commands
713
else
714
to_execute[#to_execute + 1] = handle_modem
715
end
716
717
parallel.waitForAny(table.unpack(to_execute))
718