forked from hkc/cc-stuff
Compare commits
133 Commits
augment/de
...
master
Author | SHA1 | Date |
---|---|---|
capta1nseal | 5662f536d7 | |
capta1nseal | 3583a347c2 | |
Casey | a0450a7d59 | |
Casey | d511cc407e | |
Casey | 1e364fe3ea | |
Casey | 33e3c1ad8c | |
Casey | a665f9498d | |
Casey | 094308ce0b | |
Casey | 0afb88e147 | |
Casey | a06361ad4a | |
Casey | c4740b8bfd | |
Casey | 989daae8e8 | |
Casey | bed20ee4d3 | |
Casey | 703a1744b5 | |
Casey | 92eb5b325c | |
capta1nseal | 186bc7814d | |
Casey | 89bb807cc2 | |
Casey | a49e996e1b | |
Casey | abe0b37f7d | |
Casey | 5e1d23b4bb | |
Casey | 3cb26f3b54 | |
Casey | 330bfa8f80 | |
Casey | 1ba2cf9aed | |
Casey | f6b91af2ee | |
Casey | a00dca3be1 | |
Casey | 38cce4226a | |
Casey | a3b079e638 | |
Casey | 712346e2c5 | |
Casey | 17cc43fe89 | |
Casey | 2fbb51cefe | |
Casey | 52f2a55cc1 | |
Casey | 8e9dd958eb | |
Casey | 710699f5cb | |
Casey | bf3c5e6b1c | |
Casey | 4922c5550f | |
Casey | 1e4fa997c0 | |
Casey | e61774d4dd | |
Casey | 49fa1d7547 | |
Casey | 5872a931e1 | |
Casey | ba07886a4f | |
Casey | c917da78cb | |
Casey | bd9d88e12c | |
Casey | 2f889add9b | |
Casey | 637827c6af | |
Casey | 6dcd1cd1b2 | |
Casey | 9ea564347b | |
Casey | 2bc30e773a | |
Casey | 1ea1bb7c9e | |
Casey | 5a581c519f | |
Casey | 46d68ca483 | |
Casey | f25755a684 | |
Casey | fee1bd351e | |
Casey | 4930ea6645 | |
Casey | d543f68947 | |
Casey | 7533a3bae1 | |
Casey | 7d1343a0d7 | |
Casey | 9ec3039265 | |
Casey | 06314ba26a | |
Casey | 21a63ccdef | |
Casey | bd3b8b810c | |
Casey | cea17fc255 | |
Casey | cdaa2c210d | |
Casey | a396ab67ca | |
Casey | 55a9d73fc0 | |
Casey | 6b250113be | |
Casey | b051d36bc0 | |
Casey | 3dbd27525a | |
Casey | d6d0e42978 | |
Casey | 7e94bc4f0a | |
Casey | 32caf7810e | |
Casey | 621a303553 | |
Casey | c75075ae9c | |
Casey | 55ce35743e | |
Casey | 18de04bfc1 | |
Casey | eece2153e4 | |
Casey | dd42590052 | |
Casey | 77c6d13689 | |
Casey | bbede80038 | |
Casey | d04eeacb36 | |
Casey | 062d16ea69 | |
Casey | 7d7b03474d | |
Casey | 2e64a3200f | |
Casey | d657c1852d | |
Casey | d84782802f | |
Casey | 3543ef9595 | |
Casey | 086b8374ad | |
Casey | 2dcb17488b | |
Casey | db48802e8c | |
Casey | c713e0118d | |
Casey | b4d4d64c16 | |
Casey | 11be60a0b1 | |
Casey | e938f38ab1 | |
Casey | 135b606686 | |
Casey | 9e44c53e42 | |
Casey | 736d763b64 | |
Casey | 0bf8549a65 | |
Casey | 038f15afb1 | |
Casey | 1862d70178 | |
Casey | 525fce9232 | |
Casey | ec5d0d14f8 | |
Casey | 58b63b4cde | |
Casey | 9b6b198798 | |
Casey | 426ad21103 | |
Casey | caa1b5a84f | |
Casey | 0e047a5b35 | |
Casey | e780d7017e | |
Casey | 5b261e82c2 | |
Casey | 5fc6b26428 | |
Casey | de9ea57461 | |
Casey | 0a8bbd916c | |
Casey | 58e4631df8 | |
Casey | 62891da708 | |
Casey | adcae533d0 | |
Casey | 42063e0271 | |
Casey | a8172db8f9 | |
Casey | 99d5f95757 | |
Casey | f4fb54c23a | |
Casey | 8175c31733 | |
Casey | a3c25f1442 | |
Casey | d40c689d66 | |
Casey | c23f9ed7fb | |
Casey | e8dcb586a3 | |
Casey | a316da89b9 | |
Casey | 2ed6749c87 | |
Casey | 3ad3535b67 | |
Casey | a2d8c592a7 | |
Casey | 570e721e3e | |
Casey | 635886f683 | |
Casey | 3caefb1c41 | |
Casey | 90e1709131 | |
Casey | e442a087f8 | |
Casey | c3f54e301a | |
Casey | 0c79a8017c |
|
@ -0,0 +1,41 @@
|
||||||
|
local ccpi = require("ccpi")
|
||||||
|
local args = { ... }
|
||||||
|
|
||||||
|
local terminal = term.current()
|
||||||
|
if args[1] == "-m" then
|
||||||
|
table.remove(args, 1)
|
||||||
|
print("Using monitor: " .. args[1])
|
||||||
|
terminal = peripheral.wrap(table.remove(args, 1))
|
||||||
|
end
|
||||||
|
|
||||||
|
local frames = {}
|
||||||
|
local n_frames = tonumber(args[1])
|
||||||
|
local base_path = args[2]
|
||||||
|
|
||||||
|
for i = 1, n_frames do
|
||||||
|
local url = string.format(base_path, i)
|
||||||
|
print("GET " .. url)
|
||||||
|
local req, err = http.get(url, nil, true)
|
||||||
|
if not req then
|
||||||
|
printError(err)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local img, err = ccpi.parse(req)
|
||||||
|
if not img then
|
||||||
|
printError(err)
|
||||||
|
return
|
||||||
|
else
|
||||||
|
print(img.w .. "x" .. img.h)
|
||||||
|
end
|
||||||
|
table.insert(frames, img)
|
||||||
|
req.close()
|
||||||
|
end
|
||||||
|
|
||||||
|
local frame_no = 0
|
||||||
|
while true do
|
||||||
|
local frame = frames[(frame_no % #frames) + 1]
|
||||||
|
ccpi.draw(frame, 1, 1, terminal)
|
||||||
|
os.sleep(0.0)
|
||||||
|
frame_no = frame_no + 1
|
||||||
|
end
|
||||||
|
|
|
@ -2,7 +2,11 @@
|
||||||
"repository": "https://git.salushnes.solutions/hkc/cc-stuff/raw/branch/master/augment",
|
"repository": "https://git.salushnes.solutions/hkc/cc-stuff/raw/branch/master/augment",
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"path": "startup",
|
"path": "wsvpn.lua",
|
||||||
|
"src": "wsvpn.lua"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "startup.lua",
|
||||||
"src": "startup.lua"
|
"src": "startup.lua"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -45,7 +45,7 @@ local function safeset(func, name, old)
|
||||||
if func then
|
if func then
|
||||||
local s, res = pcall(func)
|
local s, res = pcall(func)
|
||||||
if not s then
|
if not s then
|
||||||
print("ERR: " .. name .. " failed: " .. res)
|
printError("ERR: " .. name .. " failed: " .. res)
|
||||||
else
|
else
|
||||||
return res
|
return res
|
||||||
end
|
end
|
||||||
|
@ -60,10 +60,7 @@ parallel.waitForAll(function()
|
||||||
local ev = { os.pullEvent("exit") }
|
local ev = { os.pullEvent("exit") }
|
||||||
if ev[1] == "exit" then
|
if ev[1] == "exit" then
|
||||||
_G._running = false
|
_G._running = false
|
||||||
local oldc = term.getTextColor()
|
printError("Caught exit event, shutting down...")
|
||||||
term.setTextColor(colors.red)
|
|
||||||
print("Caught exit event, shutting down...")
|
|
||||||
term.setTextColor(oldc)
|
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
return function()
|
return function()
|
||||||
while _G._running do
|
while _G._running do
|
||||||
local ev = { os.pullEvent() }
|
local ev = { os.pullEvent() }
|
||||||
if ev[1] == "key" and ev[2] == keys.q then
|
if ev[1] == "key" and ev[2] == keys.f4 then
|
||||||
os.queueEvent("exit")
|
os.queueEvent("exit")
|
||||||
break
|
break
|
||||||
elseif ev[1] == "timer" or ev[1] == "plethora_task" then
|
elseif ev[1] == "timer" or ev[1] == "plethora_task" then
|
||||||
|
|
|
@ -21,6 +21,7 @@ return function()
|
||||||
cache[id].cube.setAlpha(0x20)
|
cache[id].cube.setAlpha(0x20)
|
||||||
cache[id].cube.setDepthTested(false)
|
cache[id].cube.setDepthTested(false)
|
||||||
cache[id].frame.setDepthTested(false)
|
cache[id].frame.setDepthTested(false)
|
||||||
|
|
||||||
cache[id].cube.setPosition(entity.x - 0.25, entity.y - 0.25, entity.z - 0.25)
|
cache[id].cube.setPosition(entity.x - 0.25, entity.y - 0.25, entity.z - 0.25)
|
||||||
cache[id].frame.setPosition(entity.x, entity.y, entity.z)
|
cache[id].frame.setPosition(entity.x, entity.y, entity.z)
|
||||||
cache[id].text.setAlpha(0xFF)
|
cache[id].text.setAlpha(0xFF)
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
local expect = require("cc.expect")
|
||||||
|
|
||||||
|
local WSModem = {
|
||||||
|
open = function(self, channel)
|
||||||
|
expect.expect(1, channel, "number")
|
||||||
|
expect.range(channel, 0, 65535)
|
||||||
|
self._request(0x4f, {
|
||||||
|
bit.band(0xFF, bit.brshift(channel, 8)),
|
||||||
|
bit.band(0xFF, channel)
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
isOpen = function(self, channel)
|
||||||
|
expect.expect(1, channel, "number")
|
||||||
|
expect.range(channel, 0, 65535)
|
||||||
|
return self._request(0x6f, {
|
||||||
|
bit.band(0xFF, bit.brshift(channel, 8)),
|
||||||
|
bit.band(0xFF, channel)
|
||||||
|
})[1] ~= 0
|
||||||
|
end,
|
||||||
|
close = function(self, channel)
|
||||||
|
expect.expect(1, channel, "number")
|
||||||
|
expect.range(channel, 0, 65535)
|
||||||
|
self._request(0x63, {
|
||||||
|
bit.band(0xFF, bit.brshift(channel, 8)),
|
||||||
|
bit.band(0xFF, channel)
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
closeAll = function(self)
|
||||||
|
self._request(0x43)
|
||||||
|
end,
|
||||||
|
transmit = function(self, channel, replyChannel, data)
|
||||||
|
expect.expect(1, channel, "number")
|
||||||
|
expect.expect(2, replyChannel, "number")
|
||||||
|
expect.expect(3, data, "nil", "string", "number", "table")
|
||||||
|
expect.range(channel, 0, 65535)
|
||||||
|
expect.range(replyChannel, 0, 65535)
|
||||||
|
|
||||||
|
local serialized = textutils.serializeJSON(data)
|
||||||
|
expect.range(#serialized, 0, 65535)
|
||||||
|
serialized = { serialized:byte(1, 65536) }
|
||||||
|
self._request(0x54, {
|
||||||
|
bit.band(0xFF, bit.brshift(channel, 8)),
|
||||||
|
bit.band(0xFF, channel),
|
||||||
|
bit.band(0xFF, bit.brshift(replyChannel, 8)),
|
||||||
|
bit.band(0xFF, replyChannel),
|
||||||
|
bit.band(0xFF, bit.brshift(#serialized, 8)),
|
||||||
|
bit.band(0xFF, #serialized),
|
||||||
|
table.unpack(serialized, 1, #serialized)
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
isWireless = function(self) return true end,
|
||||||
|
run = function(self)
|
||||||
|
while true do
|
||||||
|
local data, binary = self._socket.receive()
|
||||||
|
if not data then return true end
|
||||||
|
if binary == false then return false, "Not a binary message" end
|
||||||
|
data = { string.byte(data, 1, #data) }
|
||||||
|
local opcode = table.remove(data, 1)
|
||||||
|
if opcode == 0x49 then -- info
|
||||||
|
local len, msg = self._read_u16ne(data)
|
||||||
|
msg = string.char(table.unpack(msg))
|
||||||
|
os.queueEvent("wsvpn:info", msg)
|
||||||
|
elseif opcode == 0x41 then -- Set address/side
|
||||||
|
local len = table.remove(data, 1)
|
||||||
|
self.side = string.char(table.unpack(data, 1, len))
|
||||||
|
elseif opcode == 0x45 then -- Error
|
||||||
|
local request_id, error_length
|
||||||
|
request_id, data = self._read_u16ne(data)
|
||||||
|
error_length, data = self._read_u16ne(data)
|
||||||
|
local message = string.char(table.unpack(data, 1, error_length))
|
||||||
|
os.queueEvent("wsvpn:response", false, request_id, message)
|
||||||
|
elseif opcode == 0x52 then -- Response
|
||||||
|
local request_id, response = self._read_u16ne(data)
|
||||||
|
os.queueEvent("wsvpn:response", true, request_id, response)
|
||||||
|
elseif opcode == 0x54 then -- Transmission
|
||||||
|
local channel, replyChannel, dataSize, packet
|
||||||
|
channel, data = self._read_u16ne(data)
|
||||||
|
replyChannel, data = self._read_u16ne(data)
|
||||||
|
dataSize, packet = self._read_u16ne(data)
|
||||||
|
os.queueEvent("modem_message", self.side or "wsmodem_0", channel, replyChannel, textutils.unserializeJSON(string.char(table.unpack(data, 1, dataSize))), nil)
|
||||||
|
else
|
||||||
|
return false, string.format("Invalid opcode 0x%02x", opcode)
|
||||||
|
end
|
||||||
|
os.sleep(0)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
-- low-level part
|
||||||
|
|
||||||
|
_read_u16ne = function(self, data)
|
||||||
|
local v = bit.blshift(table.remove(data, 1), 8)
|
||||||
|
v = bit.bor(v, table.remove(data, 1))
|
||||||
|
return v, data
|
||||||
|
end,
|
||||||
|
|
||||||
|
_wait_response = function(self, request_id)
|
||||||
|
while true do
|
||||||
|
local ev, status, id, data = os.pullEvent("wsvpn:response")
|
||||||
|
if ev == "wsvpn:response" and id == request_id then
|
||||||
|
return status, data
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
_request = function(self, opcode, data)
|
||||||
|
local request_id = self._get_id()
|
||||||
|
self._socket.send(
|
||||||
|
string.char(
|
||||||
|
opcode,
|
||||||
|
bit.band(0xFF, bit.brshift(request_id, 8)),
|
||||||
|
bit.band(0xFF, request_id),
|
||||||
|
table.unpack(data or {})
|
||||||
|
),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
local status, response = self._wait_response(request_id)
|
||||||
|
if not status then
|
||||||
|
error(response)
|
||||||
|
end
|
||||||
|
return response
|
||||||
|
end,
|
||||||
|
|
||||||
|
_get_id = function(self)
|
||||||
|
self._req_id = bit.band(0xFFFF, self._req_id + 1)
|
||||||
|
return self._req_id
|
||||||
|
end,
|
||||||
|
|
||||||
|
_send_text = function(self, code, fmt, ...)
|
||||||
|
local msg = { fmt:format(...):byte(1, 1020) }
|
||||||
|
self._socket.send(
|
||||||
|
string.char(
|
||||||
|
code,
|
||||||
|
bit.band(0xFF, bit.brshift(#msg, 8)),
|
||||||
|
bit.band(0xFF, #msg),
|
||||||
|
table.unpack(msg, 1, #msg)
|
||||||
|
),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
|
||||||
|
_init = function(self)
|
||||||
|
self._send_text(0x49, "Hello! I'm computer %d", os.getComputerID())
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
return function(addr)
|
||||||
|
local ws = assert(http.websocket(addr))
|
||||||
|
local sock = setmetatable({ _socket = ws, _req_id = 0, side = "wsmodem_unknown" }, { __index = WSModem })
|
||||||
|
for name, method in pairs(WSModem) do
|
||||||
|
sock[name] = function(...) return method(sock, ...) end
|
||||||
|
end
|
||||||
|
sock._init()
|
||||||
|
return sock
|
||||||
|
end
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# x-run: python3 % badapple.bin ~/videos/badapple/frame*.png
|
||||||
|
|
||||||
|
from sys import argv
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
w, h = 82, 40
|
||||||
|
|
||||||
|
bits = [[1,2],[4,8],[16,0]]
|
||||||
|
|
||||||
|
with open(argv[1], "wb") as fp:
|
||||||
|
fp.write(bytes([w, h]))
|
||||||
|
for i, f in enumerate(argv[2:]):
|
||||||
|
with Image.open(f) as im:
|
||||||
|
img = im.resize((w * 2, h * 3)).convert("1")
|
||||||
|
for y in range(h):
|
||||||
|
line = bytearray()
|
||||||
|
for x in range(w):
|
||||||
|
val = 0
|
||||||
|
for oy, l in enumerate(bits):
|
||||||
|
for ox, bi in enumerate(l):
|
||||||
|
if img.getpixel((x * 2 + ox, y * 3 + oy)):
|
||||||
|
val |= bi
|
||||||
|
# if img.getpixel((x * 2 + 1, y * 3 + 2)):
|
||||||
|
# val ^= 0x9f
|
||||||
|
line.append(val)
|
||||||
|
fp.write(line)
|
||||||
|
print(f"wrote {i + 1} / {len(argv) - 2}")
|
Binary file not shown.
|
@ -0,0 +1,99 @@
|
||||||
|
local bigterm = require("bigterm")({
|
||||||
|
{ p = peripheral.wrap("monitor_1"), x = 1, y = 1 },
|
||||||
|
{ p = peripheral.wrap("monitor_2"), x = 2, y = 1 },
|
||||||
|
{ p = peripheral.wrap("monitor_3"), x = 1, y = 2 },
|
||||||
|
{ p = peripheral.wrap("monitor_4"), x = 2, y = 2 },
|
||||||
|
{ p = peripheral.wrap("monitor_5"), x = 1, y = 3 },
|
||||||
|
{ p = peripheral.wrap("monitor_6"), x = 2, y = 3 },
|
||||||
|
}, {
|
||||||
|
palette = {
|
||||||
|
[colors.white] = 0xEFEFEF,
|
||||||
|
[colors.orange] = 0xEF712A,
|
||||||
|
[colors.magenta] = 0xCF43EA,
|
||||||
|
[colors.lightBlue] = 0x5643EF,
|
||||||
|
[colors.yellow] = 0xEFCF42,
|
||||||
|
[colors.lime] = 0x43FA99,
|
||||||
|
[colors.pink] = 0xEF7192,
|
||||||
|
[colors.gray] = 0x676767,
|
||||||
|
[colors.lightGray] = 0xAAAAAA,
|
||||||
|
[colors.cyan] = 0x42DFFA,
|
||||||
|
[colors.blue] = 0x7853FF,
|
||||||
|
[colors.brown] = 0xb18624,
|
||||||
|
[colors.green] = 0x00FF00,
|
||||||
|
[colors.red] = 0xFF0000,
|
||||||
|
[colors.black] = 0x000000
|
||||||
|
},
|
||||||
|
scale = 0.5
|
||||||
|
})
|
||||||
|
|
||||||
|
bigterm._forEachMonitor(function(mon, i)
|
||||||
|
print(mon.x, mon.y, mon.p.getSize())
|
||||||
|
end)
|
||||||
|
|
||||||
|
--bigterm._blitpixel(1, 21, "A")
|
||||||
|
|
||||||
|
local w, h = bigterm.getSize()
|
||||||
|
print(w, h)
|
||||||
|
|
||||||
|
--for y = 1, h do
|
||||||
|
-- for x = 1, w do
|
||||||
|
-- bigterm._blitpixel(
|
||||||
|
-- x,
|
||||||
|
-- y,
|
||||||
|
-- string.char((x + y * 16 - 16) % 256),
|
||||||
|
-- string.format("%x", (x - y) % 16),
|
||||||
|
-- string.format("%x", (x + y) % 16)
|
||||||
|
-- )
|
||||||
|
-- end
|
||||||
|
--end
|
||||||
|
|
||||||
|
bigterm.setTextColor(colors.white)
|
||||||
|
bigterm.setBackgroundColor(colors.black)
|
||||||
|
|
||||||
|
bigterm.clear()
|
||||||
|
|
||||||
|
if false then
|
||||||
|
local lines = require("cc.strings").wrap(io.lines("cc-stuff/beemovie.txt")(), w)
|
||||||
|
for i, line in ipairs(lines) do
|
||||||
|
bigterm.setCursorPos(1, i)
|
||||||
|
bigterm.setTextColor(2 ^ (i % 2))
|
||||||
|
bigterm.write(line)
|
||||||
|
if i == h then break end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for t = 0, 31 do
|
||||||
|
for y = 1, h do
|
||||||
|
local res = {}
|
||||||
|
for x = 0, w do
|
||||||
|
table.insert(res, string.format("%x", bit.bxor(x, y * t + 1) % 16))
|
||||||
|
end
|
||||||
|
res = table.concat(res)
|
||||||
|
bigterm.setCursorPos(1, y)
|
||||||
|
bigterm.blit(res, res, string.rep("f", #res))
|
||||||
|
end
|
||||||
|
os.sleep(0.05)
|
||||||
|
end
|
||||||
|
|
||||||
|
local ccpi = require("ccpi")
|
||||||
|
local img = ccpi.load("cc-stuff/cpi-images/cute.cpi")
|
||||||
|
|
||||||
|
local ys = {}
|
||||||
|
for y = 1, img.h do table.insert(ys, y) end
|
||||||
|
|
||||||
|
for i = 1, #ys do
|
||||||
|
local a, b = math.random(1, #ys), math.random(1, #ys)
|
||||||
|
ys[a], ys[b] = ys[b], ys[a]
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, y in ipairs(ys) do
|
||||||
|
bigterm.setCursorPos(1, y)
|
||||||
|
bigterm.blit(img.lines[y].s, img.lines[y].fg, img.lines[y].bg)
|
||||||
|
os.sleep(0.05)
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, 16 do
|
||||||
|
bigterm.setPaletteColor(bit.blshift(1, i - 1), img.palette[i])
|
||||||
|
os.sleep(0.10)
|
||||||
|
end
|
||||||
|
|
|
@ -0,0 +1,231 @@
|
||||||
|
local expect = require("cc.expect").expect
|
||||||
|
local function todo() error("todo!") end
|
||||||
|
local pretty = require "cc.pretty"
|
||||||
|
|
||||||
|
local BigTerm = {
|
||||||
|
write = function(self, text)
|
||||||
|
local w = self.getSize()
|
||||||
|
for i = 1, #text do
|
||||||
|
if self._pos.x <= w then
|
||||||
|
self._blitpixel(
|
||||||
|
self._pos.x,
|
||||||
|
self._pos.y,
|
||||||
|
text:sub(i, i),
|
||||||
|
self._colorChar(self._colorForeground),
|
||||||
|
self._colorChar(self._colorBackground)
|
||||||
|
)
|
||||||
|
self._pos.x = self._pos.x + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
blit = function(self, text, foreground, background)
|
||||||
|
local x, y = self._pos.x, self._pos.y
|
||||||
|
local w = #text
|
||||||
|
local LIMIT = 0
|
||||||
|
local ox = 1
|
||||||
|
while w > 0 and LIMIT < 20 do
|
||||||
|
local mon, i, lx, ly = self._getMonitorForScreenPos(x, y)
|
||||||
|
if not mon then break end
|
||||||
|
local remaining = mon._w - lx
|
||||||
|
mon.p.setCursorPos(lx + 1, ly + 1)
|
||||||
|
mon.p.blit(
|
||||||
|
text:sub(ox, ox + w),
|
||||||
|
foreground:sub(ox, ox + w - 1),
|
||||||
|
background:sub(ox, ox + w - 1)
|
||||||
|
)
|
||||||
|
w = w - remaining
|
||||||
|
x = x + remaining
|
||||||
|
ox = ox + remaining
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
clear = function(self)
|
||||||
|
self._forEachMonitor(function(mon)
|
||||||
|
mon.p.clear()
|
||||||
|
end)
|
||||||
|
end,
|
||||||
|
clearLine = function(self) todo() end,
|
||||||
|
scroll = function(self, n)
|
||||||
|
-- TODO: NOPE! store framebuffer and write lines onto other screens
|
||||||
|
self._forEachMonitor(function(mon)
|
||||||
|
mon.p.scroll(n)
|
||||||
|
end)
|
||||||
|
end,
|
||||||
|
|
||||||
|
getCursorPos = function(self) return self._pos.x, self._pos.y end,
|
||||||
|
setCursorPos = function(self, x, y)
|
||||||
|
self._pos.x = x
|
||||||
|
self._pos.y = y
|
||||||
|
-- TODO: move cursor to the correct monitor and hide it from others
|
||||||
|
end,
|
||||||
|
|
||||||
|
setCursorBlink = function(self, state) todo() end,
|
||||||
|
getCursorBlink = function(self) todo() end,
|
||||||
|
|
||||||
|
isColor = function(self) return true end,
|
||||||
|
getSize = function(self)
|
||||||
|
local w, h = 0, 0
|
||||||
|
for ix = 1, self._w do
|
||||||
|
local mon = self._findMonitor(ix, 1)
|
||||||
|
w = w + mon._w
|
||||||
|
end
|
||||||
|
for iy = 1, self._h do
|
||||||
|
local mon = self._findMonitor(1, iy)
|
||||||
|
h = h + mon._h
|
||||||
|
end
|
||||||
|
return w, h
|
||||||
|
end,
|
||||||
|
|
||||||
|
setTextColor = function(self, fg)
|
||||||
|
self._forEachMonitor(function(mon)
|
||||||
|
mon.p.setTextColor(fg)
|
||||||
|
end)
|
||||||
|
self._colorForeground = fg
|
||||||
|
end,
|
||||||
|
getTextColor = function(self) todo() end,
|
||||||
|
|
||||||
|
setBackgroundColor = function(self, bg)
|
||||||
|
self._forEachMonitor(function(mon)
|
||||||
|
mon.p.setBackgroundColor(bg)
|
||||||
|
end)
|
||||||
|
self._colorBackground = bg
|
||||||
|
end,
|
||||||
|
getBackgroundColor = function(self) todo() end,
|
||||||
|
|
||||||
|
setTextScale = function(self, scale)
|
||||||
|
self._scale = scale
|
||||||
|
self._reset()
|
||||||
|
end,
|
||||||
|
getTextScale = function(self)
|
||||||
|
return self._scale
|
||||||
|
end,
|
||||||
|
|
||||||
|
setPaletteColor = function(self, index, color, g, b)
|
||||||
|
expect(1, index, "number")
|
||||||
|
expect(2, color, "number")
|
||||||
|
expect(3, g, "number", "nil")
|
||||||
|
expect(4, b, "number", "nil")
|
||||||
|
if index < 0 or index > 32768 or math.log(index, 2) % 1 ~= 0 then
|
||||||
|
error("index out of range")
|
||||||
|
end
|
||||||
|
|
||||||
|
local r = color
|
||||||
|
if g == nil or b == nil then
|
||||||
|
if color < 0 or color > 0xFFFFFF then
|
||||||
|
error("color out of range")
|
||||||
|
end
|
||||||
|
r = bit.band(0xFF, bit.brshift(color, 16)) / 255
|
||||||
|
g = bit.band(0xFF, bit.brshift(color, 8)) / 255
|
||||||
|
b = bit.band(0xFF, bit.brshift(color, 0)) / 255
|
||||||
|
else
|
||||||
|
if r < 0 or r > 1.0 then error("red channel out of range") end
|
||||||
|
if g < 0 or g > 1.0 then error("green channel out of range") end
|
||||||
|
if b < 0 or b > 1.0 then error("blue channel out of range") end
|
||||||
|
end
|
||||||
|
|
||||||
|
self._palette[index] = bit.bor(
|
||||||
|
bit.blshift(math.floor(r * 255), 16),
|
||||||
|
bit.blshift(math.floor(g * 255), 8),
|
||||||
|
bit.blshift(math.floor(b * 255), 0)
|
||||||
|
)
|
||||||
|
self._forEachMonitor(function(mon)
|
||||||
|
mon.p.setPaletteColor(index, r, g, b)
|
||||||
|
end)
|
||||||
|
end,
|
||||||
|
getPaletteColor = function(self, index) todo() end,
|
||||||
|
|
||||||
|
-- internals
|
||||||
|
|
||||||
|
_colorChar = function(self, v)
|
||||||
|
return string.format("%x", math.floor(math.log(v, 2)))
|
||||||
|
end,
|
||||||
|
|
||||||
|
_reset = function(self)
|
||||||
|
self._w = 1
|
||||||
|
self._h = 1
|
||||||
|
self._forEachMonitor(function(mon, i)
|
||||||
|
mon.p.setTextScale(self._scale)
|
||||||
|
local w, h = mon.p.getSize()
|
||||||
|
self._monitors[i]._w = w
|
||||||
|
self._monitors[i]._h = h
|
||||||
|
if mon.x > self._w then self._w = mon.x end
|
||||||
|
if mon.y > self._h then self._h = mon.y end
|
||||||
|
for id, color in pairs(self._palette) do
|
||||||
|
mon.p.setPaletteColor(id, color)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end,
|
||||||
|
|
||||||
|
_blitpixel = function(self, x, y, c, bg, fg)
|
||||||
|
bg = bg or "0"
|
||||||
|
fg = fg or "f"
|
||||||
|
local mon = self._getMonitorForScreenPos(x, y)
|
||||||
|
mon.p.setCursorPos(((x - 1) % mon._w) + 1, ((y - 1) % mon._h) + 1)
|
||||||
|
mon.p.blit(c, bg, fg)
|
||||||
|
end,
|
||||||
|
|
||||||
|
_findMonitor = function(self, x, y)
|
||||||
|
for i = 1, #self._monitors do
|
||||||
|
local mon = self._monitors[i]
|
||||||
|
if mon.x == x and mon.y == y then
|
||||||
|
return mon, i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
_getMonitorForScreenPos = function(self, x, y)
|
||||||
|
local oy = 1
|
||||||
|
for iy = 1, self._h do
|
||||||
|
local ox = 1
|
||||||
|
for ix = 1, self._w do
|
||||||
|
local mon, i = self._findMonitor(ix, iy)
|
||||||
|
if x >= ox and x < (ox + mon._w) and y >= oy and y < (oy + mon._h) then
|
||||||
|
return mon, i, x - ox, y - oy
|
||||||
|
end
|
||||||
|
ox = ox + mon._w
|
||||||
|
end
|
||||||
|
local mon, i = self._findMonitor(1, iy)
|
||||||
|
oy = oy + mon._h
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
_forEachMonitor = function(self, fun)
|
||||||
|
for i = 1, #self._monitors do
|
||||||
|
fun(self._monitors[i], i)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
local lib = {}
|
||||||
|
function lib.fromFile(conf)
|
||||||
|
local fp = assert(io.open(conf, "r"))
|
||||||
|
local conf = textutils.unserializeJSON(fp:read("a"))
|
||||||
|
fp:close()
|
||||||
|
local monitors = {}
|
||||||
|
for addr, pos in pairs(conf.monitors) do
|
||||||
|
local p = assert(peripheral.wrap(addr))
|
||||||
|
table.insert(monitors, { p = p, x = pos.x, y = pos.y })
|
||||||
|
end
|
||||||
|
return lib.new(monitors, { palette = conf.palette, scale = conf.scale })
|
||||||
|
end
|
||||||
|
function lib.new(monitors, args)
|
||||||
|
args = args or {}
|
||||||
|
local mon = setmetatable({
|
||||||
|
_monitors = monitors,
|
||||||
|
_pos = { x = 1, y = 1 },
|
||||||
|
_blink = false,
|
||||||
|
_colorForeground = colors.white,
|
||||||
|
_colorBackground = colors.black,
|
||||||
|
_scale = args.scale or 1.0,
|
||||||
|
_palette = args.palette or {},
|
||||||
|
}, { __index = BigTerm })
|
||||||
|
for name, method in pairs(BigTerm) do
|
||||||
|
if type(method) == "function" then
|
||||||
|
mon[name] = function(...) return method(mon, ...) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
mon._reset()
|
||||||
|
return mon
|
||||||
|
end
|
||||||
|
|
||||||
|
return lib
|
|
@ -0,0 +1,26 @@
|
||||||
|
local args = { ... }
|
||||||
|
local ccpi = require("ccpi")
|
||||||
|
local bigterm = require("bigterm").fromFile("/bigterm.json")
|
||||||
|
|
||||||
|
local img, err = ccpi.load(args[1])
|
||||||
|
if not img then
|
||||||
|
printError(err)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local ys = {}
|
||||||
|
for y = 1, img.h do table.insert(ys, y) end
|
||||||
|
|
||||||
|
for i = 1, math.floor((#ys) / 2), 2 do
|
||||||
|
local a, b = i, (#ys - 1) - i + 1
|
||||||
|
ys[a], ys[b] = ys[b], ys[a]
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, y in ipairs(ys) do
|
||||||
|
bigterm.setCursorPos(1, y)
|
||||||
|
bigterm.blit(img.lines[y].s, img.lines[y].fg, img.lines[y].bg)
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, 16 do
|
||||||
|
bigterm.setPaletteColor(bit.blshift(1, i - 1), img.palette[i])
|
||||||
|
end
|
|
@ -0,0 +1,47 @@
|
||||||
|
local args = { ... }
|
||||||
|
local ccpi = require("ccpi")
|
||||||
|
local bigterm = require("bigterm")
|
||||||
|
|
||||||
|
local conf_path = "/bigterm.json"
|
||||||
|
if args[1] == "-c" then
|
||||||
|
table.remove(args, 1)
|
||||||
|
conf_path = table.remove(args, 1)
|
||||||
|
end
|
||||||
|
local screen = bigterm.fromFile(conf_path)
|
||||||
|
|
||||||
|
local time = 10
|
||||||
|
if args[1] == "-t" then
|
||||||
|
table.remove(args, 1)
|
||||||
|
time = tonumber(table.remove(args, 1))
|
||||||
|
end
|
||||||
|
|
||||||
|
local files = fs.list(args[1])
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local img, err = ccpi.load(fs.combine(args[1], files[math.random(1, #files)]))
|
||||||
|
if not img then
|
||||||
|
printError(err)
|
||||||
|
return
|
||||||
|
else
|
||||||
|
local ys = {}
|
||||||
|
for y = 1, img.h do table.insert(ys, y) end
|
||||||
|
|
||||||
|
for i = 1, math.floor((#ys) / 2), 2 do
|
||||||
|
local a, b = i, (#ys - 1) - i + 1
|
||||||
|
ys[a], ys[b] = ys[b], ys[a]
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, y in ipairs(ys) do
|
||||||
|
screen.setCursorPos(1, y)
|
||||||
|
screen.blit(img.lines[y].s, img.lines[y].fg, img.lines[y].bg)
|
||||||
|
if (i % 10) == 0 then
|
||||||
|
os.sleep(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, 16 do
|
||||||
|
screen.setPaletteColor(bit.blshift(1, i - 1), img.palette[i])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
os.sleep(time)
|
||||||
|
end
|
|
@ -0,0 +1,412 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from typing import BinaryIO, TextIO
|
||||||
|
from PIL import Image, ImageColor
|
||||||
|
from argparse import ArgumentParser, RawTextHelpFormatter
|
||||||
|
from textwrap import dedent
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
try:
|
||||||
|
PALETTE_ADAPTIVE = Image.Palette.ADAPTIVE
|
||||||
|
except Exception:
|
||||||
|
PALETTE_ADAPTIVE = Image.ADAPTIVE
|
||||||
|
|
||||||
|
|
||||||
|
class Converter:
|
||||||
|
CC_COLORS = [
|
||||||
|
("0", "colors.white"),
|
||||||
|
("1", "colors.orange"),
|
||||||
|
("2", "colors.magenta"),
|
||||||
|
("3", "colors.lightBlue"),
|
||||||
|
("4", "colors.yellow"),
|
||||||
|
("5", "colors.lime"),
|
||||||
|
("6", "colors.pink"),
|
||||||
|
("7", "colors.gray"),
|
||||||
|
("8", "colors.lightGray"),
|
||||||
|
("9", "colors.cyan"),
|
||||||
|
("a", "colors.purple"),
|
||||||
|
("b", "colors.blue"),
|
||||||
|
("c", "colors.brown"),
|
||||||
|
("d", "colors.green"),
|
||||||
|
("e", "colors.red"),
|
||||||
|
("f", "colors.black"),
|
||||||
|
]
|
||||||
|
|
||||||
|
DEFAULT_PALETTE = [
|
||||||
|
240, 240, 240,
|
||||||
|
242, 178, 51,
|
||||||
|
229, 127, 216,
|
||||||
|
153, 178, 242,
|
||||||
|
222, 222, 108,
|
||||||
|
127, 204, 25,
|
||||||
|
242, 178, 204,
|
||||||
|
76, 76, 76,
|
||||||
|
153, 153, 153,
|
||||||
|
76, 153, 178,
|
||||||
|
178, 102, 229,
|
||||||
|
51, 102, 204,
|
||||||
|
127, 102, 76,
|
||||||
|
87, 166, 78,
|
||||||
|
204, 76, 76,
|
||||||
|
17, 17, 17
|
||||||
|
]
|
||||||
|
|
||||||
|
DEFAULT_GRAYSCALE_PALETTE = [
|
||||||
|
0xf0, 0xf0, 0xf0,
|
||||||
|
0x9d, 0x9d, 0x9d,
|
||||||
|
0xbe, 0xbe, 0xbe,
|
||||||
|
0xbf, 0xbf, 0xbf,
|
||||||
|
0xb8, 0xb8, 0xb8,
|
||||||
|
0x76, 0x76, 0x76,
|
||||||
|
0xd0, 0xd0, 0xd0,
|
||||||
|
0x4c, 0x4c, 0x4c,
|
||||||
|
0x99, 0x99, 0x99,
|
||||||
|
0x87, 0x87, 0x87,
|
||||||
|
0xa9, 0xa9, 0xa9,
|
||||||
|
0x77, 0x77, 0x77,
|
||||||
|
0x65, 0x65, 0x65,
|
||||||
|
0x6e, 0x6e, 0x6e,
|
||||||
|
0x76, 0x76, 0x76,
|
||||||
|
0x11, 0x11, 0x11
|
||||||
|
]
|
||||||
|
|
||||||
|
PIX_BITS = [[1, 2], [4, 8], [16, 0]]
|
||||||
|
|
||||||
|
MAX_DIFF = 3 * 255
|
||||||
|
|
||||||
|
def __init__(self, image: Image.Image, palette: list[int] | int = PALETTE_ADAPTIVE, dither: bool = True):
|
||||||
|
dither_mode = Image.Dither.FLOYDSTEINBERG if dither else Image.Dither.NONE
|
||||||
|
if isinstance(palette, list):
|
||||||
|
img_pal = Image.new("P", (1, 1))
|
||||||
|
img_pal.putpalette(palette)
|
||||||
|
self._img = image.quantize(len(palette) // 3, palette=img_pal, dither=dither_mode)
|
||||||
|
else:
|
||||||
|
self._img = image.convert("P", palette=palette, colors=16, dither=dither_mode)
|
||||||
|
|
||||||
|
self._imgdata = self._img.load()
|
||||||
|
self._palette: list[int] = self._img.getpalette() or []
|
||||||
|
if len(self._palette) < 16 * 3:
|
||||||
|
self._palette += [0] * ((16 * 3) - len(self._palette))
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def _brightness(self, i: int) -> float:
|
||||||
|
r, g, b = self._palette[i * 3 : (i + 1) * 3]
|
||||||
|
return (r + g + b) / 768
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def _distance(self, a: int, b: int) -> float:
|
||||||
|
r1, g1, b1 = self._palette[a * 3 : (a + 1) * 3]
|
||||||
|
r2, g2, b2 = self._palette[b * 3 : (b + 1) * 3]
|
||||||
|
rd, gd, bd = r1 - r2, g1 - g2, b1 - b2
|
||||||
|
return (rd * rd + gd * gd + bd * bd) / self.MAX_DIFF
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def _get_colors(self, x: int, y: int) -> tuple[int, int]:
|
||||||
|
brightest_i, brightest_l = 0, 0
|
||||||
|
darkest_i, darkest_l = 0, 768
|
||||||
|
for oy, line in enumerate(self.PIX_BITS):
|
||||||
|
for ox in range(len(line)):
|
||||||
|
pix = self._imgdata[x + ox, y + oy]
|
||||||
|
assert pix < 16, f"{pix} is too big at {x+ox}:{y+oy}"
|
||||||
|
brightness = self._brightness(pix)
|
||||||
|
if brightness > brightest_l:
|
||||||
|
brightest_l, brightest_i = brightness, pix
|
||||||
|
if brightness < darkest_l:
|
||||||
|
darkest_l, darkest_i = brightness, pix
|
||||||
|
return darkest_i, brightest_i
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
|
def _is_darker(self, bg: int, fg: int, c: int) -> bool:
|
||||||
|
return self._distance(bg, c) < self._distance(fg, c)
|
||||||
|
|
||||||
|
def _get_block(self, x: int, y: int) -> tuple[int, int, int]:
|
||||||
|
dark_i, bri_i = self._get_colors(x, y)
|
||||||
|
assert dark_i < 16, f"{dark_i} is too big"
|
||||||
|
assert bri_i < 16, f"{bri_i} is too big"
|
||||||
|
out: int = 0
|
||||||
|
for oy, line in enumerate(self.PIX_BITS):
|
||||||
|
for ox, bit in enumerate(line):
|
||||||
|
if not self._is_darker(
|
||||||
|
dark_i, bri_i, self._imgdata[x + ox, y + oy]
|
||||||
|
):
|
||||||
|
out |= bit
|
||||||
|
# bottom right pixel fix?
|
||||||
|
if not self._is_darker(dark_i, bri_i, self._imgdata[x + 1, y + 2]):
|
||||||
|
out ^= 31
|
||||||
|
dark_i, bri_i = bri_i, dark_i
|
||||||
|
return out, dark_i, bri_i
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _write_varint(fp: BinaryIO, value: int):
|
||||||
|
value &= 0xFFFFFFFF
|
||||||
|
mask: int = 0xFFFFFF80
|
||||||
|
while True:
|
||||||
|
if (value & mask) == 0:
|
||||||
|
fp.write(bytes([value & 0xFF]))
|
||||||
|
return
|
||||||
|
fp.write(bytes([(value & 0x7F) | 0x80]))
|
||||||
|
value >>= 7
|
||||||
|
|
||||||
|
def export_binary(self, io: BinaryIO, version: int = -1):
|
||||||
|
if version == -2:
|
||||||
|
for y in range(0, self._img.height - 2, 3):
|
||||||
|
line: bytearray = bytearray()
|
||||||
|
for x in range(0, self._img.width - 1, 2):
|
||||||
|
ch, bg, fg = self._get_block(x, y)
|
||||||
|
line.extend([(ch + 0x80) & 0xFF, fg << 4 | bg])
|
||||||
|
io.write(line)
|
||||||
|
return
|
||||||
|
if version == -1:
|
||||||
|
if self._img.width <= 255 * 2 and self._img.height < 255 * 3:
|
||||||
|
version = 0
|
||||||
|
else:
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
if version == 0:
|
||||||
|
io.write(b"CCPI") # old format
|
||||||
|
io.write(bytes([self._img.width // 2, self._img.height // 3, 0]))
|
||||||
|
io.write(bytes(self._palette[: 16 * 3]))
|
||||||
|
elif version == 1:
|
||||||
|
io.write(b"CPI\x01") # CPIv1
|
||||||
|
self._write_varint(io, self._img.width // 2)
|
||||||
|
self._write_varint(io, self._img.height // 3)
|
||||||
|
io.write(bytes(self._palette[: 16 * 3]))
|
||||||
|
else:
|
||||||
|
raise ValueError(f"invalid version {version}")
|
||||||
|
|
||||||
|
for y in range(0, self._img.height - 2, 3):
|
||||||
|
line: bytearray = bytearray()
|
||||||
|
for x in range(0, self._img.width - 1, 2):
|
||||||
|
ch, bg, fg = self._get_block(x, y)
|
||||||
|
line.extend([(ch + 0x80) & 0xFF, fg << 4 | bg])
|
||||||
|
io.write(line)
|
||||||
|
|
||||||
|
def export(self, io: TextIO):
|
||||||
|
io.write("local m = peripheral.find('monitor')\n")
|
||||||
|
io.write("m.setTextScale(0.5)\n")
|
||||||
|
io.write(f"-- image: {self._img.width}x{self._img.height}\n")
|
||||||
|
io.write("\n")
|
||||||
|
io.write("-- configuring palette\n")
|
||||||
|
for i in range(16):
|
||||||
|
r, g, b = self._palette[i * 3 : (i + 1) * 3]
|
||||||
|
io.write(
|
||||||
|
f"m.setPaletteColor({self.CC_COLORS[i][1]}, 0x{r:02x}{g:02x}{b:02x})\n"
|
||||||
|
)
|
||||||
|
io.write("\n")
|
||||||
|
io.write("-- writing pixels\n")
|
||||||
|
|
||||||
|
for i, y in enumerate(range(0, self._img.height - 2, 3), 1):
|
||||||
|
s = []
|
||||||
|
bgs = ""
|
||||||
|
fgs = ""
|
||||||
|
io.write(f"m.setCursorPos(1, {i}); ")
|
||||||
|
for x in range(0, self._img.width - 1, 2):
|
||||||
|
ch, bg, fg = self._get_block(x, y)
|
||||||
|
s.append(ch + 0x80)
|
||||||
|
bgs += self.CC_COLORS[bg][0]
|
||||||
|
fgs += self.CC_COLORS[fg][0]
|
||||||
|
io.write(
|
||||||
|
"m.blit(string.char(%s), '%s', '%s')\n"
|
||||||
|
% (str.join(", ", map(str, s)), fgs, bgs)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = ArgumentParser(
|
||||||
|
description="ComputerCraft Palette Image converter",
|
||||||
|
formatter_class=RawTextHelpFormatter,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-t",
|
||||||
|
dest="textmode",
|
||||||
|
action="store_true",
|
||||||
|
help="Output a Lua script instead of binary image",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-D",
|
||||||
|
dest="nodither",
|
||||||
|
action="store_true",
|
||||||
|
help="Disable dithering"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-W",
|
||||||
|
dest="width",
|
||||||
|
default=4 * 8 - 1,
|
||||||
|
type=int,
|
||||||
|
help="Width in characters",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-H",
|
||||||
|
dest="height",
|
||||||
|
default=3 * 6 - 2,
|
||||||
|
type=int,
|
||||||
|
help="Height in characters",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-V",
|
||||||
|
dest="cpi_version",
|
||||||
|
type=int,
|
||||||
|
default=-1,
|
||||||
|
choices=(-2, -1, 0, 1),
|
||||||
|
help=dedent(
|
||||||
|
"""\
|
||||||
|
Force specific CPI version to be used.
|
||||||
|
Only applies to binary format.
|
||||||
|
Valid versions:
|
||||||
|
-V -2 Uses raw format. No headers, default palette.
|
||||||
|
Used for OBCB-CC project.
|
||||||
|
-V -1 Choose any fitting one
|
||||||
|
For images smaller than 255x255, uses CPIv0
|
||||||
|
-V 0 OG CPI, 255x255 maximum, uncompressed
|
||||||
|
-V 1 CPIv1, huge images, uncompressed"""
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p",
|
||||||
|
dest="placement",
|
||||||
|
choices=("center", "cover", "tile", "full", "extend", "fill"),
|
||||||
|
default="full",
|
||||||
|
help=dedent(
|
||||||
|
"""\
|
||||||
|
Image placement mode (same as in hsetroot)
|
||||||
|
-p center Render image centered on screen
|
||||||
|
-p cover Centered on screen, scaled to fill fully
|
||||||
|
-p tile Render image tiles
|
||||||
|
-p full Maximum aspect ratio
|
||||||
|
-p extend Same as "full" but filling borders
|
||||||
|
-p fill Stretch to fill"""
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-P",
|
||||||
|
dest="palette",
|
||||||
|
default="auto",
|
||||||
|
help=dedent(
|
||||||
|
"""\
|
||||||
|
Palette to be used for that conversion.
|
||||||
|
Should be 16 colors or less
|
||||||
|
Valid options are:
|
||||||
|
-P auto Determine palette automatically
|
||||||
|
-P default Use default CC:Tweaked color palette
|
||||||
|
-P defaultgray Use default CC:Tweaked grayscale palette
|
||||||
|
-P "list:#RRGGBB,#RRGGBB,..." Use a set list of colors
|
||||||
|
-P "cpi:path" Load palette from a CCPI file
|
||||||
|
-P "gpl:path" Parse GIMP palette file and use first 16 colors
|
||||||
|
-P "txt:path" Load palette from a list of hex values
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
parser.add_argument("image_path")
|
||||||
|
parser.add_argument("output_path")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
with Image.new("RGB", (args.width * 2, args.height * 3)) as canv:
|
||||||
|
with Image.open(args.image_path).convert("RGB") as img:
|
||||||
|
if args.placement == "fill":
|
||||||
|
canv.paste(img.resize(canv.size), (0, 0))
|
||||||
|
elif args.placement in ("full", "extend", "cover"):
|
||||||
|
aspect = canv.width / img.width
|
||||||
|
if (img.height * aspect > canv.height) != (
|
||||||
|
args.placement == "cover"
|
||||||
|
):
|
||||||
|
aspect = canv.height / img.height
|
||||||
|
new_w, new_h = int(img.width * aspect), int(
|
||||||
|
img.height * aspect
|
||||||
|
)
|
||||||
|
top = int((canv.height - new_h) / 2)
|
||||||
|
left = int((canv.width - new_w) / 2)
|
||||||
|
resized_img = img.resize((new_w, new_h))
|
||||||
|
canv.paste(resized_img, (left, top))
|
||||||
|
if args.placement == "extend":
|
||||||
|
if left > 0:
|
||||||
|
right = left - 1 + new_w
|
||||||
|
w = 1
|
||||||
|
while right + w < canv.width:
|
||||||
|
canv.paste(
|
||||||
|
canv.crop(
|
||||||
|
(left + 1 - w, 0, left + 1, canv.height)
|
||||||
|
),
|
||||||
|
(left + 1 - w * 2, 0),
|
||||||
|
)
|
||||||
|
canv.paste(
|
||||||
|
canv.crop((right, 0, right + w, canv.height)),
|
||||||
|
(right + w, 0),
|
||||||
|
)
|
||||||
|
w *= 2
|
||||||
|
if top > 0:
|
||||||
|
bottom = top - 1 + new_h
|
||||||
|
h = 1
|
||||||
|
while bottom + h < canv.height:
|
||||||
|
canv.paste(
|
||||||
|
canv.crop(
|
||||||
|
(0, top + 1 - h, canv.width, top + 1)
|
||||||
|
),
|
||||||
|
(top + 1 - h * 2, 0),
|
||||||
|
)
|
||||||
|
canv.paste(
|
||||||
|
canv.crop((0, bottom, canv.width, bottom + h)),
|
||||||
|
(0, bottom + h),
|
||||||
|
)
|
||||||
|
h *= 2
|
||||||
|
|
||||||
|
elif args.placement in ("center", "tile"):
|
||||||
|
left = int((canv.width - img.width) / 2)
|
||||||
|
top = int((canv.height - img.height) / 2)
|
||||||
|
if args.placement == "tile":
|
||||||
|
while left > 0:
|
||||||
|
left -= img.width
|
||||||
|
while top > 0:
|
||||||
|
top -= img.height
|
||||||
|
x = left
|
||||||
|
while x < canv.width:
|
||||||
|
y = top
|
||||||
|
while y < canv.height:
|
||||||
|
canv.paste(img, (x, y))
|
||||||
|
y += img.height
|
||||||
|
x += img.width
|
||||||
|
else:
|
||||||
|
canv.paste(img, (left, top))
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
palette = PALETTE_ADAPTIVE
|
||||||
|
if args.cpi_version == -2:
|
||||||
|
args.palette = "default"
|
||||||
|
|
||||||
|
if args.palette == "auto":
|
||||||
|
palette = PALETTE_ADAPTIVE
|
||||||
|
elif args.palette == "default":
|
||||||
|
palette = Converter.DEFAULT_PALETTE
|
||||||
|
elif args.palette == "defaultgray":
|
||||||
|
palette = Converter.DEFAULT_GRAYSCALE_PALETTE
|
||||||
|
elif args.palette.startswith("txt:"):
|
||||||
|
with open(args.palette[4:], "r") as fp:
|
||||||
|
palette = []
|
||||||
|
for line in fp:
|
||||||
|
palette += ImageColor.getcolor(line.strip(), "RGB") # type: ignore
|
||||||
|
assert len(palette) <= 16 * 3
|
||||||
|
elif args.palette.startswith("list:"):
|
||||||
|
palette = []
|
||||||
|
for c in args.palette[5:].split(","):
|
||||||
|
palette += ImageColor.getcolor(c, "RGB") # type: ignore
|
||||||
|
assert len(palette) <= 16 * 3
|
||||||
|
elif args.palette.startswith("cpi:"):
|
||||||
|
raise ValueError("not implemented yet")
|
||||||
|
elif args.palette.startswith("gpl:"):
|
||||||
|
raise ValueError("not implemented yet")
|
||||||
|
else:
|
||||||
|
raise ValueError(f"invalid palette identifier: {args.palette!r}")
|
||||||
|
|
||||||
|
converter = Converter(canv, palette, dither=not args.nodither)
|
||||||
|
converter._img.save("/tmp/_ccpictmp.png")
|
||||||
|
if args.textmode:
|
||||||
|
with open(args.output_path, "w") as fp:
|
||||||
|
converter.export(fp)
|
||||||
|
else:
|
||||||
|
with open(args.output_path, "wb") as fp:
|
||||||
|
converter.export_binary(fp, args.cpi_version)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
|
@ -0,0 +1,129 @@
|
||||||
|
|
||||||
|
local decoders = {}
|
||||||
|
|
||||||
|
local function read_palette_full(palette, fp)
|
||||||
|
for i = 1, 16 do
|
||||||
|
palette[i] = bit.blshift(string.byte(fp.read(1)), 16)
|
||||||
|
palette[i] = bit.bor(palette[i], bit.blshift(string.byte(fp.read(1)), 8))
|
||||||
|
palette[i] = bit.bor(palette[i], string.byte(fp.read(1)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function read_pixeldata_v0(image, fp)
|
||||||
|
for y = 1, image.h do
|
||||||
|
local line = { s = "", bg = "", fg = "" }
|
||||||
|
for x = 1, image.w do
|
||||||
|
local data = fp.read(2)
|
||||||
|
if data == nil or #data == 0 then
|
||||||
|
return nil, string.format("Failed to read character at x=%d y=%d", x, y)
|
||||||
|
end
|
||||||
|
|
||||||
|
line.s = line.s .. data:sub(1, 1)
|
||||||
|
local color = string.byte(data, 2, 2)
|
||||||
|
|
||||||
|
if color == nil then
|
||||||
|
return nil, string.format("Failed to read color data for x=%d y=%d", x, y)
|
||||||
|
end
|
||||||
|
line.bg = line.bg .. string.format("%x", bit.band(0xF, color))
|
||||||
|
line.fg = line.fg .. string.format("%x", bit.band(0xF, bit.brshift(color, 4)))
|
||||||
|
end
|
||||||
|
table.insert(image.lines, line)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function read_varint(fp)
|
||||||
|
local value = 0
|
||||||
|
local current = 0
|
||||||
|
local offset = 0
|
||||||
|
repeat
|
||||||
|
if offset >= 5 then return nil, "varint too long" end
|
||||||
|
current = string.byte(fp.read(1))
|
||||||
|
value = bit.bor(value, bit.blshift(bit.band(current, 0x7f), offset * 7))
|
||||||
|
offset = offset + 1
|
||||||
|
until bit.band(current, 0x80) == 0
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
decoders[0] = function(image, fp)
|
||||||
|
image.w, image.h = string.byte(fp.read(1)), string.byte(fp.read(1))
|
||||||
|
image.scale = 0.5 + string.byte(fp.read(1)) * 5 / 255
|
||||||
|
read_palette_full(image.palette, fp)
|
||||||
|
local success, err = read_pixeldata_v0(image, fp)
|
||||||
|
if not success then return false, err end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
decoders[1] = function(image, fp)
|
||||||
|
image.w = read_varint(fp)
|
||||||
|
image.h = read_varint(fp)
|
||||||
|
image.scale = 0.5 -- CPIv1 doesn't have a scale property
|
||||||
|
read_palette_full(image.palette, fp)
|
||||||
|
local success, err = read_pixeldata_v0(image, fp)
|
||||||
|
if not success then return false, err end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parse(fp)
|
||||||
|
local res
|
||||||
|
local image = { w = 0, h = 0, scale = 1.0, palette = {}, lines = {} }
|
||||||
|
|
||||||
|
local magic = fp.read(4)
|
||||||
|
if magic == "CCPI" then
|
||||||
|
res, err = decoders[0](image, fp)
|
||||||
|
elseif magic:sub(1, 3) == "CPI" then
|
||||||
|
local version = magic:byte(4, 4)
|
||||||
|
if decoders[version] == nil then
|
||||||
|
return nil, string.format("Invalid CPI version 0x%02x", version)
|
||||||
|
end
|
||||||
|
res, err = decoders[version](image, fp)
|
||||||
|
else
|
||||||
|
return nil, "Invalid header: expected CCPI got " .. magic
|
||||||
|
end
|
||||||
|
|
||||||
|
if not res then return false, err end
|
||||||
|
return image
|
||||||
|
end
|
||||||
|
|
||||||
|
local function load(path)
|
||||||
|
local fp, err = io.open(path, "rb")
|
||||||
|
if not fp then return nil, err end
|
||||||
|
local img
|
||||||
|
img, err = parse(fp._handle)
|
||||||
|
fp:close()
|
||||||
|
return img, err
|
||||||
|
end
|
||||||
|
|
||||||
|
local function draw(img, ox, oy, monitor)
|
||||||
|
-- todo: add expect()
|
||||||
|
local t = monitor or term.current()
|
||||||
|
ox = ox or 1
|
||||||
|
oy = oy or 1
|
||||||
|
|
||||||
|
if not t.setPaletteColor then
|
||||||
|
return nil, "setPaletteColor is not supported on this term"
|
||||||
|
end
|
||||||
|
|
||||||
|
if not t.setTextScale and img.scale ~= 1 then
|
||||||
|
return nil, "setTextScale is not supported on this term"
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, 16 do
|
||||||
|
t.setPaletteColor(bit.blshift(1, i - 1), img.palette[i])
|
||||||
|
end
|
||||||
|
|
||||||
|
if img.scale ~= 1 then
|
||||||
|
t.setTextScale(img.scale)
|
||||||
|
end
|
||||||
|
|
||||||
|
for y = 1, img.h do
|
||||||
|
t.setCursorPos(ox, oy + y - 1)
|
||||||
|
t.blit(img.lines[y].s, img.lines[y].fg, img.lines[y].bg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
load = load,
|
||||||
|
draw = draw,
|
||||||
|
parse = parse
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
local ccpi = require("ccpi")
|
||||||
|
local args = { ... }
|
||||||
|
|
||||||
|
local terminal = term.current()
|
||||||
|
if args[1] == "-m" then
|
||||||
|
table.remove(args, 1)
|
||||||
|
terminal = peripheral.wrap(table.remove(args, 1))
|
||||||
|
end
|
||||||
|
|
||||||
|
local img, err = ccpi.load(args[1])
|
||||||
|
if not img then
|
||||||
|
printError(err)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
terminal.clear()
|
||||||
|
ccpi.draw(img, 1, 1, terminal)
|
|
@ -0,0 +1,58 @@
|
||||||
|
#!/usr/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
export TMP_DIR="$(mktemp -d)";
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -fr "${TMP_DIR}";
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
|
||||||
|
export INPUT="$1";
|
||||||
|
export OUTPUT="$(realpath "$2")";
|
||||||
|
export BASE_URL="$3"
|
||||||
|
|
||||||
|
if [ -z "${BASE_URL}" ]; then
|
||||||
|
export BASE_URL="CHANGEME";
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
mkdir -p "${OUTPUT}"
|
||||||
|
|
||||||
|
export ORIG="$(pwd)";
|
||||||
|
|
||||||
|
cd "${TMP_DIR}"
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
ffmpeg -i "${INPUT}" -filter_complex "[0:a]channelsplit=channel_layout=stereo[left][right]" -map '[left]' -f s8 -ac 1 -ar 48k "${OUTPUT}/left.s8" -map '[right]' -f s8 -ac 1 -ar 48k "${OUTPUT}/right.s8"
|
||||||
|
ffmpeg -i "${INPUT}" -vf fps=20 frame%04d.png
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
ls frame*.png | parallel 'echo {}; python3 ${ORIG}/cc-pic.py -W 164 -H 81 -p cover {} ${OUTPUT}/{.}.cpi'
|
||||||
|
=======
|
||||||
|
ffmpeg -i $2.* -filter_complex "[0:a]channelsplit=channel_layout=stereo[left][right]" -map '[left]' -f s8 -ac 1 -ar 48k "${ORIG}/${NAME}/left.s8" -map '[right]' -f s8 -ac 1 -ar 48k "${ORIG}/${NAME}/right.s8"
|
||||||
|
=======
|
||||||
|
yt-dlp "${URL}" -S "+height:720" -f "b" -o "${NAME}"
|
||||||
|
|
||||||
|
ffmpeg -i $2* -filter_complex "[0:a]channelsplit=channel_layout=stereo[left][right]" -map '[left]' -f s8 -ac 1 -ar 48k "${ORIG}/${NAME}/left.s8" -map '[right]' -f s8 -ac 1 -ar 48k "${ORIG}/${NAME}/right.s8"
|
||||||
|
>>>>>>> fb01265 (Decrease max video download quality to 720p. Larger downloads were causing errors, slower to process, and unnecessary.)
|
||||||
|
|
||||||
|
ffmpeg -i $2* -vf fps=20 frame%04d.png
|
||||||
|
rm $2*
|
||||||
|
ls frame*.png | parallel 'echo {}; python3 ${ORIG}/cc-pic.py -W 164 -H 81 -p full {} ${ORIG}/${NAME}/{.}.cpi'
|
||||||
|
>>>>>>> 544c8a9 (Swap to -p full.)
|
||||||
|
rm frame*.png
|
||||||
|
|
||||||
|
cd "${ORIG}"
|
||||||
|
|
||||||
|
export FRAME_COUNT="$(ls ${OUTPUT}/*.cpi | wc -l)";
|
||||||
|
|
||||||
|
printf '{"frame_time": 0.05, "frame_count": %d, "video": "%s", "audio": {"l": "%s", "r": "%s"}}\n' "${FRAME_COUNT}" "${BASE_URL}/frame%04d.cpi" "${BASE_URL}/left.s8" "${BASE_URL}/right.s8" > "${OUTPUT}/info.json"
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
#'{"frame_time": 0.05,"frame_count": ${FRAME_COUNT},"video": "${BASE_URL}/${NAME}/frame%04d.cpi","audio": {"l": "${BASE_URL}/${NAME}/left.s8", "r": "${BASE_URL}/${NAME}/right.s8"}}' > "${NAME}/${NAME}.json"
|
||||||
|
printf '{"frame_time": 0.05, "frame_count": %d, "video": "%s", "audio": {"l": "%s", "r": "%s"}}\n' "${FRAME_COUNT}" "${BASE_URL}/${NAME}/frame%04d.cpi" "${BASE_URL}/${NAME}/left.s8" "${BASE_URL}/${NAME}/right.s8" > "${NAME}/${NAME}.json"
|
||||||
|
>>>>>>> 544c8a9 (Swap to -p full.)
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,21 @@
|
||||||
|
local customColors = {
|
||||||
|
[colors.white] = 0xEFEFEF,
|
||||||
|
[colors.orange] = 0xD99F82,
|
||||||
|
[colors.magenta] = 0x464A73,
|
||||||
|
[colors.lightBlue] = 0xA1D5E6,
|
||||||
|
[colors.yellow] = 0xE6CCA1,
|
||||||
|
[colors.lime] = 0x86BF8F,
|
||||||
|
[colors.pink] = 0xC98F8F,
|
||||||
|
[colors.gray] = 0x515151,
|
||||||
|
[colors.lightGray] = 0xA3A3A3,
|
||||||
|
[colors.cyan] = 0xC2F2F2,
|
||||||
|
[colors.blue] = 0x6699CC,
|
||||||
|
[colors.brown] = 0x735F4B,
|
||||||
|
[colors.green] = 0x6DA18A,
|
||||||
|
[colors.red] = 0xBD555F,
|
||||||
|
[colors.black] = 0x131313
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, color in pairs(customColors) do
|
||||||
|
term.setPaletteColor(id, color)
|
||||||
|
end
|
|
@ -0,0 +1,71 @@
|
||||||
|
local ecc = require("ecc")
|
||||||
|
|
||||||
|
string.toHex = function(str)
|
||||||
|
return str:gsub(".", function(ch) return string.format("%02x", ch:byte()) end)
|
||||||
|
end
|
||||||
|
string.fromHex = function(hex)
|
||||||
|
return hex:gsub("%x%x", function(d) return string.char(tonumber(d, 16)) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local keypair = {}
|
||||||
|
if fs.exists("/.id_dh.json") then
|
||||||
|
local fp = io.open("/.id_dh.json", "r")
|
||||||
|
local d = textutils.unserializeJSON(fp:read())
|
||||||
|
keypair.sk = string.fromHex(d.secret)
|
||||||
|
keypair.pk = string.fromHex(d.public)
|
||||||
|
fp:close()
|
||||||
|
else
|
||||||
|
printError("no identity found, generating...")
|
||||||
|
local sk, pk = ecc.keypair(ecc.random.random())
|
||||||
|
io.open("/.id_dh.json", "w"):write(textutils.serializeJSON({
|
||||||
|
secret = string.toHex(sk),
|
||||||
|
public = string.toHex(pk)
|
||||||
|
})):close()
|
||||||
|
keypair.sk = string.char(table.unpack(sk))
|
||||||
|
keypair.pk = string.char(table.unpack(pk))
|
||||||
|
end
|
||||||
|
|
||||||
|
print("pubkey: "..string.toHex(keypair.pk))
|
||||||
|
|
||||||
|
local running = true
|
||||||
|
|
||||||
|
local known_hosts = {}
|
||||||
|
|
||||||
|
parallel.waitForAll(function() while running do -- dh:discover sender
|
||||||
|
rednet.broadcast(keypair.pk, "dh:discover")
|
||||||
|
os.sleep(10)
|
||||||
|
end end,
|
||||||
|
function() while running do -- dh:discover handler
|
||||||
|
local id, pk, proto = rednet.receive("dh:discover")
|
||||||
|
if proto == "dh:discover" then
|
||||||
|
print("DH discover from "..id.." with key "..pk)
|
||||||
|
local nonce = string.toHex(ecc.random.random())
|
||||||
|
local key = ecc.exchange(keypair.sk, pk)
|
||||||
|
known_hosts[id] = {
|
||||||
|
id = id,
|
||||||
|
pk = pk,
|
||||||
|
sk = key,
|
||||||
|
verified = false,
|
||||||
|
nonce = nonce,
|
||||||
|
t = os.clock()
|
||||||
|
}
|
||||||
|
known_hosts[pk] = known_hosts[id]
|
||||||
|
rednet.send(id, {
|
||||||
|
pk = keypair.pk,
|
||||||
|
msg = ecc.encrypt(nonce, key)
|
||||||
|
}, "dh:pair")
|
||||||
|
end
|
||||||
|
end end,
|
||||||
|
function() while running do -- dh:pair handler
|
||||||
|
local id, msg, proto = rednet.receive("dh:pair")
|
||||||
|
if proto == "dh:pair" then
|
||||||
|
local key = ecc.exchange(keypair.sk, msg.pk)
|
||||||
|
known_hosts[id] = {
|
||||||
|
id = id,
|
||||||
|
pk = msg.pk,
|
||||||
|
sk = key,
|
||||||
|
verified = false,
|
||||||
|
nonce = ecc.decrypt(msg.msg, msg.msg)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end end)
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,15 @@
|
||||||
|
local pretty = require("cc.pretty")
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local evd = { os.pullEvent() }
|
||||||
|
local ev, evd = table.remove(evd, 1), evd
|
||||||
|
if ev == "key_up" and evd[1] == keys.q then
|
||||||
|
break
|
||||||
|
elseif ev == "term_resize" then
|
||||||
|
local w, h = term.getSize()
|
||||||
|
print("term_resize", w, h)
|
||||||
|
else
|
||||||
|
io.write(ev.." ")
|
||||||
|
pretty.print(pretty.pretty(evd))
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,75 @@
|
||||||
|
local pretty = require("cc.pretty")
|
||||||
|
|
||||||
|
local args = { ... }
|
||||||
|
|
||||||
|
local instance, user, repo = args[1]:match("https?://([^/]+)/([^/]+)/([^/]+)")
|
||||||
|
|
||||||
|
local function getContents(path)
|
||||||
|
local url = "https://" .. instance .. "/api/v1/repos/" .. user .. "/" .. repo .. "/contents" .. (path and ("/" .. path) or "")
|
||||||
|
local res, err = http.get(url)
|
||||||
|
if not res then
|
||||||
|
printError(err)
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
return textutils.unserializeJSON(res:readAll())
|
||||||
|
end
|
||||||
|
|
||||||
|
local function walkRepository(basedir, callback)
|
||||||
|
local res, err = getContents(basedir)
|
||||||
|
if not res then
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
for _, elem in ipairs(res) do
|
||||||
|
if elem.type == "file" then
|
||||||
|
callback(elem.path, elem)
|
||||||
|
elseif elem.type == "dir" then
|
||||||
|
walkRepository(elem.path, callback)
|
||||||
|
else
|
||||||
|
printError("unknown type: " .. elem.type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function downloadFile(url, path)
|
||||||
|
local fp, err = io.open(path, "wb")
|
||||||
|
if not fp then
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
|
||||||
|
local rq
|
||||||
|
rq, err = http.get(url, nil, true)
|
||||||
|
if not rq then
|
||||||
|
return nil, err
|
||||||
|
end
|
||||||
|
|
||||||
|
local headers = rq.getResponseHeaders()
|
||||||
|
local length = tonumber(headers["Content-Length"]) or 1
|
||||||
|
|
||||||
|
local written = 0
|
||||||
|
local i = 0
|
||||||
|
local _, y = term.getCursorPos()
|
||||||
|
while true do
|
||||||
|
local chunk = rq.read(100)
|
||||||
|
if not chunk then break end
|
||||||
|
fp:write(chunk)
|
||||||
|
written = written + #chunk
|
||||||
|
|
||||||
|
term.setCursorPos(1, y)
|
||||||
|
term.clearLine()
|
||||||
|
|
||||||
|
local w = math.min(25, math.floor(written * 25 / length))
|
||||||
|
term.write("["..string.rep("=", w)..string.rep(" ", 25-w).."] ")
|
||||||
|
term.write(string.format("%7.3f%% %s", 100 * written / length, path))
|
||||||
|
i = i + 1
|
||||||
|
if (i % 20) == 0 then
|
||||||
|
sleep(0.1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
fp:close()
|
||||||
|
print()
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
walkRepository(nil, function(path, file)
|
||||||
|
downloadFile(file.download_url, repo.."-clone/"..path)
|
||||||
|
end)
|
|
@ -0,0 +1,906 @@
|
||||||
|
// x-run: ~/scripts/runc.sh % -Wall -Wextra -lm --- ~/images/boykisser.png cpi-images/boykisser.cpi
|
||||||
|
#define STB_IMAGE_IMPLEMENTATION
|
||||||
|
#include <stb/stb_image.h>
|
||||||
|
#define STB_IMAGE_RESIZE_IMPLEMENTATION
|
||||||
|
#include <stb/stb_image_resize2.h>
|
||||||
|
#include <argp.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <getopt.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <strings.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#define MAX_COLOR_DIFFERENCE 768
|
||||||
|
|
||||||
|
struct rgba { uint8_t r, g, b, a; };
|
||||||
|
union color {
|
||||||
|
struct rgba rgba;
|
||||||
|
uint32_t v;
|
||||||
|
};
|
||||||
|
struct cc_char {
|
||||||
|
unsigned char character;
|
||||||
|
unsigned char bg, fg;
|
||||||
|
};
|
||||||
|
|
||||||
|
const extern char font_atlas[256][11];
|
||||||
|
const extern union color DEFAULT_PALETTE[16], DEFAULT_GRAY_PALETTE[16];
|
||||||
|
|
||||||
|
struct arguments {
|
||||||
|
bool fast_mode;
|
||||||
|
int width, height;
|
||||||
|
enum cpi_version {
|
||||||
|
CPI_VERSION_AUTO,
|
||||||
|
CPI_VERSION_RAW,
|
||||||
|
CPI_VERSION_0,
|
||||||
|
CPI_VERSION_1,
|
||||||
|
CPI_VERSION_2,
|
||||||
|
} cpi_version;
|
||||||
|
enum placement {
|
||||||
|
PLACEMENT_CENTER,
|
||||||
|
PLACEMENT_COVER,
|
||||||
|
PLACEMENT_TILE,
|
||||||
|
PLACEMENT_FULL,
|
||||||
|
PLACEMENT_EXTEND,
|
||||||
|
PLACEMENT_FILL
|
||||||
|
} placement;
|
||||||
|
enum palette_type {
|
||||||
|
PALETTE_DEFAULT,
|
||||||
|
PALETTE_DEFAULT_GRAY,
|
||||||
|
PALETTE_AUTO,
|
||||||
|
PALETTE_PATH,
|
||||||
|
PALETTE_LIST
|
||||||
|
} palette_type;
|
||||||
|
char *palette;
|
||||||
|
char *input_path;
|
||||||
|
char *output_path;
|
||||||
|
} args = {
|
||||||
|
.fast_mode = false,
|
||||||
|
.width = 4 * 8 - 1, // 4x3 blocks screen
|
||||||
|
.height = 3 * 6 - 2,
|
||||||
|
.cpi_version = CPI_VERSION_AUTO,
|
||||||
|
.placement = PLACEMENT_FULL,
|
||||||
|
.input_path = NULL,
|
||||||
|
.output_path = NULL,
|
||||||
|
.palette = NULL,
|
||||||
|
.palette_type = PALETTE_DEFAULT // TODO(kc): change to PALETTE_AUTO when
|
||||||
|
// k-means is implemented
|
||||||
|
};
|
||||||
|
|
||||||
|
struct image {
|
||||||
|
int w, h;
|
||||||
|
union color *pixels;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct image_pal {
|
||||||
|
int w, h;
|
||||||
|
uint8_t *pixels;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool parse_cmdline(int argc, char **argv);
|
||||||
|
void show_help(const char *progname, bool show_all, FILE *fp);
|
||||||
|
struct image *image_load(const char *fp);
|
||||||
|
struct image *image_new(int w, int h);
|
||||||
|
struct image *image_resize(struct image *original, int new_w, int new_h);
|
||||||
|
struct image_pal *image_quantize(struct image *original, const union color *colors, size_t n_colors);
|
||||||
|
float get_color_difference(union color a, union color b);
|
||||||
|
float get_color_brightness(union color clr);
|
||||||
|
void image_unload(struct image *img);
|
||||||
|
void get_size_keep_aspect(int w, int h, int dw, int dh, int *ow, int *oh);
|
||||||
|
|
||||||
|
const char *known_file_extensions[] = {
|
||||||
|
".png", ".jpg", ".jpeg", ".jfif", ".jpg", ".gif",
|
||||||
|
".tga", ".bmp", ".hdr", ".pnm", 0
|
||||||
|
};
|
||||||
|
|
||||||
|
static const struct optiondocs {
|
||||||
|
char shortopt;
|
||||||
|
char *longopt;
|
||||||
|
char *target;
|
||||||
|
char *doc;
|
||||||
|
struct optiondocs_choice { char *value; char *doc; } *choices;
|
||||||
|
} optiondocs[] = {
|
||||||
|
{ 'h', "help", 0, "Show help", 0 },
|
||||||
|
{ 'f', "fast", 0, "Use fast (old) method for picking characters and colors", 0 },
|
||||||
|
{ 'W', "width", "width", "Width in characters", 0 },
|
||||||
|
{ 'h', "height", "height", "Height in characters", 0 },
|
||||||
|
{ 'P', "palette", "palette", "Use specific palette.\n"
|
||||||
|
" `auto` uses automatic selection\n"
|
||||||
|
" `default` uses default palette\n"
|
||||||
|
" `defaultgray` uses default grayscale palette\n"
|
||||||
|
" `list:#RRGGBB,#RRGGBB,...` uses hard-coded one\n"
|
||||||
|
" `txt:PATH` reads hex colors from each line in a file\n", 0 },
|
||||||
|
{ 'V', "cpi_version", "version", "Force specific version of CPI",
|
||||||
|
(struct optiondocs_choice[]) {
|
||||||
|
{ "-2,raw", "Use raw format. No headers, no palette, just characters and colors" },
|
||||||
|
{ "-1,auto", "Choose best available" },
|
||||||
|
{ "0", "OG CPI, 255x255, uncompressed" },
|
||||||
|
{ "1", "CPIv1, huge images, uncompressed" },
|
||||||
|
{ "255", "In-dev version, may not work" },
|
||||||
|
{ 0, 0 } } },
|
||||||
|
{ 'p', "placement", "placement", "Image placement mode (same as in hsetroot)",
|
||||||
|
(struct optiondocs_choice[]){
|
||||||
|
{ "center", "Render image centered on the canvas" },
|
||||||
|
{ "cover", "Centered on screen, scaled to fill fully" },
|
||||||
|
{ "tile", "Render image tiled" },
|
||||||
|
{ "full", "Use maximum aspect ratio" },
|
||||||
|
{ "extend", "Same as \"full\", but filling borders" },
|
||||||
|
{ "fill", "Stretch to fill" },
|
||||||
|
{ 0, 0 } } },
|
||||||
|
{ 0, 0, "input.*", "Input file path", 0 },
|
||||||
|
{ 0, 0, "output.cpi", "Output file path", 0 },
|
||||||
|
{ 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (!parse_cmdline(argc, argv)) {
|
||||||
|
show_help(argv[0], false, stderr);
|
||||||
|
fprintf(stderr, "Fatal error occurred, exiting.\n");
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
struct image *src_image = image_load(args.input_path);
|
||||||
|
if (!src_image) {
|
||||||
|
fprintf(stderr, "Error: failed to open the file\n");
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct image *canvas;
|
||||||
|
if (args.fast_mode) {
|
||||||
|
canvas = image_new(args.width * 2, args.height * 3);
|
||||||
|
} else {
|
||||||
|
canvas = image_new(args.width * 8, args.height * 11);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canvas) {
|
||||||
|
fprintf(stderr, "Error: failed to allocate second image buffer\n");
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: load palette, maybe calculate it too? k-means?
|
||||||
|
const union color *palette = DEFAULT_PALETTE;
|
||||||
|
switch (args.palette_type) {
|
||||||
|
case PALETTE_DEFAULT: palette = DEFAULT_PALETTE; break;
|
||||||
|
case PALETTE_DEFAULT_GRAY: palette = DEFAULT_GRAY_PALETTE; break;
|
||||||
|
case PALETTE_AUTO: assert(0 && "Not implemented"); break;
|
||||||
|
case PALETTE_LIST: assert(0 && "Not implemented"); break;
|
||||||
|
case PALETTE_PATH: assert(0 && "Not implemented"); break;
|
||||||
|
default: assert(0 && "Unreachable");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: properly scale
|
||||||
|
struct image *scaled_image;
|
||||||
|
{
|
||||||
|
int new_w, new_h;
|
||||||
|
get_size_keep_aspect(src_image->w, src_image->h, canvas->w, canvas->h, &new_w, &new_h);
|
||||||
|
|
||||||
|
scaled_image = image_resize(src_image, new_w, new_h);
|
||||||
|
if (!scaled_image) {
|
||||||
|
fprintf(stderr, "Error: failed to open the file\n");
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: position image properly
|
||||||
|
int small_w = scaled_image->w < canvas->w ? scaled_image->w : canvas->w;
|
||||||
|
int small_h = scaled_image->h < canvas->h ? scaled_image->h : canvas->h;
|
||||||
|
for (int y = 0; y < small_h; y++) {
|
||||||
|
memcpy(&canvas->pixels[y * canvas->w],
|
||||||
|
&scaled_image->pixels[y * scaled_image->w],
|
||||||
|
small_w * sizeof(union color));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: actually do stuff
|
||||||
|
struct cc_char *characters = calloc(args.width * args.height, sizeof(struct cc_char));
|
||||||
|
|
||||||
|
struct image_pal *quantized_image = image_quantize(canvas, palette, 16);
|
||||||
|
if (!quantized_image) {
|
||||||
|
fprintf(stderr, "Error: failed to open the file\n");
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE *tmp = fopen("/tmp/img.raw", "wb");
|
||||||
|
for (int i = 0; i < quantized_image->w * quantized_image->h; i++) {
|
||||||
|
union color pix = palette[quantized_image->pixels[i]];
|
||||||
|
fputc(pix.rgba.r, tmp);
|
||||||
|
fputc(pix.rgba.g, tmp);
|
||||||
|
fputc(pix.rgba.b, tmp);
|
||||||
|
}
|
||||||
|
fclose(tmp);
|
||||||
|
|
||||||
|
if (args.fast_mode) {
|
||||||
|
// use old 2x3
|
||||||
|
for (int y = 0; y < args.height; y++) {
|
||||||
|
for (int x = 0; x < args.width; x++) {
|
||||||
|
unsigned char darkest_i = 0, brightest_i = 0;
|
||||||
|
float darkest_diff = 0xffffff, brightest_diff = 0;
|
||||||
|
|
||||||
|
for (int oy = 0; oy < 3; oy++) {
|
||||||
|
for (int ox = 0; ox < 2; ox++) {
|
||||||
|
unsigned char pix = quantized_image->pixels[ox + (x + (y * 3 + oy) * args.width) * 2];
|
||||||
|
float brightness = get_color_brightness(palette[pix]);
|
||||||
|
if (brightness >= brightest_diff) {
|
||||||
|
brightest_i = pix;
|
||||||
|
brightest_diff = brightness;
|
||||||
|
}
|
||||||
|
if (brightness <= darkest_diff) {
|
||||||
|
darkest_i = pix;
|
||||||
|
darkest_diff = brightness;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char bitmap = 0;
|
||||||
|
const static unsigned char pixel_bits[3][2] = { { 1, 2}, { 4, 8 }, { 16, 0 } };
|
||||||
|
for (int oy = 0; oy < 3; oy++) {
|
||||||
|
for (int ox = 0; ox < 2; ox++) {
|
||||||
|
if (ox == 1 && oy == 2) continue;
|
||||||
|
unsigned char pix = quantized_image->pixels[ox + (x + (y * 3 + oy) * args.width) * 2];
|
||||||
|
float diff_bg = get_color_difference(palette[darkest_i], palette[pix]);
|
||||||
|
float diff_fg = get_color_difference(palette[brightest_i], palette[pix]);
|
||||||
|
if (diff_fg < diff_bg) {
|
||||||
|
bitmap |= pixel_bits[oy][ox];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
unsigned char pix = quantized_image->pixels[1 + (x + (y * 3 + 2) * args.width) * 2];
|
||||||
|
float diff_bg = get_color_difference(palette[darkest_i], palette[pix]);
|
||||||
|
float diff_fg = get_color_difference(palette[brightest_i], palette[pix]);
|
||||||
|
if (diff_fg < diff_bg) {
|
||||||
|
bitmap ^= 31;
|
||||||
|
unsigned char tmp = darkest_i;
|
||||||
|
darkest_i = brightest_i;
|
||||||
|
brightest_i = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
characters[x + y * args.width].character = 0x80 + bitmap;
|
||||||
|
characters[x + y * args.width].bg = darkest_i;
|
||||||
|
characters[x + y * args.width].fg = brightest_i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// use new 8x11 character matching
|
||||||
|
for (int y = 0; y < args.height; y++) {
|
||||||
|
for (int x = 0; x < args.width; x++) {
|
||||||
|
// Oh boy...
|
||||||
|
float min_diff = 0xffffff;
|
||||||
|
char closest_sym = 0x00, closest_color = 0xae;
|
||||||
|
for (int sym = 0x01; sym <= 0xFF; sym++) {
|
||||||
|
if (sym == '\t' || sym == '\n' || sym == '\r' || sym == '\x0e') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int color = 0x00; color <= 0xff; color++) {
|
||||||
|
union color cell_bg = palette[color & 0xF],
|
||||||
|
cell_fg = palette[color >> 4];
|
||||||
|
float difference = 0;
|
||||||
|
for (int oy = 0; oy < 11; oy++) {
|
||||||
|
unsigned char sym_line = font_atlas[sym][oy];
|
||||||
|
for (int ox = 0; ox < 8; ox++) {
|
||||||
|
bool lit = sym_line & (0x80 >> ox);
|
||||||
|
union color pixel = palette[quantized_image->pixels[
|
||||||
|
ox + (x + (y * 11 + oy) * args.width) * 8
|
||||||
|
]];
|
||||||
|
difference += get_color_difference(pixel, lit ? cell_fg : cell_bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (difference <= min_diff) {
|
||||||
|
min_diff = difference;
|
||||||
|
closest_sym = sym;
|
||||||
|
closest_color = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
characters[x + y * args.width].character = closest_sym;
|
||||||
|
characters[x + y * args.width].bg = closest_color & 0xF;
|
||||||
|
characters[x + y * args.width].fg = closest_color >> 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement something other than CPIv0
|
||||||
|
FILE *fp = fopen(args.output_path, "wb");
|
||||||
|
fwrite("CCPI", 1, 4, fp);
|
||||||
|
fputc(args.width, fp);
|
||||||
|
fputc(args.height, fp);
|
||||||
|
fputc(0x00, fp);
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
fputc(palette[i].rgba.r, fp);
|
||||||
|
fputc(palette[i].rgba.g, fp);
|
||||||
|
fputc(palette[i].rgba.b, fp);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < args.width * args.height; i++) {
|
||||||
|
fputc(characters[i].character, fp);
|
||||||
|
fputc(characters[i].bg | (characters[i].fg << 4), fp);
|
||||||
|
}
|
||||||
|
fclose(fp);
|
||||||
|
|
||||||
|
image_unload(src_image);
|
||||||
|
image_unload(canvas);
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parse_cmdline(int argc, char **argv) {
|
||||||
|
static struct option options[] = {
|
||||||
|
{ "help", no_argument, 0, 'h' },
|
||||||
|
{ "fast", no_argument, 0, 'f' },
|
||||||
|
{ "width", required_argument, 0, 'W' },
|
||||||
|
{ "height", required_argument, 0, 'H' },
|
||||||
|
{ "cpi_version", required_argument, 0, 'V' },
|
||||||
|
{ "placement", required_argument, 0, 'p' },
|
||||||
|
{ "palette", required_argument, 0, 'P' },
|
||||||
|
{ 0, 0, 0, 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
int option_index = 0;
|
||||||
|
int c = getopt_long(argc, argv, "hfW:H:V:p:P:", options, &option_index);
|
||||||
|
if (c == -1) break;
|
||||||
|
if (c == 0) c = options[option_index].val;
|
||||||
|
if (c == '?') break;
|
||||||
|
|
||||||
|
switch (c) {
|
||||||
|
case 'h': // --help
|
||||||
|
show_help(argv[0], true, stdout);
|
||||||
|
exit(EXIT_SUCCESS);
|
||||||
|
break;
|
||||||
|
case 'f': // --fast
|
||||||
|
args.fast_mode = true;
|
||||||
|
if (args.cpi_version != CPI_VERSION_AUTO) {
|
||||||
|
fprintf(stderr, "Warning: text mode ignores version\n");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'W': // --width
|
||||||
|
args.width = atoi(optarg);
|
||||||
|
break;
|
||||||
|
case 'H': // --height
|
||||||
|
args.height = atoi(optarg);
|
||||||
|
break;
|
||||||
|
case 'V': // --cpi_version
|
||||||
|
{
|
||||||
|
if (0 == strcmp(optarg, "auto") || 0 == strcmp(optarg, "-1")) {
|
||||||
|
args.cpi_version = CPI_VERSION_AUTO;
|
||||||
|
} else if (0 == strcmp(optarg, "raw") || 0 == strcmp(optarg, "-2")) {
|
||||||
|
args.cpi_version = CPI_VERSION_RAW;
|
||||||
|
} else if (0 == strcmp(optarg, "0")) {
|
||||||
|
args.cpi_version = CPI_VERSION_0;
|
||||||
|
} else if (0 == strcmp(optarg, "1")) {
|
||||||
|
args.cpi_version = CPI_VERSION_1;
|
||||||
|
} else if (0 == strcmp(optarg, "2")) {
|
||||||
|
args.cpi_version = CPI_VERSION_2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'p': // --placement
|
||||||
|
if (0 == strcmp(optarg, "center")) {
|
||||||
|
args.placement = PLACEMENT_CENTER;
|
||||||
|
} else if (0 == strcmp(optarg, "cover")) {
|
||||||
|
args.placement = PLACEMENT_COVER;
|
||||||
|
} else if (0 == strcmp(optarg, "tile")) {
|
||||||
|
args.placement = PLACEMENT_TILE;
|
||||||
|
} else if (0 == strcmp(optarg, "full")) {
|
||||||
|
args.placement = PLACEMENT_FULL; }
|
||||||
|
else if (0 == strcmp(optarg, "extend")) {
|
||||||
|
args.placement = PLACEMENT_EXTEND;
|
||||||
|
} else if (0 == strcmp(optarg, "fill")) {
|
||||||
|
args.placement = PLACEMENT_FILL;
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "Error: invaild placement %s\n", optarg);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'P': // --palette
|
||||||
|
if (0 == strcmp(optarg, "default")) {
|
||||||
|
args.palette_type = PALETTE_DEFAULT;
|
||||||
|
} else if (0 == strcmp(optarg, "defaultgray")) {
|
||||||
|
args.palette_type = PALETTE_DEFAULT_GRAY;
|
||||||
|
} else if (0 == strcmp(optarg, "auto")) {
|
||||||
|
args.palette_type = PALETTE_AUTO;
|
||||||
|
} else if (0 == strncmp(optarg, "list:", 5)) {
|
||||||
|
args.palette_type = PALETTE_LIST;
|
||||||
|
args.palette = &optarg[5];
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "Error: invaild palette %s\n", optarg);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optind == argc) {
|
||||||
|
fprintf(stderr, "Error: no input file provided\n");
|
||||||
|
return false;
|
||||||
|
} else if (optind + 1 == argc) {
|
||||||
|
fprintf(stderr, "Error: no output file provided\n");
|
||||||
|
return false;
|
||||||
|
} else if ((argc - optind) != 2) {
|
||||||
|
fprintf(stderr, "Error: too many arguments\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
args.input_path = argv[optind];
|
||||||
|
args.output_path = argv[optind + 1];
|
||||||
|
|
||||||
|
const char *extension = strrchr(args.input_path, '.');
|
||||||
|
if (!extension) {
|
||||||
|
fprintf(stderr, "Warning: no file extension, reading may fail!\n");
|
||||||
|
} else {
|
||||||
|
bool known = false;
|
||||||
|
for (int i = 0; known_file_extensions[i] != 0; i++) {
|
||||||
|
if (0 == strcasecmp(known_file_extensions[i], extension)) {
|
||||||
|
known = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!known) {
|
||||||
|
fprintf(stderr, "Warning: unknown file extension %s, reading may fail!\n", extension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void show_help(const char *progname, bool show_all, FILE *fp) {
|
||||||
|
fprintf(fp, "usage: %s", progname);
|
||||||
|
for (int i = 0; optiondocs[i].doc != 0; i++) {
|
||||||
|
struct optiondocs doc = optiondocs[i];
|
||||||
|
fprintf(fp, " [");
|
||||||
|
if (doc.shortopt) fprintf(fp, "-%c", doc.shortopt);
|
||||||
|
if (doc.shortopt && doc.longopt) fprintf(fp, "|");
|
||||||
|
if (doc.longopt) fprintf(fp, "--%s", doc.longopt);
|
||||||
|
if (doc.target) {
|
||||||
|
if (doc.shortopt || doc.longopt) fprintf(fp, " ");
|
||||||
|
fprintf(fp, "%s", doc.target);
|
||||||
|
}
|
||||||
|
fprintf(fp, "]");
|
||||||
|
}
|
||||||
|
fprintf(fp, "\n");
|
||||||
|
|
||||||
|
if (!show_all) return;
|
||||||
|
|
||||||
|
fprintf(fp, "\n");
|
||||||
|
fprintf(fp, "ComputerCraft Palette Image converter\n");
|
||||||
|
fprintf(fp, "\n");
|
||||||
|
fprintf(fp, "positional arguments:\n");
|
||||||
|
for (int i = 0; optiondocs[i].doc != 0; i++) {
|
||||||
|
struct optiondocs doc = optiondocs[i];
|
||||||
|
if (!doc.shortopt && !doc.longopt) {
|
||||||
|
fprintf(fp, " %s\t%s\n", doc.target, doc.doc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fprintf(fp, "\n");
|
||||||
|
fprintf(fp, "options:\n");
|
||||||
|
for (int i = 0; optiondocs[i].doc != 0; i++) {
|
||||||
|
struct optiondocs doc = optiondocs[i];
|
||||||
|
if (!doc.shortopt && !doc.longopt) { continue; }
|
||||||
|
fprintf(fp, " ");
|
||||||
|
int x = 2;
|
||||||
|
if (doc.shortopt) { fprintf(fp, "-%c", doc.shortopt); x += 2; }
|
||||||
|
if (doc.shortopt && doc.longopt) { fprintf(fp, ", "); x += 2; }
|
||||||
|
if (doc.longopt) { fprintf(fp, "--%s", doc.longopt); x += strlen(doc.longopt) + 2; }
|
||||||
|
if (doc.choices) {
|
||||||
|
fprintf(fp, " {");
|
||||||
|
for (int j = 0; doc.choices[j].value != 0; j++) {
|
||||||
|
if (j > 0) { fprintf(fp, ","); x += 1; }
|
||||||
|
fprintf(fp, "%s", doc.choices[j].value);
|
||||||
|
x += strlen(doc.choices[j].value);
|
||||||
|
}
|
||||||
|
fprintf(fp, "}");
|
||||||
|
x += 3;
|
||||||
|
} else if (doc.target) {
|
||||||
|
fprintf(fp, " ");
|
||||||
|
fprintf(fp, "%s", doc.target);
|
||||||
|
x += strlen(doc.target) + 1;
|
||||||
|
}
|
||||||
|
if (x > 24) fprintf(fp, "\n%24c", ' ');
|
||||||
|
else fprintf(fp, "%*c", 24 - x, ' ');
|
||||||
|
|
||||||
|
fprintf(fp, "%s\n", doc.doc);
|
||||||
|
|
||||||
|
if (doc.choices) {
|
||||||
|
for (int j = 0; doc.choices[j].value != 0; j++) {
|
||||||
|
fprintf(fp, "%26c", ' ');
|
||||||
|
if (doc.shortopt) fprintf(fp, "-%c ", doc.shortopt);
|
||||||
|
else if (doc.longopt) fprintf(fp, "--%s", doc.longopt);
|
||||||
|
fprintf(fp, "%-12s %s\n", doc.choices[j].value, doc.choices[j].doc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct image *image_load(const char *fp) {
|
||||||
|
struct image *img = calloc(1, sizeof(struct image));
|
||||||
|
if (!img) return NULL;
|
||||||
|
img->pixels = (union color*)stbi_load(fp, &img->w, &img->h, 0, 4);
|
||||||
|
if (!img->pixels) {
|
||||||
|
free(img);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct image *image_new(int w, int h) {
|
||||||
|
struct image *img = calloc(1, sizeof(struct image));
|
||||||
|
if (!img) return NULL;
|
||||||
|
img->pixels = calloc(h, sizeof(union color) * w);
|
||||||
|
img->w = w;
|
||||||
|
img->h = h;
|
||||||
|
if (!img->pixels) {
|
||||||
|
free(img);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct image *image_resize(struct image *original, int new_w, int new_h) {
|
||||||
|
struct image *resized = image_new(new_w, new_h);
|
||||||
|
if (!resized) return NULL;
|
||||||
|
stbir_resize_uint8_srgb((unsigned char *)original->pixels, original->w, original->h, 0,
|
||||||
|
(unsigned char *)resized->pixels, resized->w, resized->h, 0,
|
||||||
|
STBIR_RGBA);
|
||||||
|
return resized;
|
||||||
|
}
|
||||||
|
|
||||||
|
void image_unload(struct image *img) {
|
||||||
|
free(img->pixels);
|
||||||
|
free(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
void get_size_keep_aspect(int w, int h, int dw, int dh, int *ow, int *oh)
|
||||||
|
{
|
||||||
|
*ow = dw;
|
||||||
|
*oh = dh;
|
||||||
|
float ratio = (float)w / (float)h;
|
||||||
|
float ratio_dst = (float)dw / (float)dh;
|
||||||
|
int tmp_1, tmp_2;
|
||||||
|
if (ratio_dst >= ratio)
|
||||||
|
{
|
||||||
|
tmp_1 = floor(dh * ratio);
|
||||||
|
tmp_2 = ceil(dh * ratio);
|
||||||
|
if (fabsf(ratio - (float)tmp_1 / dh) < fabsf(ratio - (float)tmp_2 / dh))
|
||||||
|
*ow = tmp_1 < 1 ? 1 : tmp_1;
|
||||||
|
else
|
||||||
|
*ow = tmp_2 < 1 ? 1 : tmp_2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tmp_1 = floor(dw / ratio);
|
||||||
|
tmp_2 = ceil(dw / ratio);
|
||||||
|
if (tmp_2 == 0 ||
|
||||||
|
fabs(ratio - (float)dw / tmp_1) < fabs(ratio - (float)dw / tmp_2))
|
||||||
|
(*oh) = tmp_1 < 1 ? 1 : tmp_1;
|
||||||
|
else
|
||||||
|
(*oh) = tmp_2 < 1 ? 1 : tmp_2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct image_pal *image_quantize(struct image *original, const union color *colors, size_t n_colors) {
|
||||||
|
struct image_pal *out = calloc(1, sizeof(struct image_pal));
|
||||||
|
out->w = original->w;
|
||||||
|
out->h = original->h;
|
||||||
|
out->pixels = calloc(original->w, original->h);
|
||||||
|
|
||||||
|
for (int i = 0; i < out->w * out->h; i++) {
|
||||||
|
int closest_color = 0;
|
||||||
|
float closest_distance = 1e20;
|
||||||
|
for (int color = 0; color < n_colors; color++) {
|
||||||
|
float dist = get_color_difference(colors[color], original->pixels[i]);
|
||||||
|
if (dist <= closest_distance) {
|
||||||
|
closest_distance = dist;
|
||||||
|
closest_color = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out->pixels[i] = closest_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
float get_color_difference(union color a, union color b) {
|
||||||
|
int dr = a.rgba.r - b.rgba.r,
|
||||||
|
dg = a.rgba.g - b.rgba.g,
|
||||||
|
db = a.rgba.b - b.rgba.b;
|
||||||
|
return dr * dr + dg * dg + db * db;
|
||||||
|
}
|
||||||
|
|
||||||
|
float get_color_brightness(union color clr) {
|
||||||
|
return get_color_difference(clr, (union color){ .v = 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const union color DEFAULT_PALETTE[16] = {
|
||||||
|
{ 0xf0, 0xf0, 0xf0, 0xff },
|
||||||
|
{ 0xf2, 0xb2, 0x33, 0xff },
|
||||||
|
{ 0xe5, 0x7f, 0xd8, 0xff },
|
||||||
|
{ 0x99, 0xb2, 0xf2, 0xff },
|
||||||
|
{ 0xde, 0xde, 0x6c, 0xff },
|
||||||
|
{ 0x7f, 0xcc, 0x19, 0xff },
|
||||||
|
{ 0xf2, 0xb2, 0xcc, 0xff },
|
||||||
|
{ 0x4c, 0x4c, 0x4c, 0xff },
|
||||||
|
{ 0x99, 0x99, 0x99, 0xff },
|
||||||
|
{ 0x4c, 0x99, 0xb2, 0xff },
|
||||||
|
{ 0xb2, 0x66, 0xe5, 0xff },
|
||||||
|
{ 0x33, 0x66, 0xcc, 0xff },
|
||||||
|
{ 0x7f, 0x66, 0x4c, 0xff },
|
||||||
|
{ 0x57, 0xa6, 0x4e, 0xff },
|
||||||
|
{ 0xcc, 0x4c, 0x4c, 0xff },
|
||||||
|
{ 0x11, 0x11, 0x11, 0xff }
|
||||||
|
}, DEFAULT_GRAY_PALETTE[16] = {
|
||||||
|
{ 0xf0, 0xf0, 0xf0, 0xff },
|
||||||
|
{ 0x9d, 0x9d, 0x9d, 0xff },
|
||||||
|
{ 0xbe, 0xbe, 0xbe, 0xff },
|
||||||
|
{ 0xbf, 0xbf, 0xbf, 0xff },
|
||||||
|
{ 0xb8, 0xb8, 0xb8, 0xff },
|
||||||
|
{ 0x76, 0x76, 0x76, 0xff },
|
||||||
|
{ 0xd0, 0xd0, 0xd0, 0xff },
|
||||||
|
{ 0x4c, 0x4c, 0x4c, 0xff },
|
||||||
|
{ 0x99, 0x99, 0x99, 0xff },
|
||||||
|
{ 0x87, 0x87, 0x87, 0xff },
|
||||||
|
{ 0xa9, 0xa9, 0xa9, 0xff },
|
||||||
|
{ 0x77, 0x77, 0x77, 0xff },
|
||||||
|
{ 0x65, 0x65, 0x65, 0xff },
|
||||||
|
{ 0x6e, 0x6e, 0x6e, 0xff },
|
||||||
|
{ 0x76, 0x76, 0x76, 0xff },
|
||||||
|
{ 0x11, 0x11, 0x11, 0xff }
|
||||||
|
};
|
||||||
|
|
||||||
|
const char font_atlas[256][11] = {
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x44, 0x6c, 0x44, 0x54, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x7c, 0x54, 0x7c, 0x44, 0x6c, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x28, 0x7c, 0x7c, 0x7c, 0x38, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x10, 0x38, 0x7c, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x38, 0x10, 0x7c, 0x7c, 0x10, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x10, 0x38, 0x7c, 0x7c, 0x10, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x18, 0x3c, 0x3c, 0x18, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x7e, 0x7e, 0x66, 0x42, 0x42, 0x66, 0x7e, 0x7e, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x1c, 0x0c, 0x34, 0x48, 0x48, 0x30, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x44, 0x44, 0x44, 0x38, 0x10, 0x38, 0x10, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x3c, 0x24, 0x3c, 0x20, 0x60, 0x60, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x3e, 0x22, 0x3e, 0x22, 0x66, 0x66, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x40, 0x70, 0x7c, 0x70, 0x40, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x04, 0x1c, 0x7c, 0x1c, 0x04, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x38, 0x7c, 0x10, 0x10, 0x7c, 0x38, 0x10, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x24, 0x24, 0x24, 0x24, 0x24, 0x00, 0x24, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x3c, 0x54, 0x54, 0x34, 0x14, 0x14, 0x14, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x3c, 0x60, 0x58, 0x44, 0x34, 0x0c, 0x78, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x38, 0x7c, 0x10, 0x7c, 0x38, 0x10, 0x7c, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x38, 0x7c, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x10, 0x10, 0x10, 0x7c, 0x38, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x10, 0x18, 0x7c, 0x18, 0x10, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x10, 0x30, 0x7c, 0x30, 0x10, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x7c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x24, 0x7e, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x10, 0x10, 0x38, 0x38, 0x7c, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x7c, 0x38, 0x38, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x14, 0x14, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x28, 0x28, 0x7c, 0x28, 0x7c, 0x28, 0x28, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x3c, 0x40, 0x38, 0x04, 0x78, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x44, 0x48, 0x08, 0x10, 0x20, 0x24, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x28, 0x10, 0x34, 0x58, 0x48, 0x34, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x08, 0x08, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x0c, 0x10, 0x20, 0x20, 0x20, 0x10, 0x0c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x30, 0x08, 0x04, 0x04, 0x04, 0x08, 0x30, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x24, 0x18, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x10, 0x10, 0x7c, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x10, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x04, 0x08, 0x08, 0x10, 0x20, 0x20, 0x40, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x44, 0x4c, 0x54, 0x64, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x30, 0x10, 0x10, 0x10, 0x10, 0x7c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x44, 0x04, 0x18, 0x20, 0x44, 0x7c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x44, 0x04, 0x18, 0x04, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x0c, 0x14, 0x24, 0x44, 0x7c, 0x04, 0x04, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x7c, 0x40, 0x78, 0x04, 0x04, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x18, 0x20, 0x40, 0x78, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x7c, 0x44, 0x04, 0x08, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x44, 0x44, 0x38, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x44, 0x44, 0x3c, 0x04, 0x08, 0x30, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x10, 0x10, 0x10, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x04, 0x08, 0x10, 0x20, 0x10, 0x08, 0x04, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x20, 0x10, 0x08, 0x04, 0x08, 0x10, 0x20, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x44, 0x04, 0x08, 0x10, 0x00, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x3c, 0x42, 0x5a, 0x5a, 0x5e, 0x40, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x44, 0x7c, 0x44, 0x44, 0x44, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x78, 0x44, 0x78, 0x44, 0x44, 0x44, 0x78, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x44, 0x40, 0x40, 0x40, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x78, 0x44, 0x44, 0x44, 0x44, 0x44, 0x78, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x7c, 0x40, 0x70, 0x40, 0x40, 0x40, 0x7c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x7c, 0x40, 0x70, 0x40, 0x40, 0x40, 0x40, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x3c, 0x40, 0x4c, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x44, 0x44, 0x7c, 0x44, 0x44, 0x44, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x10, 0x10, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x44, 0x48, 0x70, 0x48, 0x44, 0x44, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x7c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x44, 0x6c, 0x54, 0x44, 0x44, 0x44, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x44, 0x64, 0x54, 0x4c, 0x44, 0x44, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x44, 0x44, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x78, 0x44, 0x78, 0x40, 0x40, 0x40, 0x40, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x44, 0x44, 0x44, 0x44, 0x48, 0x34, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x78, 0x44, 0x78, 0x44, 0x44, 0x44, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x3c, 0x40, 0x38, 0x04, 0x04, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x7c, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x44, 0x44, 0x44, 0x44, 0x28, 0x28, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x44, 0x44, 0x44, 0x44, 0x54, 0x6c, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x44, 0x28, 0x10, 0x28, 0x44, 0x44, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x44, 0x28, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x7c, 0x04, 0x08, 0x10, 0x20, 0x40, 0x7c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x20, 0x20, 0x20, 0x20, 0x20, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x40, 0x20, 0x20, 0x10, 0x08, 0x08, 0x04, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x08, 0x08, 0x08, 0x08, 0x08, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x28, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x38, 0x04, 0x3c, 0x44, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x40, 0x40, 0x58, 0x64, 0x44, 0x44, 0x78, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x38, 0x44, 0x40, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x04, 0x04, 0x34, 0x4c, 0x44, 0x44, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x38, 0x44, 0x7c, 0x40, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x0c, 0x10, 0x3c, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x3c, 0x44, 0x44, 0x3c, 0x04, 0x78, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x40, 0x40, 0x58, 0x64, 0x44, 0x44, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x04, 0x00, 0x04, 0x04, 0x04, 0x44, 0x44, 0x38, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x20, 0x20, 0x24, 0x28, 0x30, 0x28, 0x24, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x08, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x68, 0x54, 0x54, 0x44, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x78, 0x44, 0x44, 0x44, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x38, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x58, 0x64, 0x44, 0x78, 0x40, 0x40, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x34, 0x4c, 0x44, 0x3c, 0x04, 0x04, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x58, 0x64, 0x40, 0x40, 0x40, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x3c, 0x40, 0x38, 0x04, 0x78, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x10, 0x38, 0x10, 0x10, 0x10, 0x08, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x44, 0x44, 0x44, 0x44, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x44, 0x44, 0x44, 0x28, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x44, 0x44, 0x54, 0x54, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x44, 0x28, 0x10, 0x28, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x44, 0x44, 0x44, 0x3c, 0x04, 0x78, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x7c, 0x08, 0x10, 0x20, 0x7c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x0c, 0x10, 0x10, 0x20, 0x10, 0x10, 0x0c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x30, 0x08, 0x08, 0x04, 0x08, 0x08, 0x30, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x32, 0x4c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x24, 0x48, 0x12, 0x24, 0x48, 0x12, 0x24, 0x48, 0x12, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0xf0, 0xf0, 0xf0, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x0f, 0x0f, 0x0f, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0xfe, 0xfe, 0xfe, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0xf0, 0xf0, 0xf0, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x0f, 0x0f, 0x0f, 0x0f, 0xf0, 0xf0, 0xf0, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0xff, 0xff, 0xff, 0xff, 0xf0, 0xf0, 0xf0, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x0f, 0x0f, 0x0f, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0xf0, 0xf0, 0xf0, 0xf0, 0x0f, 0x0f, 0x0f, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0xff, 0xff, 0xff, 0xff, 0x0f, 0x0f, 0x0f, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0xf0, 0xf0, 0xf0, 0xf0, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x0f, 0x0f, 0x0f, 0x0f, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xf0, 0xf0, 0xf0, },
|
||||||
|
{ 0xf0, 0xf0, 0xf0, 0xf0, 0x00, 0x00, 0x00, 0xf0, 0xf0, 0xf0, 0xf0, },
|
||||||
|
{ 0x0f, 0x0f, 0x0f, 0x0f, 0x00, 0x00, 0x00, 0xf0, 0xf0, 0xf0, 0xf0, },
|
||||||
|
{ 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0xf0, 0xf0, 0xf0, 0xf0, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, },
|
||||||
|
{ 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, },
|
||||||
|
{ 0x0f, 0x0f, 0x0f, 0x0f, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, },
|
||||||
|
{ 0xff, 0xff, 0xff, 0xff, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x0f, 0x0f, 0x0f, 0xf0, 0xf0, 0xf0, 0xf0, },
|
||||||
|
{ 0xf0, 0xf0, 0xf0, 0xf0, 0x0f, 0x0f, 0x0f, 0xf0, 0xf0, 0xf0, 0xf0, },
|
||||||
|
{ 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0xf0, 0xf0, 0xf0, 0xf0, },
|
||||||
|
{ 0x7f, 0x7f, 0x7f, 0x7f, 0x0f, 0x0f, 0x0f, 0xf0, 0xf0, 0xf0, 0xf0, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xf0, 0xf0, 0xf0, 0xf0, },
|
||||||
|
{ 0xf0, 0xf0, 0xf0, 0xf0, 0xff, 0xff, 0xff, 0xf0, 0xf0, 0xf0, 0xf0, },
|
||||||
|
{ 0x0f, 0x0f, 0x0f, 0x0f, 0xff, 0xff, 0xff, 0xf0, 0xf0, 0xf0, 0xf0, },
|
||||||
|
{ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0xf0, 0xf0, 0xf0, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x10, 0x38, 0x44, 0x40, 0x44, 0x38, 0x10, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x18, 0x24, 0x20, 0x78, 0x20, 0x20, 0x7c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x44, 0x38, 0x44, 0x44, 0x44, 0x38, 0x44, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x44, 0x28, 0x7c, 0x10, 0x7c, 0x10, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x10, 0x10, 0x00, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x3c, 0x60, 0x58, 0x44, 0x34, 0x0c, 0x78, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x3c, 0x4a, 0x52, 0x52, 0x4a, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x30, 0x08, 0x38, 0x48, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x14, 0x28, 0x50, 0x28, 0x14, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x7c, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x3c, 0x5a, 0x5a, 0x56, 0x42, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x30, 0x48, 0x48, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x10, 0x10, 0x7c, 0x10, 0x10, 0x00, 0x7c, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x40, 0x20, 0x60, 0x40, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x60, 0x20, 0x60, 0x20, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x20, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x44, 0x44, 0x44, 0x44, 0x7a, 0x40, 0x40, 0x00, },
|
||||||
|
{ 0x00, 0x3c, 0x54, 0x54, 0x34, 0x14, 0x14, 0x14, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x10, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x20, 0x60, 0x20, 0x20, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x38, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x50, 0x28, 0x14, 0x28, 0x50, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x44, 0x48, 0x08, 0x10, 0x2c, 0x2c, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x44, 0x48, 0x08, 0x10, 0x24, 0x28, 0x4c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x64, 0x28, 0x68, 0x10, 0x2c, 0x2c, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x00, 0x10, 0x20, 0x40, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x60, 0x00, 0x38, 0x44, 0x7c, 0x44, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x0c, 0x00, 0x38, 0x44, 0x7c, 0x44, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x44, 0x38, 0x44, 0x7c, 0x44, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x28, 0x50, 0x38, 0x44, 0x7c, 0x44, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x28, 0x00, 0x38, 0x44, 0x7c, 0x44, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x00, 0x38, 0x44, 0x7c, 0x44, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x3c, 0x50, 0x50, 0x78, 0x50, 0x50, 0x5c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x44, 0x40, 0x40, 0x44, 0x38, 0x08, 0x10, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x60, 0x00, 0x7c, 0x40, 0x78, 0x40, 0x7c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x0c, 0x00, 0x7c, 0x40, 0x78, 0x40, 0x7c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x44, 0x7c, 0x40, 0x78, 0x40, 0x7c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x28, 0x00, 0x7c, 0x40, 0x78, 0x40, 0x7c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x30, 0x00, 0x38, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x18, 0x00, 0x38, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x28, 0x38, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x28, 0x00, 0x38, 0x10, 0x10, 0x10, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x78, 0x44, 0x44, 0x64, 0x44, 0x44, 0x78, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x14, 0x28, 0x44, 0x64, 0x54, 0x4c, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x60, 0x38, 0x44, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x0c, 0x38, 0x44, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x44, 0x38, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x28, 0x50, 0x38, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x28, 0x38, 0x44, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x44, 0x28, 0x10, 0x28, 0x44, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x44, 0x4c, 0x54, 0x64, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x60, 0x00, 0x44, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x0c, 0x00, 0x44, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x28, 0x00, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x28, 0x00, 0x44, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x0c, 0x00, 0x44, 0x28, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x10, 0x18, 0x14, 0x18, 0x10, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x78, 0x44, 0x58, 0x44, 0x44, 0x44, 0x58, 0x40, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x60, 0x00, 0x38, 0x04, 0x3c, 0x44, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x0c, 0x00, 0x38, 0x04, 0x3c, 0x44, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x44, 0x38, 0x04, 0x3c, 0x44, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x28, 0x50, 0x38, 0x04, 0x3c, 0x44, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x28, 0x00, 0x38, 0x04, 0x3c, 0x44, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x00, 0x38, 0x04, 0x3c, 0x44, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x2c, 0x52, 0x7c, 0x50, 0x2e, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x38, 0x44, 0x40, 0x44, 0x38, 0x08, 0x10, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x60, 0x00, 0x38, 0x44, 0x7c, 0x40, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x0c, 0x00, 0x38, 0x44, 0x7c, 0x40, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x44, 0x38, 0x44, 0x7c, 0x40, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x28, 0x00, 0x38, 0x44, 0x7c, 0x40, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x30, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x18, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x28, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x28, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x08, 0x3c, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x28, 0x50, 0x78, 0x44, 0x44, 0x44, 0x44, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x60, 0x00, 0x38, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x0c, 0x00, 0x38, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x38, 0x44, 0x38, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x28, 0x50, 0x38, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x28, 0x00, 0x38, 0x44, 0x44, 0x44, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x10, 0x00, 0x7c, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x00, 0x00, 0x38, 0x4c, 0x54, 0x64, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x60, 0x00, 0x44, 0x44, 0x44, 0x44, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x0c, 0x00, 0x44, 0x44, 0x44, 0x44, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x10, 0x28, 0x00, 0x44, 0x44, 0x44, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x28, 0x00, 0x44, 0x44, 0x44, 0x44, 0x3c, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x0c, 0x00, 0x44, 0x44, 0x44, 0x3c, 0x04, 0x78, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x30, 0x10, 0x18, 0x14, 0x18, 0x10, 0x38, 0x00, 0x00, 0x00, },
|
||||||
|
{ 0x00, 0x28, 0x00, 0x44, 0x44, 0x44, 0x3c, 0x04, 0x78, 0x00, 0x00, },
|
||||||
|
};
|
|
@ -0,0 +1,43 @@
|
||||||
|
term.clear()
|
||||||
|
term.setCursorPos(1, 1)
|
||||||
|
term.setTextColor(colors.white)
|
||||||
|
|
||||||
|
local tw, th = term.getSize()
|
||||||
|
print(string.format("Terminal: %dx%d", tw, th))
|
||||||
|
local function printScale(c1, c2, p, fmt, ...)
|
||||||
|
local str = string.format(fmt, ...)
|
||||||
|
local w1 = math.ceil(p * tw)
|
||||||
|
local w2 = tw - w1
|
||||||
|
|
||||||
|
local bg = term.getBackgroundColor()
|
||||||
|
term.setBackgroundColor(c1)
|
||||||
|
term.write(str:sub(1, w1))
|
||||||
|
local rem = w1 - #str
|
||||||
|
if rem > 0 then
|
||||||
|
term.write(string.rep(" ", rem))
|
||||||
|
end
|
||||||
|
|
||||||
|
term.setBackgroundColor(c2)
|
||||||
|
term.write(str:sub(w1 + 1, w1 + w2))
|
||||||
|
|
||||||
|
rem = math.min(tw - #str, w2)
|
||||||
|
if rem > 0 then
|
||||||
|
term.write(string.rep(" ", rem))
|
||||||
|
end
|
||||||
|
|
||||||
|
term.setBackgroundColor(bg)
|
||||||
|
print()
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
term.setBackgroundColor(colors.black)
|
||||||
|
local t = 0
|
||||||
|
while true do
|
||||||
|
for i = 1, th do
|
||||||
|
local p = 0.5 + 0.5 * math.sin((i + t) * math.pi / 25)
|
||||||
|
term.setCursorPos(1, i)
|
||||||
|
printScale(colors.red, colors.gray, p, "%7.3f%%", p * 100)
|
||||||
|
end
|
||||||
|
os.sleep(0.05)
|
||||||
|
t = t + 1
|
||||||
|
end
|
|
@ -0,0 +1,42 @@
|
||||||
|
|
||||||
|
local lines = { "owo" }
|
||||||
|
for line in io.lines("cc-stuff/mess/scrolling-text.lua") do
|
||||||
|
table.insert(lines, line)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawScrollingText(x, y, w, time, t)
|
||||||
|
term.setCursorPos(x, y)
|
||||||
|
if #t <= w then
|
||||||
|
term.write(t)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
time = (time % (#t + 5)) + 1
|
||||||
|
term.write((t .. " ::: " .. t):sub(time, time + w - 1))
|
||||||
|
end
|
||||||
|
|
||||||
|
term.clear()
|
||||||
|
term.setCursorPos(1, 1)
|
||||||
|
term.setCursorBlink(false)
|
||||||
|
|
||||||
|
parallel.waitForAny(
|
||||||
|
function()
|
||||||
|
local time = 0
|
||||||
|
while true do
|
||||||
|
local tw, th = term.getSize()
|
||||||
|
for i = 1, th do
|
||||||
|
local txt = lines[((#lines - th + i - 1) % #lines) + 1]
|
||||||
|
drawScrollingText(math.floor(tw / 2) - i, i, i * 2, time, txt)
|
||||||
|
drawScrollingText(math.floor(tw / 2) - i, i, i * 2, time, txt)
|
||||||
|
end
|
||||||
|
time = time + 1
|
||||||
|
os.sleep(0.05)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
function()
|
||||||
|
os.pullEvent("key")
|
||||||
|
end,
|
||||||
|
function()
|
||||||
|
os.sleep(10)
|
||||||
|
end)
|
||||||
|
|
||||||
|
print("exited")
|
|
@ -0,0 +1,637 @@
|
||||||
|
settings.define("mplayer.colors.bg", {
|
||||||
|
description = "Background color in media player",
|
||||||
|
default = 0x131313, -- #131313
|
||||||
|
type = number
|
||||||
|
})
|
||||||
|
|
||||||
|
settings.define("mplayer.colors.fg", {
|
||||||
|
description = "Text color in media player",
|
||||||
|
default = 0xEFEFEF, -- #EFEFEF
|
||||||
|
type = number
|
||||||
|
})
|
||||||
|
|
||||||
|
settings.define("mplayer.colors.cursor", {
|
||||||
|
description = "Color of the cursor",
|
||||||
|
default = 0x8080EF, -- #8080EF
|
||||||
|
type = number
|
||||||
|
})
|
||||||
|
|
||||||
|
settings.define("mplayer.colors.current", {
|
||||||
|
description = "Color of the currently playing song",
|
||||||
|
default = 0xEFEF80, -- #EFEF80
|
||||||
|
type = number
|
||||||
|
})
|
||||||
|
|
||||||
|
settings.define("mplayer.colors.status", {
|
||||||
|
description = "Color of the statusbar",
|
||||||
|
default = 0x80EF80, -- #80EF80
|
||||||
|
type = number
|
||||||
|
})
|
||||||
|
|
||||||
|
local tw, th = term.getSize()
|
||||||
|
|
||||||
|
os.queueEvent("dummy")
|
||||||
|
while tw < 25 or th < 10 do
|
||||||
|
local ev = { os.pullEvent() }
|
||||||
|
term.clear()
|
||||||
|
term.setCursorPos(1, 1)
|
||||||
|
printError("Too small: " .. tw .. "x" .. th .. " < 25x10")
|
||||||
|
printError("Q to exit")
|
||||||
|
printError("Y to ignore")
|
||||||
|
|
||||||
|
local setTextScale = term.current().setTextScale
|
||||||
|
if setTextScale ~= nil then
|
||||||
|
printError("S to try rescaling")
|
||||||
|
end
|
||||||
|
printError("I'll wait while you're adding more")
|
||||||
|
|
||||||
|
if ev[1] == "term_resize" then
|
||||||
|
tw, th = term.getSize()
|
||||||
|
elseif ev[1] == "key" and ev[2] == keys.s and setTextScale ~= nil then
|
||||||
|
setTextScale(0.5)
|
||||||
|
elseif ev[1] == "key" and ev[2] == keys.y then
|
||||||
|
break
|
||||||
|
elseif ev[1] == "key" and ev[2] == keys.q then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local drive = peripheral.find("tape_drive")
|
||||||
|
if not drive then
|
||||||
|
printError("No drive found, starting in dummy mode")
|
||||||
|
local fp = io.open("noita.dfpwm", "rb")
|
||||||
|
if fp == nil then
|
||||||
|
printError("No sample file found, are you running it on a real computer without a tape drive?")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local size = fp:seek("end", 0)
|
||||||
|
fp:seek("set", 0)
|
||||||
|
drive = {
|
||||||
|
_pos = 0,
|
||||||
|
_fp = fp,
|
||||||
|
_state = "STOPPED",
|
||||||
|
isDummy = true,
|
||||||
|
seek = function(howMuch)
|
||||||
|
drive._pos = math.min(drive.getSize(), math.max(0, drive._pos + howMuch))
|
||||||
|
end,
|
||||||
|
getPosition = function()
|
||||||
|
return drive._pos
|
||||||
|
end,
|
||||||
|
read = function(n)
|
||||||
|
local out = { drive._fp:read(n) }
|
||||||
|
drive.seek(n or 1)
|
||||||
|
return table.unpack(out)
|
||||||
|
end,
|
||||||
|
getSize = function()
|
||||||
|
return size
|
||||||
|
end,
|
||||||
|
getState = function()
|
||||||
|
return drive._state
|
||||||
|
end,
|
||||||
|
play = function()
|
||||||
|
drive._state = "PLAYING"
|
||||||
|
end,
|
||||||
|
stop = function()
|
||||||
|
drive._state = "STOPPED"
|
||||||
|
end,
|
||||||
|
isReady = function()
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
getLabel = function()
|
||||||
|
return "Dummy drive tape"
|
||||||
|
end,
|
||||||
|
_tick = function()
|
||||||
|
if drive._state == "PLAYING" then
|
||||||
|
drive.read(600)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
os.sleep(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function time2str(ti)
|
||||||
|
ti = math.floor(ti)
|
||||||
|
local m, s = math.floor(ti / 60), ti % 60
|
||||||
|
return string.format("%02d:%02d", m, s)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function read32()
|
||||||
|
local v = 0
|
||||||
|
for i = 1, 4 do
|
||||||
|
local b = string.byte(drive.read(1), 1)
|
||||||
|
v = bit32.bor(bit32.lshift(v, 8), b)
|
||||||
|
end
|
||||||
|
return v
|
||||||
|
end
|
||||||
|
|
||||||
|
local help = {}
|
||||||
|
|
||||||
|
for line in ([[# Movement:
|
||||||
|
--
|
||||||
|
Up k : Move cursor up
|
||||||
|
Dn j : Move cursor down
|
||||||
|
H : Jump to top
|
||||||
|
L : Jump down
|
||||||
|
PgUp : Page up
|
||||||
|
PgDn : Page down
|
||||||
|
|
||||||
|
Tab : Next screen
|
||||||
|
S+Tab : Prev. screen
|
||||||
|
|
||||||
|
1 F1 : Help screen
|
||||||
|
2 F2 : Songs list
|
||||||
|
3 F3 : Settings
|
||||||
|
|
||||||
|
# Global:
|
||||||
|
--
|
||||||
|
s : Stop, jump to start
|
||||||
|
p : Pause/resume
|
||||||
|
< : Next track
|
||||||
|
> : Previous track
|
||||||
|
f : Seek forward
|
||||||
|
b : Seek backward
|
||||||
|
\x1b - : Lower volume
|
||||||
|
\x1a + : Higher volume
|
||||||
|
|
||||||
|
# List screen:
|
||||||
|
--
|
||||||
|
Enter : Play
|
||||||
|
Ctrl+l : Center
|
||||||
|
l : Jump to current
|
||||||
|
|
||||||
|
]]):gsub("\\x(..)", function(m)
|
||||||
|
return string.char(tonumber(m, 16))
|
||||||
|
end):gmatch("[^\n]+") do
|
||||||
|
table.insert(help, line)
|
||||||
|
end
|
||||||
|
|
||||||
|
local statusText, statusTicks = nil, 0
|
||||||
|
local function setStatus(txt, ticks)
|
||||||
|
statusText, statusTicks = txt, ticks or 10
|
||||||
|
end
|
||||||
|
|
||||||
|
local mplayer = {
|
||||||
|
colors = {
|
||||||
|
bg = colors.black,
|
||||||
|
fg = colors.white,
|
||||||
|
cursor = colors.blue,
|
||||||
|
current = colors.yellow,
|
||||||
|
status = colors.lime
|
||||||
|
},
|
||||||
|
heldKeys = {},
|
||||||
|
screens = {
|
||||||
|
{
|
||||||
|
title = "Help",
|
||||||
|
scroll = 0,
|
||||||
|
render = function(self)
|
||||||
|
for i = 1, th - 3 do
|
||||||
|
local line = help[i + self.screens[1].scroll] or "~"
|
||||||
|
term.setCursorPos(1, i + 1)
|
||||||
|
term.clearLine()
|
||||||
|
if line:sub(1, 1) == "~" then
|
||||||
|
term.setTextColor(self.colors.cursor)
|
||||||
|
elseif line:sub(1, 1) == "#" then
|
||||||
|
term.setTextColor(self.colors.current)
|
||||||
|
elseif line:sub(1, 2) == "--" then
|
||||||
|
term.setTextColor(self.colors.status)
|
||||||
|
term.write(("-"):rep(tw - 2))
|
||||||
|
else
|
||||||
|
term.setTextColor(self.colors.fg)
|
||||||
|
end
|
||||||
|
term.write(line)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
handleKey = function(self, key, repeating)
|
||||||
|
if key == keys.down or key == keys.j then
|
||||||
|
self.screens[1].handleScroll(self, 1)
|
||||||
|
elseif key == keys.up or key == keys.k then
|
||||||
|
self.screens[1].handleScroll(self, -1)
|
||||||
|
elseif key == keys.pageDown then
|
||||||
|
self.screens[1].handleScroll(self, th - 3)
|
||||||
|
elseif key == keys.pageUp then
|
||||||
|
self.screens[1].handleScroll(self, -(th - 3))
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
handleScroll = function(self, direction, x, y)
|
||||||
|
self.screens[1].scroll = math.max(0, math.min(th - 1, self.screens[1].scroll + direction))
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title = "List",
|
||||||
|
scroll = 0,
|
||||||
|
cursor = 1,
|
||||||
|
textScroll = 0,
|
||||||
|
render = function(self)
|
||||||
|
for i = 1, th - 3 do
|
||||||
|
local song = self.songs[i + self.screens[2].scroll]
|
||||||
|
local isCurrent = (i + self.screens[2].scroll) == self.currentSong
|
||||||
|
local isHovered = (i + self.screens[2].scroll) == self.screens[2].cursor
|
||||||
|
term.setCursorPos(1, i + 1)
|
||||||
|
local bg, fg = self.colors.bg, (isCurrent and self.colors.current or self.colors.fg)
|
||||||
|
if isHovered then bg, fg = fg, bg end
|
||||||
|
term.setBackgroundColor(bg)
|
||||||
|
term.setTextColor(fg)
|
||||||
|
term.clearLine()
|
||||||
|
if song then
|
||||||
|
local timeString = " ["..time2str(song.length / 6000) .. "]"
|
||||||
|
local w = tw - #timeString
|
||||||
|
if #song.title <= w then
|
||||||
|
term.write(song.title)
|
||||||
|
else
|
||||||
|
local off = isHovered and ((self.screens[2].textScroll % (#song.title + 5)) + 1) or 1
|
||||||
|
local txt = song.title .. " ::: " .. song.title
|
||||||
|
term.write(txt:sub(off, off + w - 1))
|
||||||
|
end
|
||||||
|
term.setCursorPos(tw - #timeString + 1, i + 1)
|
||||||
|
term.write(timeString)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
handleKey = function(self, key, repeating)
|
||||||
|
local shiftHeld = self.heldKeys[keys.leftShift] ~= nil or self.heldKeys[keys.rightShift] ~= nil
|
||||||
|
local ctrlHeld = self.heldKeys[keys.leftCtrl] ~= nil or self.heldKeys[keys.rightCtrl] ~= nil
|
||||||
|
|
||||||
|
if key == keys.down or key == keys.j then
|
||||||
|
self.screens[2].handleScroll(self, 1, 1, 1)
|
||||||
|
elseif key == keys.up or key == keys.k then
|
||||||
|
self.screens[2].handleScroll(self, -1, 1, 1)
|
||||||
|
elseif key == keys.pageDown then
|
||||||
|
self.screens[2].handleScroll(self, th - 3, 1, 1)
|
||||||
|
elseif key == keys.pageUp then
|
||||||
|
self.screens[2].handleScroll(self, -(th - 3), 1, 1)
|
||||||
|
elseif key == keys.h and shiftHeld then
|
||||||
|
self.screens[2].handleScroll(self, -#self.songs, 1, 1)
|
||||||
|
elseif key == keys.l and shiftHeld then
|
||||||
|
self.screens[2].handleScroll(self, #self.songs, 1, 1)
|
||||||
|
elseif key == keys.l then
|
||||||
|
self.screens[2].cursor = self.currentSong or 1
|
||||||
|
if ctrlHeld then
|
||||||
|
self.screens[2].scroll = self.screens[2].scroll - math.floor((th - 3) / 2)
|
||||||
|
end
|
||||||
|
self.screens[2].handleScroll(self, 0, 1, 1)
|
||||||
|
elseif key == keys.enter then
|
||||||
|
drive.seek(-drive.getSize())
|
||||||
|
drive.seek(self.songs[self.screens[2].cursor].offset)
|
||||||
|
drive.play()
|
||||||
|
self.currentSong = self.screens[2].cursor
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
handleScroll = function(self, direction, x, y)
|
||||||
|
self.screens[2].cursor = math.max(1, math.min(#self.songs, self.screens[2].cursor + direction))
|
||||||
|
if self.screens[2].scroll + 1 > self.screens[2].cursor then
|
||||||
|
self.screens[2].scroll = self.screens[2].cursor - 1
|
||||||
|
end
|
||||||
|
local maxi = self.screens[2].scroll + th - 3
|
||||||
|
if self.screens[2].cursor > maxi then
|
||||||
|
self.screens[2].scroll = self.screens[2].cursor - (th - 3)
|
||||||
|
end
|
||||||
|
if self.screens[2].scroll > #self.songs - (th - 3) then
|
||||||
|
self.screens[2].scroll = #self.songs - (th - 3)
|
||||||
|
end
|
||||||
|
if self.screens[2].scroll < 0 then
|
||||||
|
self.screens[2].scroll = 0
|
||||||
|
end
|
||||||
|
self.screens[2].textScroll = 0
|
||||||
|
end,
|
||||||
|
handleClick = function(self, x, y)
|
||||||
|
local i = self.screens[2].scroll + y - 1
|
||||||
|
if i == self.screens[2].cursor then
|
||||||
|
self.screens[2].handleKey(self, keys.enter, false)
|
||||||
|
elseif i <= #self.songs then
|
||||||
|
self.screens[2].cursor = i
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title = "Settings",
|
||||||
|
scroll = 0,
|
||||||
|
cursor = 1,
|
||||||
|
render = function(self)
|
||||||
|
term.clear()
|
||||||
|
term.write(string.format("opt = %d, scroll = %d", self.screens[3].cursor, self.screens[3].scroll))
|
||||||
|
term.write(" // TODO")
|
||||||
|
end,
|
||||||
|
handleKey = function(self, key, repeating)
|
||||||
|
if key == keys.down or key == keys.j then
|
||||||
|
self.screens[3].handleScroll(self, 1)
|
||||||
|
elseif key == keys.up or key == keys.k then
|
||||||
|
self.screens[3].handleScroll(self, -1)
|
||||||
|
elseif key == keys.pageDown then
|
||||||
|
self.screens[3].handleScroll(self, th - 3)
|
||||||
|
elseif key == keys.pageUp then
|
||||||
|
self.screens[3].handleScroll(self, -(th - 3))
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
handleScroll = function(self, direction, x, y)
|
||||||
|
self.screens[3].cursor = math.max(1, math.min(20, self.screens[3].cursor + direction))
|
||||||
|
if self.screens[3].scroll + 1 > self.screens[3].cursor then
|
||||||
|
self.screens[3].scroll = self.screens[3].cursor - 1
|
||||||
|
end
|
||||||
|
local maxi = self.screens[3].scroll + th - 3
|
||||||
|
if self.screens[3].cursor > maxi then
|
||||||
|
self.screens[3].scroll = self.screens[3].cursor - (th - 3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
},
|
||||||
|
songs = {},
|
||||||
|
currentSong = 0,
|
||||||
|
statusLineScroll = 0,
|
||||||
|
currentScreen = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v in pairs(mplayer.colors) do
|
||||||
|
term.setPaletteColor(v, settings.get("mplayer.colors." .. k))
|
||||||
|
end
|
||||||
|
term.setCursorPos(1, 1)
|
||||||
|
term.setBackgroundColor(mplayer.colors.bg)
|
||||||
|
term.setTextColor(mplayer.colors.fg)
|
||||||
|
term.clear()
|
||||||
|
|
||||||
|
|
||||||
|
local spinner = {
|
||||||
|
"\x81", "\x83", "\x82",
|
||||||
|
"\x8a", "\x88", "\x8c",
|
||||||
|
"\x84", "\x85"
|
||||||
|
}
|
||||||
|
|
||||||
|
parallel.waitForAny(
|
||||||
|
function()
|
||||||
|
while true do
|
||||||
|
-- Current screen
|
||||||
|
term.setCursorPos(1, 2)
|
||||||
|
mplayer.screens[mplayer.currentScreen].render(mplayer)
|
||||||
|
|
||||||
|
-- Top bar
|
||||||
|
term.setCursorPos(1, 1)
|
||||||
|
for i, screen in ipairs(mplayer.screens) do
|
||||||
|
term.setTextColor(mplayer.colors[i == mplayer.currentScreen and "bg" or "fg"])
|
||||||
|
term.setBackgroundColor(mplayer.colors[i == mplayer.currentScreen and "fg" or "bg"])
|
||||||
|
term.write(" "..i..":"..screen.title.." ")
|
||||||
|
end
|
||||||
|
|
||||||
|
term.setBackgroundColor(mplayer.colors.bg)
|
||||||
|
|
||||||
|
local title, time, duration = "Whatever is on the tape", drive.getPosition() / 6000, drive.getSize() / 6000
|
||||||
|
if mplayer.currentSong ~= 0 then
|
||||||
|
local song = mplayer.songs[mplayer.currentSong]
|
||||||
|
time = (drive.getPosition() - song.offset) / 6000
|
||||||
|
duration = song.length / 6000
|
||||||
|
title = song.title
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Progressbar
|
||||||
|
local lw = math.floor(tw * time / duration)
|
||||||
|
term.setCursorPos(1, th - 1)
|
||||||
|
term.setTextColor(mplayer.colors.current)
|
||||||
|
term.clearLine()
|
||||||
|
term.write(string.rep("=", lw))
|
||||||
|
term.write("\x10")
|
||||||
|
term.setTextColor(mplayer.colors.cursor)
|
||||||
|
term.write(string.rep("\xad", tw - lw - 1))
|
||||||
|
|
||||||
|
local timeString = string.format("[%s:%s]", time2str(time), time2str(duration))
|
||||||
|
|
||||||
|
term.setCursorPos(1, th)
|
||||||
|
term.clearLine()
|
||||||
|
if statusTicks > 0 then
|
||||||
|
term.setTextColor(colors.red)
|
||||||
|
term.write(statusText)
|
||||||
|
statusTicks = statusTicks - 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if drive.getState() ~= "STOPPED" then
|
||||||
|
term.setTextColor(mplayer.colors.status)
|
||||||
|
|
||||||
|
if statusTicks <= 0 then
|
||||||
|
local speen = spinner[(math.floor(drive.getPosition() / 3000) % #spinner) + 1]
|
||||||
|
local action = ""
|
||||||
|
if drive.getState() == "PLAYING" then action = "Playing:"
|
||||||
|
elseif drive.getState() == "REWINDING" then action = "Rewinding"
|
||||||
|
elseif drive.getState() == "FORWARDING" then action = "Forwarding"
|
||||||
|
else
|
||||||
|
printError("Unknown drive state: "..tostring(drive.getState()))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
action = speen .. " " .. action
|
||||||
|
|
||||||
|
term.write(action)
|
||||||
|
|
||||||
|
-- Statusline text
|
||||||
|
term.setCursorPos(#action + 2, th)
|
||||||
|
local w = tw - #timeString - #action - 2 -- "Playing: ", spinner plus spacing
|
||||||
|
|
||||||
|
term.setTextColor(mplayer.colors.current)
|
||||||
|
if #title <= w then
|
||||||
|
term.write(title)
|
||||||
|
else
|
||||||
|
local off = (mplayer.statusLineScroll % (#title + 5)) + 1
|
||||||
|
local txt = title .. " ::: " .. title
|
||||||
|
term.write(txt:sub(off, off + w - 1))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if statusTicks <= 0 then
|
||||||
|
term.setTextColor(mplayer.colors.status)
|
||||||
|
term.setCursorPos(tw - #timeString + 1, th)
|
||||||
|
term.write(timeString)
|
||||||
|
end
|
||||||
|
os.sleep(0.1)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
function()
|
||||||
|
local pretty = require("cc.pretty")
|
||||||
|
while true do
|
||||||
|
local _evd = { os.pullEvent() }
|
||||||
|
local ev, evd = table.remove(_evd, 1), _evd
|
||||||
|
if ev == "key" then
|
||||||
|
mplayer.heldKeys[evd[1]] = evd[2]
|
||||||
|
elseif ev == "key_up" then
|
||||||
|
mplayer.heldKeys[evd[1]] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local shiftHeld = mplayer.heldKeys[keys.leftShift] ~= nil or mplayer.heldKeys[keys.rightShift] ~= nil
|
||||||
|
local ctrlHeld = mplayer.heldKeys[keys.leftCtrl] ~= nil or mplayer.heldKeys[keys.rightCtrl] ~= nil
|
||||||
|
|
||||||
|
if ev == "key_up" and evd[1] == keys.q then
|
||||||
|
break
|
||||||
|
elseif ev == "key" and (evd[1] == keys.one or evd[1] == keys.f1) then
|
||||||
|
mplayer.currentScreen = 1
|
||||||
|
elseif ev == "key" and (evd[1] == keys.two or evd[1] == keys.f2) then
|
||||||
|
mplayer.currentScreen = 2
|
||||||
|
elseif ev == "key" and (evd[1] == keys.three or evd[1] == keys.f3) then
|
||||||
|
mplayer.currentScreen = 3
|
||||||
|
elseif ev == "key" and evd[1] == keys.f then
|
||||||
|
drive.seek(3000)
|
||||||
|
elseif ev == "key" and evd[1] == keys.b then
|
||||||
|
drive.seek(-3000)
|
||||||
|
elseif ev == "key" and (evd[1] == keys.comma or evd[1] == keys.period) and shiftHeld then
|
||||||
|
setStatus("Not implemented yet!", 20)
|
||||||
|
elseif ev == "key" and evd[1] == keys.left or evd[1] == keys.right or evd[1] == keys.minus or (evd[1] == keys.equals and shiftHeld) then
|
||||||
|
setStatus("Not implemented yet!", 20)
|
||||||
|
elseif ev == "key" and evd[1] == keys.s then
|
||||||
|
drive.stop()
|
||||||
|
drive.seek(-drive.getSize())
|
||||||
|
drive.seek(6000)
|
||||||
|
elseif ev == "key" and (evd[1] == keys.p or evd[1] == keys.space) then
|
||||||
|
if drive.getState() ~= "STOPPED" then
|
||||||
|
drive.stop()
|
||||||
|
else
|
||||||
|
drive.play()
|
||||||
|
end
|
||||||
|
elseif ev == "key" and evd[1] == keys.tab then
|
||||||
|
local dir = ((mplayer.heldKeys[keys.leftShift] ~= nil) or (mplayer.heldKeys[keys.rightShift] ~= nil)) and -1 or 1
|
||||||
|
mplayer.currentScreen = ((mplayer.currentScreen - 1 + #mplayer.screens + dir)) % #mplayer.screens + 1
|
||||||
|
elseif ev == "key" then
|
||||||
|
mplayer.screens[mplayer.currentScreen].handleKey(mplayer, evd[1], evd[2])
|
||||||
|
elseif ev == "mouse_scroll" then
|
||||||
|
local dir, x, y = table.unpack(evd)
|
||||||
|
mplayer.screens[mplayer.currentScreen].handleScroll(mplayer, dir, x, y)
|
||||||
|
elseif ev == "song_change" then
|
||||||
|
mplayer.statusLineScroll = 0
|
||||||
|
elseif (ev == "mouse_click" or ev == "mouse_drag") and evd[3] == th - 1 then
|
||||||
|
local p = (evd[2] - 1) / tw
|
||||||
|
local song = mplayer.songs[mplayer.currentSong]
|
||||||
|
drive.seek(-drive.getSize())
|
||||||
|
if song ~= nil then
|
||||||
|
drive.seek(song.offset + math.floor(song.length * p))
|
||||||
|
else
|
||||||
|
drive.seek(math.floor(p * drive.getSize()))
|
||||||
|
end
|
||||||
|
elseif (ev == "mouse_click" or ev == "mouse_drag") and evd[3] == 1 then
|
||||||
|
local cx, x = 1, evd[2]
|
||||||
|
for i, screen in ipairs(mplayer.screens) do
|
||||||
|
local caption = " "..i..""..screen.title.." "
|
||||||
|
if x >= cx and x <= (cx + #caption) then
|
||||||
|
mplayer.currentScreen = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
cx = cx + #caption
|
||||||
|
end
|
||||||
|
elseif ev == "mouse_click" or ev == "mouse_drag" then
|
||||||
|
if mplayer.screens[mplayer.currentScreen].handleClick then
|
||||||
|
local butt, x, y = table.unpack(evd)
|
||||||
|
mplayer.screens[mplayer.currentScreen].handleClick(mplayer, x, y)
|
||||||
|
end
|
||||||
|
elseif ev == "term_resize" then
|
||||||
|
tw, th = term.getSize()
|
||||||
|
elseif ev == "tape_removed" then
|
||||||
|
mplayer.songs = {}
|
||||||
|
mplayer.currentSong = 0
|
||||||
|
elseif ev == "tape_inserted" then
|
||||||
|
local seekToAndPlay = nil
|
||||||
|
if drive.getState() == "PLAYING" then
|
||||||
|
seekToAndPlay = drive.getPosition()
|
||||||
|
end
|
||||||
|
|
||||||
|
drive.stop()
|
||||||
|
drive.seek(-drive.getSize())
|
||||||
|
for i = 1, 48 do
|
||||||
|
local offset = read32()
|
||||||
|
local length = read32()
|
||||||
|
local title = drive.read(117)
|
||||||
|
if offset > drive.getSize() or length > drive.getSize() then
|
||||||
|
mplayer.songs = {
|
||||||
|
{
|
||||||
|
title = "NOTE: It's just a regular tape",
|
||||||
|
offset = drive.getSize() - 10,
|
||||||
|
length = 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title = drive.getLabel(),
|
||||||
|
offset = 0,
|
||||||
|
length = drive.getSize() - 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for t = 1, drive.getSize(), 6000 * 60 * 5 do
|
||||||
|
table.insert(mplayer.songs, {
|
||||||
|
title = "Skip to " .. time2str(t / 6000),
|
||||||
|
offset = t,
|
||||||
|
length = 6000 * 60 * 5
|
||||||
|
})
|
||||||
|
end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
title = title:sub(1, title:find("\x00"))
|
||||||
|
if length > 0 and offset > 0 then
|
||||||
|
mplayer.songs[i] = {
|
||||||
|
title = title,
|
||||||
|
offset = offset,
|
||||||
|
length = length
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if seekToAndPlay ~= nil then
|
||||||
|
drive.seek(-drive.getSize())
|
||||||
|
drive.seek(seekToAndPlay)
|
||||||
|
drive.play()
|
||||||
|
end
|
||||||
|
elseif ev ~= "timer" then
|
||||||
|
if drive.isDummy then
|
||||||
|
local m = term.redirect(peripheral.wrap("right"))
|
||||||
|
io.write(ev .. " ")
|
||||||
|
pretty.print(pretty.pretty(evd))
|
||||||
|
term.redirect(m)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
function()
|
||||||
|
while true do
|
||||||
|
mplayer.statusLineScroll = mplayer.statusLineScroll + 1
|
||||||
|
mplayer.screens[2].textScroll = mplayer.screens[2].textScroll + 1
|
||||||
|
os.sleep(0.25)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
function()
|
||||||
|
local oldSong = nil
|
||||||
|
local oldDriveState = nil
|
||||||
|
local oldTapeState = nil
|
||||||
|
while true do
|
||||||
|
mplayer.currentSong = 0
|
||||||
|
if drive._tick then drive._tick() end -- dummy mode
|
||||||
|
|
||||||
|
local tapeState = drive.isReady()
|
||||||
|
if tapeState ~= oldTapeState then
|
||||||
|
os.queueEvent(tapeState and "tape_inserted" or "tape_removed")
|
||||||
|
oldTapeState = tapeState
|
||||||
|
end
|
||||||
|
|
||||||
|
local driveState = drive.getState()
|
||||||
|
if driveState ~= oldDriveState then
|
||||||
|
os.queueEvent("drive_state", driveState)
|
||||||
|
oldDriveState = driveState
|
||||||
|
end
|
||||||
|
|
||||||
|
local pos = drive.getPosition()
|
||||||
|
for i, song in ipairs(mplayer.songs) do
|
||||||
|
if pos >= song.offset and pos < (song.offset + song.length) then
|
||||||
|
mplayer.currentSong = i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if oldSong ~= mplayer.currentSong then
|
||||||
|
os.queueEvent("song_change")
|
||||||
|
oldSong = mplayer.currentSong
|
||||||
|
end
|
||||||
|
|
||||||
|
os.sleep(0.1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
-- cleanup
|
||||||
|
term.clear()
|
||||||
|
term.setCursorPos(1, 1)
|
||||||
|
for i = 1, 16 do
|
||||||
|
local c = math.pow(2, i - 1)
|
||||||
|
term.setPaletteColor(c, term.nativePaletteColor(c))
|
||||||
|
end
|
||||||
|
term.setBackgroundColor(colors.black)
|
||||||
|
term.setTextColor(colors.white)
|
||||||
|
if term.current().setTextScale then
|
||||||
|
term.current().setTextScale(1.0)
|
||||||
|
end
|
||||||
|
print("<tmpc> Goodbye!")
|
|
@ -0,0 +1,62 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
|
||||||
|
colors = {
|
||||||
|
"0": (240, 240, 240),
|
||||||
|
"1": (242, 178, 51),
|
||||||
|
"2": (229, 127, 216),
|
||||||
|
"3": (153, 178, 242),
|
||||||
|
"4": (222, 222, 108),
|
||||||
|
"5": (127, 204, 25),
|
||||||
|
"6": (242, 178, 204),
|
||||||
|
"7": ( 76, 76, 76),
|
||||||
|
"8": (153, 153, 153),
|
||||||
|
"9": ( 76, 153, 178),
|
||||||
|
"a": (178, 102, 229),
|
||||||
|
"b": ( 51, 102, 204),
|
||||||
|
"c": (127, 102, 76),
|
||||||
|
"d": ( 87, 166, 78),
|
||||||
|
"e": (204, 76, 76),
|
||||||
|
"f": ( 17, 17, 17),
|
||||||
|
}
|
||||||
|
|
||||||
|
color_names = {
|
||||||
|
"0": "white",
|
||||||
|
"1": "orange",
|
||||||
|
"2": "magenta",
|
||||||
|
"3": "lightBlue",
|
||||||
|
"4": "yellow",
|
||||||
|
"5": "lime",
|
||||||
|
"6": "pink",
|
||||||
|
"7": "gray",
|
||||||
|
"8": "lightGray",
|
||||||
|
"9": "cyan",
|
||||||
|
"a": "purple",
|
||||||
|
"b": "blue",
|
||||||
|
"c": "brown",
|
||||||
|
"d": "green",
|
||||||
|
"e": "red",
|
||||||
|
"f": "black",
|
||||||
|
}
|
||||||
|
|
||||||
|
parser = ArgumentParser(description="CC default image viewer")
|
||||||
|
parser.add_argument("filename")
|
||||||
|
|
||||||
|
def color(v: str):
|
||||||
|
return tuple(bytes.fromhex(v.lstrip('#')))
|
||||||
|
|
||||||
|
|
||||||
|
for k, (r, g, b) in colors.items():
|
||||||
|
parser.add_argument(f"-{k}", type=color, default=(r, g, b),
|
||||||
|
help="Set color %s" % color_names[k])
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
with open(args.filename, "r") as fp:
|
||||||
|
for line in fp:
|
||||||
|
for char in line.rstrip():
|
||||||
|
if char in colors:
|
||||||
|
print("\x1b[48;2;%d;%d;%dm \x1b[0m" % colors[char], end="")
|
||||||
|
else:
|
||||||
|
print(" ", end="")
|
||||||
|
print()
|
|
@ -0,0 +1,99 @@
|
||||||
|
|
||||||
|
local ws = assert(http.websocket("wss://bitmap-ws.alula.me/"))
|
||||||
|
|
||||||
|
function parse_u16(data)
|
||||||
|
local v = table.remove(data, 1)
|
||||||
|
v = bit.bor(v, bit.blshift(table.remove(data, 1), 8))
|
||||||
|
return v, data
|
||||||
|
end
|
||||||
|
|
||||||
|
function parse_u32(data)
|
||||||
|
local v = table.remove(data, 1)
|
||||||
|
v = bit.bor(v, bit.blshift(table.remove(data, 1), 8))
|
||||||
|
v = bit.bor(v, bit.blshift(table.remove(data, 1), 16))
|
||||||
|
v = bit.bor(v, bit.blshift(table.remove(data, 1), 24))
|
||||||
|
return v, data
|
||||||
|
end
|
||||||
|
|
||||||
|
local chunk_id = 421
|
||||||
|
local chunk_state = { 0x48, 0x65, 0x6c, 0x6c, 0x6f }
|
||||||
|
|
||||||
|
local function send_chunk_request(chunk)
|
||||||
|
ws.send(string.char(0x10, bit.band(chunk, 0xFF), bit.band(bit.brshift(chunk, 8), 0xFF)), true)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function send_chunk_subscribe_request(chunk)
|
||||||
|
ws.send(string.char(0x14, bit.band(chunk, 0xFF), bit.band(bit.brshift(chunk, 8), 0xFF)), true)
|
||||||
|
end
|
||||||
|
|
||||||
|
parallel.waitForAll(function()
|
||||||
|
while true do
|
||||||
|
local data, is_binary = ws.receive()
|
||||||
|
data = { string.byte(data, 1, #data) }
|
||||||
|
local opcode = table.remove(data, 1)
|
||||||
|
if opcode == 0x00 then -- hello
|
||||||
|
ver_major, data = parse_u16(data)
|
||||||
|
ver_minor, data = parse_u16(data)
|
||||||
|
os.queueEvent("obcb:hello", ver_major, ver_minor)
|
||||||
|
elseif opcode == 0x01 then
|
||||||
|
local clients = parse_u32(data)
|
||||||
|
-- TODO: show them somewhere
|
||||||
|
os.queueEvent("obcb:clients", clients)
|
||||||
|
elseif opcode == 0x11 then
|
||||||
|
chunk_index, data = parse_u16(data)
|
||||||
|
if chunk_index == chunk_id then
|
||||||
|
chunk_state = data
|
||||||
|
end
|
||||||
|
os.queueEvent("obcb:update")
|
||||||
|
elseif opcode == 0x12 then
|
||||||
|
offset, chunk = parse_u32(data)
|
||||||
|
local c_id = math.floor(offset / 32768)
|
||||||
|
local b_off = offset % 32768
|
||||||
|
for i = 1, 32 do
|
||||||
|
chunk_state[i + b_off] = chunk[i]
|
||||||
|
end
|
||||||
|
os.queueEvent("obcb:update")
|
||||||
|
else
|
||||||
|
print(table.unpack(data))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
function()
|
||||||
|
local mon = peripheral.wrap("monitor_0")
|
||||||
|
mon.setTextScale(0.5)
|
||||||
|
mon.clear()
|
||||||
|
local tw, th = term.getSize()
|
||||||
|
send_chunk_request(chunk_id)
|
||||||
|
send_chunk_subscribe_request(chunk_id)
|
||||||
|
term.setCursorPos(1, 3)
|
||||||
|
print("Showing chunk " .. chunk_id)
|
||||||
|
print(string.format("Screen: %dx%d", mon.getSize()))
|
||||||
|
while true do
|
||||||
|
local ev = { os.pullEvent() }
|
||||||
|
if ev[1] == "char" and ev[2] == "q" then
|
||||||
|
break
|
||||||
|
elseif ev[1] == "obcb:hello" then
|
||||||
|
term.setCursorPos(1, 1)
|
||||||
|
print(string.format("Hello: obcb v%d.%d", ev[2], ev[3]))
|
||||||
|
elseif ev[1] == "obcb:clients" then
|
||||||
|
term.setCursorPos(1, 2)
|
||||||
|
print("Clients: " .. ev[2])
|
||||||
|
elseif ev[1] == "obcb:update" then
|
||||||
|
for y = 1, 81 do
|
||||||
|
mon.setCursorPos(1, y)
|
||||||
|
local tx = {}
|
||||||
|
local bg = {}
|
||||||
|
local fg = {}
|
||||||
|
for x = 1, 164 do
|
||||||
|
local i = (y - 1) * 164 + (x - 1)
|
||||||
|
tx[x] = chunk_state[i * 2 + 1]
|
||||||
|
fg[x] = string.format("%x", bit.band(chunk_state[i * 2 + 2], 0xF))
|
||||||
|
bg[x] = string.format("%x", bit.band(bit.brshift(chunk_state[i * 2 + 2], 4), 0xF))
|
||||||
|
end
|
||||||
|
mon.blit(string.char(table.unpack(tx)), table.concat(bg), table.concat(fg))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
ws.close()
|
|
@ -0,0 +1,43 @@
|
||||||
|
local args = { ... }
|
||||||
|
|
||||||
|
local modtimes = {}
|
||||||
|
|
||||||
|
if #args == 0 then
|
||||||
|
print("usage: runonchange [files to watch ...] -- program [args...]")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
while #args > 0 do
|
||||||
|
local name = table.remove(args, 1)
|
||||||
|
if name == "--" then break end
|
||||||
|
modtimes[name] = -1
|
||||||
|
end
|
||||||
|
|
||||||
|
if #args == 0 then
|
||||||
|
printError("No executable was given")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local curdir = shell.dir()
|
||||||
|
|
||||||
|
while true do
|
||||||
|
local shall_run = false
|
||||||
|
for name, modtime in pairs(modtimes) do
|
||||||
|
local path = name:sub(1,1) == "/" and name or (curdir .. "/" .. name)
|
||||||
|
local modtime_new = fs.attributes(path).modified
|
||||||
|
if modtime_new ~= modtime then
|
||||||
|
shall_run = true
|
||||||
|
modtimes[name] = modtime_new
|
||||||
|
print(name .. " was modified")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if shall_run then
|
||||||
|
local succ, err = pcall(shell.run, table.unpack(args))
|
||||||
|
if succ then
|
||||||
|
print("Process finished successfully")
|
||||||
|
else
|
||||||
|
printError("Process crashed: " .. err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
os.sleep(0.5)
|
||||||
|
end
|
32
stream.lua
32
stream.lua
|
@ -1,6 +1,6 @@
|
||||||
local args = { ... }
|
local args = { ... }
|
||||||
|
|
||||||
local buffer_size = 8192
|
local dfpwm = require("cc.audio.dfpwm")
|
||||||
|
|
||||||
if not http then
|
if not http then
|
||||||
print("no http, check config")
|
print("no http, check config")
|
||||||
|
@ -21,21 +21,35 @@ if not req then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local buffer = { }
|
local headers = req.getResponseHeaders()
|
||||||
for i = 1, buffer_size do
|
local length = tonumber(headers["Content-Length"]) or 0
|
||||||
buffer[i] = 0
|
|
||||||
|
local function decode_s8(data)
|
||||||
|
local buffer = {}
|
||||||
|
for i = 1, #data do
|
||||||
|
local v = string.byte(data, i)
|
||||||
|
if bit32.band(v, 0x80) then
|
||||||
|
v = bit32.bxor(v, 0x7F) - 128
|
||||||
|
end
|
||||||
|
buffer[i] = v
|
||||||
|
end
|
||||||
|
return buffer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local use_dfpwm = ({ args[1]:find("%.dfpwm") })[2] == #args[1]
|
||||||
|
|
||||||
|
local decode = use_dfpwm and dfpwm.make_decoder() or decode_s8
|
||||||
|
local read_bytes = 0
|
||||||
while true do
|
while true do
|
||||||
local chunk = req.read(buffer_size)
|
local chunk = req.read(16384)
|
||||||
if not chunk then
|
if not chunk then
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
buffer = {}
|
|
||||||
for i = 1, #chunk do
|
local buffer = decode(chunk)
|
||||||
buffer[i] = string.byte(chunk, i) - 128
|
|
||||||
end
|
|
||||||
while not speaker.playAudio(buffer) do
|
while not speaker.playAudio(buffer) do
|
||||||
os.pullEvent("speaker_audio_empty")
|
os.pullEvent("speaker_audio_empty")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
|
@ -0,0 +1,143 @@
|
||||||
|
local drive = peripheral.find("tape_drive")
|
||||||
|
if not drive then
|
||||||
|
printError("no tape drive found")
|
||||||
|
printError("it's kinda required to play tapes, you know?")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local running = true
|
||||||
|
|
||||||
|
term.setBackgroundColor(colors.black)
|
||||||
|
term.clear()
|
||||||
|
local screen_w, screen_h = term.getSize()
|
||||||
|
local table_of_contents = {}
|
||||||
|
|
||||||
|
|
||||||
|
local function read32()
|
||||||
|
local v = 0
|
||||||
|
for i = 1, 4 do
|
||||||
|
local b = drive.read()
|
||||||
|
v = bit32.bor(bit32.lshift(v, 8), b)
|
||||||
|
end
|
||||||
|
return v
|
||||||
|
end
|
||||||
|
|
||||||
|
local function bytes2time(b)
|
||||||
|
local s = math.floor(b / 6000)
|
||||||
|
if s < 60 then return string.format("%ds", s) end
|
||||||
|
return string.format("%dm, %ds", math.floor(s / 60), s % 60)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function textProgress(p, c1, c2, fmt, ...)
|
||||||
|
local tw = term.getSize()
|
||||||
|
local str = string.format(fmt, ...)
|
||||||
|
local w1 = math.ceil(p * tw)
|
||||||
|
local w2 = tw - w1
|
||||||
|
|
||||||
|
local bg = term.getBackgroundColor()
|
||||||
|
|
||||||
|
term.setBackgroundColor(c1)
|
||||||
|
term.write(str:sub(1, w1))
|
||||||
|
local rem = w1 - #str
|
||||||
|
if rem > 0 then
|
||||||
|
term.write(string.rep(" ", rem))
|
||||||
|
end
|
||||||
|
|
||||||
|
term.setBackgroundColor(c2)
|
||||||
|
term.write(str:sub(w1 + 1, w1 + w2))
|
||||||
|
|
||||||
|
rem = math.min(tw - #str, w2)
|
||||||
|
if rem > 0 then
|
||||||
|
term.write(string.rep(" ", rem))
|
||||||
|
end
|
||||||
|
|
||||||
|
term.setBackgroundColor(bg)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
parallel.waitForAll(
|
||||||
|
function()
|
||||||
|
while running do
|
||||||
|
if drive.isReady() then
|
||||||
|
local pos, size = drive.getPosition(), drive.getSize()
|
||||||
|
for i = 1, math.min(screen_h - 2, 48) do
|
||||||
|
term.setCursorPos(1, i)
|
||||||
|
local song = table_of_contents[i]
|
||||||
|
if song then
|
||||||
|
local is_playing = pos >= song.offset and pos < song.ending
|
||||||
|
local s = string.format("#%2d %9s %s", i, bytes2time(song.length), song.title)
|
||||||
|
if is_playing then
|
||||||
|
local p = (pos - song.offset) / song.length
|
||||||
|
textProgress(p, colors.lime, colors.lightGray, s)
|
||||||
|
else
|
||||||
|
term.setBackgroundColor(i % 2 == 0 and colors.gray or colors.black)
|
||||||
|
term.clearLine()
|
||||||
|
term.write(s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
term.setCursorPos(1, screen_h)
|
||||||
|
textProgress(pos / size, colors.red, colors.gray, "%8d / %8d [%s]", pos, size, drive.getState())
|
||||||
|
end
|
||||||
|
os.sleep(0.1)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
function()
|
||||||
|
while running do
|
||||||
|
local evd = { os.pullEvent() }
|
||||||
|
local ev, evd = table.remove(evd, 1), evd
|
||||||
|
|
||||||
|
if ev == "mouse_click" then
|
||||||
|
local x, y = table.unpack(evd, 2)
|
||||||
|
if drive.isReady() and y <= #table_of_contents then
|
||||||
|
drive.seek(-drive.getSize())
|
||||||
|
drive.seek(table_of_contents[y].offset)
|
||||||
|
drive.play()
|
||||||
|
end
|
||||||
|
elseif ev == "term_resize" then
|
||||||
|
term.setBackgroundColor(colors.black)
|
||||||
|
term.clear()
|
||||||
|
screen_w, screen_h = term.getSize()
|
||||||
|
elseif ev == "tape_present" then
|
||||||
|
table_of_contents = {}
|
||||||
|
term.clear()
|
||||||
|
if evd[1] then
|
||||||
|
drive.stop()
|
||||||
|
drive.seek(-drive.getSize())
|
||||||
|
for i = 1, 48 do
|
||||||
|
local offset = read32()
|
||||||
|
local length = read32()
|
||||||
|
local title = drive.read(117):gsub("\x00", "")
|
||||||
|
if length > 0 then
|
||||||
|
table.insert(table_of_contents, {
|
||||||
|
title = title,
|
||||||
|
offset = offset,
|
||||||
|
length = length,
|
||||||
|
ending = offset + length
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
function()
|
||||||
|
local tape_was_present = nil
|
||||||
|
local drive_old_state = nil
|
||||||
|
while running do
|
||||||
|
local tape_present = drive.isReady()
|
||||||
|
if tape_present ~= tape_was_present then
|
||||||
|
os.queueEvent("tape_present", tape_present)
|
||||||
|
tape_was_present = tape_present
|
||||||
|
end
|
||||||
|
|
||||||
|
local drive_state = drive.getState()
|
||||||
|
if drive_old_state ~= drive_state then
|
||||||
|
os.queueEvent("drive_state", drive_state)
|
||||||
|
drive_old_state = drive_state
|
||||||
|
end
|
||||||
|
|
||||||
|
os.sleep(0.25)
|
||||||
|
end
|
||||||
|
end)
|
|
@ -0,0 +1,82 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# x-run: python3 % /mnt/windows/Users/user/Music/vgm/night-in-the-woods/2014-lost-constellation /tmp/lost-constellation.dfpwm
|
||||||
|
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
from subprocess import Popen, PIPE
|
||||||
|
from sys import argv
|
||||||
|
from pathlib import Path
|
||||||
|
from os.path import splitext
|
||||||
|
from math import ceil
|
||||||
|
from re import search
|
||||||
|
|
||||||
|
DFPWM_ENCODER_EXECUTABLE = "aucmp"
|
||||||
|
FFMPEG_EXECUTABLE = "ffmpeg"
|
||||||
|
FFPROBE_EXECUTABLE = "ffprobe"
|
||||||
|
|
||||||
|
DFPWM_SAMPLE_RATE = 6000
|
||||||
|
|
||||||
|
TAPE_SIZES = [ 2, 4, 8, 16, 32, 64, 128 ]
|
||||||
|
|
||||||
|
input_folder = Path(argv[1])
|
||||||
|
output_file = Path(argv[2])
|
||||||
|
|
||||||
|
with TemporaryDirectory(prefix="tapedrive") as tmpdir_str:
|
||||||
|
tmpdir = Path(tmpdir_str)
|
||||||
|
|
||||||
|
filelist: list[Path] = []
|
||||||
|
titles: list[bytes] = []
|
||||||
|
for i, name in enumerate(input_folder.rglob("*.mp3")):
|
||||||
|
if i >= 48:
|
||||||
|
print(f"more than 48 tracks, skipping {name}")
|
||||||
|
continue
|
||||||
|
encoded_file = tmpdir / (splitext(name.name)[0] + ".dfpwm")
|
||||||
|
filelist.append(encoded_file)
|
||||||
|
with encoded_file.open("wb") as fout:
|
||||||
|
with Popen([ FFPROBE_EXECUTABLE, name ], stderr=PIPE) as ffprobe:
|
||||||
|
metadata: str = ffprobe.stderr.read().decode() # type: ignore
|
||||||
|
if (match := search(r"title\s+: (.*)", metadata)):
|
||||||
|
titles.append(match.groups()[0].encode("ascii", errors="ignore"))
|
||||||
|
else:
|
||||||
|
titles.append(splitext(name.name)[0].encode("ascii", errors="ignore"))
|
||||||
|
with Popen([ DFPWM_ENCODER_EXECUTABLE ], stdout=fout, stdin=PIPE) as dfpwm:
|
||||||
|
with Popen([ FFMPEG_EXECUTABLE, "-i", name, "-f", "s8", "-ac", "1",
|
||||||
|
"-ar", "48k", "-" ], stdout=dfpwm.stdin) as ffmpeg:
|
||||||
|
ffmpeg.wait()
|
||||||
|
|
||||||
|
offset: int = 6000
|
||||||
|
positions: list[tuple[int, int]] = []
|
||||||
|
for file in filelist:
|
||||||
|
size = ceil(file.stat().st_size / DFPWM_SAMPLE_RATE) * DFPWM_SAMPLE_RATE
|
||||||
|
positions.append((offset, size))
|
||||||
|
offset += size
|
||||||
|
|
||||||
|
with output_file.open("wb") as fout:
|
||||||
|
print("Writing header...")
|
||||||
|
written_bytes: int = 0
|
||||||
|
for i in range(48):
|
||||||
|
name = (titles[i] if i < len(titles) else b"")[:117]
|
||||||
|
pos = positions[i] if i < len(titles) else (0, 0)
|
||||||
|
if i < len(titles):
|
||||||
|
print(f"{i:2d} {pos[0]} + {pos[1]} {name}")
|
||||||
|
written_bytes += fout.write(pos[0].to_bytes(4, "big"))
|
||||||
|
written_bytes += fout.write(pos[1].to_bytes(4, "big"))
|
||||||
|
written_bytes += fout.write(name)
|
||||||
|
written_bytes += fout.write(b"\x00" * (117 - len(name)))
|
||||||
|
if written_bytes != 6000:
|
||||||
|
print(f"!!! expected 6000 bytes to be written in header, but it's {written_bytes}")
|
||||||
|
|
||||||
|
print("Writing files...")
|
||||||
|
for i, (pos, file) in enumerate(zip(positions, filelist)):
|
||||||
|
print(f"Writing {file.name}")
|
||||||
|
if written_bytes != pos[0]:
|
||||||
|
print(f"!!! expected to be at {pos[0]}, rn at {written_bytes}")
|
||||||
|
with file.open("rb") as fin:
|
||||||
|
file_size: int = 0
|
||||||
|
while (chunk := fin.read(DFPWM_SAMPLE_RATE)):
|
||||||
|
file_size += fout.write(chunk)
|
||||||
|
written_bytes += file_size
|
||||||
|
padding_size = DFPWM_SAMPLE_RATE - (written_bytes % DFPWM_SAMPLE_RATE)
|
||||||
|
written_bytes += fout.write(b"\x00" * padding_size)
|
||||||
|
print("Use any of those tapes to store properly:")
|
||||||
|
for size in filter(lambda sz: sz * DFPWM_SAMPLE_RATE * 60 >= written_bytes, TAPE_SIZES):
|
||||||
|
print(f"{size} minutes ({size * DFPWM_SAMPLE_RATE * 60} bytes, {written_bytes * 100 / (size * DFPWM_SAMPLE_RATE * 60):7.3f}% used)")
|
|
@ -0,0 +1,91 @@
|
||||||
|
local args = { ... }
|
||||||
|
|
||||||
|
if #args == 0 then
|
||||||
|
print("Usage: tape-rip [-S] [source-drive] [dst-drive]")
|
||||||
|
print(" -S don't seek to the beginning of destination drive")
|
||||||
|
end
|
||||||
|
|
||||||
|
local seekToStart = true
|
||||||
|
if args[1] == "-S" then
|
||||||
|
seekToStart= false
|
||||||
|
table.remove(args, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function textProgress(p, c1, c2, fmt, ...)
|
||||||
|
p = math.min(p, 1)
|
||||||
|
local tw = term.getSize()
|
||||||
|
local str = string.format(fmt, ...)
|
||||||
|
local w1 = math.ceil(p * tw)
|
||||||
|
local w2 = tw - w1
|
||||||
|
|
||||||
|
local bg = term.getBackgroundColor()
|
||||||
|
|
||||||
|
term.setBackgroundColor(c1)
|
||||||
|
term.write(str:sub(1, w1))
|
||||||
|
local rem = w1 - #str
|
||||||
|
if rem > 0 then
|
||||||
|
term.write(string.rep(" ", rem))
|
||||||
|
end
|
||||||
|
|
||||||
|
term.setBackgroundColor(c2)
|
||||||
|
term.write(str:sub(w1 + 1, w1 + w2))
|
||||||
|
|
||||||
|
rem = math.min(tw - #str, w2)
|
||||||
|
if rem > 0 then
|
||||||
|
term.write(string.rep(" ", rem))
|
||||||
|
end
|
||||||
|
|
||||||
|
term.setBackgroundColor(bg)
|
||||||
|
end
|
||||||
|
|
||||||
|
local src = peripheral.wrap(args[1])
|
||||||
|
local dst = peripheral.wrap(args[2])
|
||||||
|
|
||||||
|
local failure = false
|
||||||
|
if not src then
|
||||||
|
printError("Source drive not found")
|
||||||
|
failure = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if not dst then
|
||||||
|
printError("Destination drive not found")
|
||||||
|
failure = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if src and not src.isReady() then
|
||||||
|
printError("Source drive is empty")
|
||||||
|
failure = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if dst and not dst.isReady() then
|
||||||
|
printError("Destination drive is empty")
|
||||||
|
failure = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if failure then
|
||||||
|
printError("Some errors occurred, exiting")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
src.seek(-src.getSize())
|
||||||
|
if seekToStart then
|
||||||
|
dst.seek(-dst.getSize())
|
||||||
|
end
|
||||||
|
|
||||||
|
local _, y = term.getCursorPos()
|
||||||
|
local i = 0
|
||||||
|
while not src.isEnd() do
|
||||||
|
dst.write(src.read(6000 * 30))
|
||||||
|
local pos, sz = src.getPosition(), src.getSize()
|
||||||
|
term.setCursorPos(1, y)
|
||||||
|
term.clearLine()
|
||||||
|
if (i % 256) == 0 then
|
||||||
|
textProgress(pos / sz, colors.green, colors.gray, "%7.3f%% %8d / %8d", pos * 100 / sz, pos, sz)
|
||||||
|
os.sleep(0.01)
|
||||||
|
end
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local pos, sz = src.getPosition(), src.getSize()
|
||||||
|
textProgress(pos / sz, colors.green, colors.gray, "%7.3f%% %8d / %8d", pos * 100 / sz, pos, sz)
|
||||||
|
print()
|
128
tapeget.lua
128
tapeget.lua
|
@ -1,9 +1,70 @@
|
||||||
local args = { ... }
|
local args = { ... }
|
||||||
|
|
||||||
local tape = peripheral.find("tape_drive")
|
local seekTo = 0
|
||||||
if not tape then
|
local driveName = nil
|
||||||
print("tape where")
|
if args[1] == "-S" then
|
||||||
|
table.remove(args, 1)
|
||||||
|
seekTo = tonumber(table.remove(args, 1))
|
||||||
|
end
|
||||||
|
if args[1] == "-D" then
|
||||||
|
table.remove(args, 1)
|
||||||
|
driveName = table.remove(args, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function n_to_kib(value)
|
||||||
|
return string.format("%6.1f kiB", value / 1024)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function textProgress(p, c1, c2, fmt, ...)
|
||||||
|
p = math.min(p, 1)
|
||||||
|
local tw = term.getSize()
|
||||||
|
local str = string.format(fmt, ...)
|
||||||
|
local w1 = math.ceil(p * tw)
|
||||||
|
local w2 = tw - w1
|
||||||
|
|
||||||
|
local bg = term.getBackgroundColor()
|
||||||
|
|
||||||
|
term.setBackgroundColor(c1)
|
||||||
|
term.write(str:sub(1, w1))
|
||||||
|
local rem = w1 - #str
|
||||||
|
if rem > 0 then
|
||||||
|
term.write(string.rep(" ", rem))
|
||||||
|
end
|
||||||
|
|
||||||
|
term.setBackgroundColor(c2)
|
||||||
|
term.write(str:sub(w1 + 1, w1 + w2))
|
||||||
|
|
||||||
|
rem = math.min(tw - #str, w2)
|
||||||
|
if rem > 0 then
|
||||||
|
term.write(string.rep(" ", rem))
|
||||||
|
end
|
||||||
|
|
||||||
|
term.setBackgroundColor(bg)
|
||||||
|
end
|
||||||
|
|
||||||
|
local tape
|
||||||
|
if driveName ~= nil then
|
||||||
|
tape = peripheral.wrap(driveName)
|
||||||
|
else
|
||||||
|
local drives = { peripheral.find("tape_drive") }
|
||||||
|
if #drives == 0 then
|
||||||
|
print("Drive where")
|
||||||
return
|
return
|
||||||
|
elseif #drives ~= 1 then
|
||||||
|
print("More than one drive found:")
|
||||||
|
for i = 1, #drives do
|
||||||
|
print(peripheral.getName(drives[i]))
|
||||||
|
end
|
||||||
|
print("Input drive name:")
|
||||||
|
io.write("> ")
|
||||||
|
tape = peripheral.wrap(read())
|
||||||
|
if not tape then
|
||||||
|
printError("wrong name")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
else
|
||||||
|
tape = drives[1]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if not http then
|
if not http then
|
||||||
|
@ -11,40 +72,53 @@ if not http then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
tape.stop()
|
local req, err = http.get(args[1], {}, true)
|
||||||
tape.seek(-tape.getSize())
|
|
||||||
tape.stop()
|
|
||||||
|
|
||||||
local req = http.get(args[1], {}, true)
|
|
||||||
if not req then
|
if not req then
|
||||||
print("oopsie")
|
print("oopsie: "..err)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local headers = req.getResponseHeaders()
|
local headers = req.getResponseHeaders()
|
||||||
local length = headers["content-length"]
|
local length = tonumber(headers["content-length"]) or 1
|
||||||
|
|
||||||
local function n_to_kib(value)
|
if length > tape.getSize() then
|
||||||
return string.format("%6.1f kiB", value / 1024)
|
printError("Tape is smaller than the file you're trying to write")
|
||||||
end
|
printError("Are you sure?")
|
||||||
|
|
||||||
local written = 1
|
io.write("Write anyways? [y/N]: ")
|
||||||
local _, y = term.getCursorPos()
|
local r = read()
|
||||||
while true do
|
if r ~= "y" and r ~= "Y" then
|
||||||
local chunk = req.read()
|
return
|
||||||
if not chunk then
|
|
||||||
print("EOF")
|
|
||||||
break
|
|
||||||
end
|
|
||||||
tape.write(chunk)
|
|
||||||
written = written + 1
|
|
||||||
if (written % 8192) == 0 then
|
|
||||||
os.sleep(0.01)
|
|
||||||
term.setCursorPos(1, y)
|
|
||||||
term.write(n_to_kib(written) .. " / " .. n_to_kib(length) .. string.format(" %7.3f%%", 100 * written / length))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
tape.stop()
|
||||||
|
tape.seek(-tape.getSize())
|
||||||
|
tape.seek(seekTo)
|
||||||
|
tape.stop()
|
||||||
|
|
||||||
|
local _, y = term.getCursorPos()
|
||||||
|
local written = 0
|
||||||
|
|
||||||
|
local i = 1
|
||||||
|
while true do
|
||||||
|
local chunk = req.read(256)
|
||||||
|
if not chunk then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
written = written + #chunk
|
||||||
|
tape.write(chunk)
|
||||||
|
if i % 10 == 0 then
|
||||||
|
term.setCursorPos(1, y)
|
||||||
|
term.clearLine()
|
||||||
|
textProgress(written / length, colors.green, colors.gray, "%s / %s", n_to_kib(written), n_to_kib(length))
|
||||||
|
os.sleep(0.01)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
term.setCursorPos(1, y)
|
||||||
|
term.clearLine()
|
||||||
|
print("Written "..n_to_kib(written))
|
||||||
|
|
||||||
tape.stop()
|
tape.stop()
|
||||||
tape.seek(-tape.getSize())
|
tape.seek(-tape.getSize())
|
||||||
tape.stop()
|
tape.stop()
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
local wlan = peripheral.find("modem", function(addr, modem)
|
||||||
|
return modem.isWireless()
|
||||||
|
end)
|
||||||
|
|
||||||
|
if not wlan then error("no wireless interface") end
|
||||||
|
|
||||||
|
wlan.open(9998)
|
||||||
|
|
||||||
|
local respondedTurtles = {}
|
||||||
|
local keyStates = {}
|
||||||
|
|
||||||
|
parallel.waitForAny(
|
||||||
|
function()
|
||||||
|
while true do
|
||||||
|
local ev, keycode, repeating = os.pullEvent()
|
||||||
|
if ev == "key" then keyStates[keycode] = true
|
||||||
|
elseif ev == "key_up" then keyStates[keycode] = false
|
||||||
|
end
|
||||||
|
|
||||||
|
if ev == "key" and not repeating then
|
||||||
|
if keycode == keys.up then
|
||||||
|
wlan.transmit(9999, 9998, { _ = "move", dir = "fwd", dig = keyStates[keys.leftShift] })
|
||||||
|
elseif keycode == keys.down then
|
||||||
|
wlan.transmit(9999, 9998, { _ = "move", dir = "bck" })
|
||||||
|
elseif keycode == keys.left then
|
||||||
|
wlan.transmit(9999, 9998, { _ = "move", dir = "rotl" })
|
||||||
|
elseif keycode == keys.right then
|
||||||
|
wlan.transmit(9999, 9998, { _ = "move", dir = "rotr" })
|
||||||
|
elseif keycode == keys.pageUp then
|
||||||
|
wlan.transmit(9999, 9998, { _ = "move", dir = "up", dig = keyStates[keys.leftShift] })
|
||||||
|
elseif keycode == keys.pageDown then
|
||||||
|
wlan.transmit(9999, 9998, { _ = "move", dir = "down", dig = keyStates[keys.leftShift] })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
function()
|
||||||
|
while true do
|
||||||
|
local _, side, chan, rchan, data, dist = os.pullEvent("modem_message")
|
||||||
|
if chan == 9998 and rchan == 9999 then
|
||||||
|
if data._ == "WakeUp" then
|
||||||
|
respondedTurtles[data.from] = { true, "Hello!" }
|
||||||
|
elseif data._ == "Ack" then
|
||||||
|
respondedTurtles[data.from] = { true, "Got it" }
|
||||||
|
elseif data._ == "Error" then
|
||||||
|
respondedTurtles[data.from] = { false, data.error }
|
||||||
|
elseif data._ == "Result" then
|
||||||
|
respondedTurtles[data.from] = { true, data.out}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
function()
|
||||||
|
while true do
|
||||||
|
os.pullEvent()
|
||||||
|
term.clear()
|
||||||
|
local i = 1
|
||||||
|
for id, res in pairs(respondedTurtles) do
|
||||||
|
term.setCursorPos(1, i)
|
||||||
|
term.write(string.format("%5d => ", id))
|
||||||
|
term.setTextColor(res[1] and colors.green or colors.red)
|
||||||
|
term.write(res[1] and "OK " or "ER ")
|
||||||
|
term.write(tostring(res[2]))
|
||||||
|
term.setTextColor(colors.white)
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
|
@ -0,0 +1,60 @@
|
||||||
|
local wlan = peripheral.find("modem", function(addr, modem)
|
||||||
|
return modem.isWireless()
|
||||||
|
end)
|
||||||
|
|
||||||
|
if not wlan then error("no wireless interface") end
|
||||||
|
|
||||||
|
wlan.open(9999)
|
||||||
|
|
||||||
|
local ID = os.getComputerID()
|
||||||
|
|
||||||
|
wlan.transmit(9998, 9999, {
|
||||||
|
["_"] = "WakeUp",
|
||||||
|
["from"] = ID
|
||||||
|
})
|
||||||
|
|
||||||
|
parallel.waitForAll(function()
|
||||||
|
while true do
|
||||||
|
local _, side, chan, rchan, data, dist = os.pullEvent("modem_message")
|
||||||
|
if chan == 9999 and rchan == 9998 then
|
||||||
|
if data._ == "move" then
|
||||||
|
wlan.transmit(rchan, chan, { ["_"] = "Ack", ["from"] = ID })
|
||||||
|
if data.tgt == nil or data.tgt == ID then
|
||||||
|
local out = { false, "Invalid direction: " .. data.dir }
|
||||||
|
if data.dig then
|
||||||
|
if data.dir == "fwd" and turtle.detect() then turtle.dig()
|
||||||
|
elseif data.dir == "down" and turtle.detectDown() then turtle.digDown()
|
||||||
|
elseif data.dir == "up" and turtle.detectUp() then turtle.digUp()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if data.dir == "fwd" then out = { turtle.forward() }
|
||||||
|
elseif data.dir == "bck" then out = { turtle.back() }
|
||||||
|
elseif data.dir == "up" then out = { turtle.up() }
|
||||||
|
elseif data.dir == "down" then out = { turtle.down() }
|
||||||
|
elseif data.dir == "rotl" then out = { turtle.turnLeft() }
|
||||||
|
elseif data.dir == "rotr" then out = { turtle.turnRight() }
|
||||||
|
end
|
||||||
|
if not out[1] then
|
||||||
|
wlan.transmit(rchan, chan, {
|
||||||
|
["_"] = "Error",
|
||||||
|
["from"] = ID,
|
||||||
|
["error"] = out[2]
|
||||||
|
})
|
||||||
|
else
|
||||||
|
wlan.transmit(rchan, chan, {
|
||||||
|
["_"] = "Result",
|
||||||
|
["from"] = ID,
|
||||||
|
["out"] = out
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif data._ == "shutdown" then
|
||||||
|
wlan.transmit(rchan, chan, { ["_"] = "Ack", ["from"] = ID })
|
||||||
|
os.shutdown()
|
||||||
|
elseif data._ == "reboot" then
|
||||||
|
wlan.transmit(rchan, chan, { ["_"] = "Ack", ["from"] = ID })
|
||||||
|
os.reboot()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
|
@ -0,0 +1,57 @@
|
||||||
|
if not turtle then
|
||||||
|
printError("not a turtle")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local base_path = "https://git.salushnes.solutions/hkc/cc-stuff/raw/branch/master/turtos"
|
||||||
|
|
||||||
|
_G._TOS_VER = "N/A"
|
||||||
|
local tos_ver_fp = io.open("/.tos-ver", "r")
|
||||||
|
if tos_ver_fp then
|
||||||
|
_G._TOS_VER = tos_ver_fp:read("l")
|
||||||
|
tos_ver_fp:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getFile(url, path)
|
||||||
|
local r, err = http.get(url)
|
||||||
|
io.write("GET " .. path .. " ... ")
|
||||||
|
if not r then
|
||||||
|
print("FAIL: " .. err)
|
||||||
|
return false, err
|
||||||
|
end
|
||||||
|
io.open(path, "w"):write(r.readAll()):close()
|
||||||
|
io.write("OK\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local req, err = http.get(base_path .. "/update.json")
|
||||||
|
if not req then
|
||||||
|
printError("Failed to get update info:", err)
|
||||||
|
else
|
||||||
|
local info = textutils.unserializeJSON(req.readAll())
|
||||||
|
req.close()
|
||||||
|
|
||||||
|
print("OTP version: " .. info.ver)
|
||||||
|
print("H/W version: " .. _TOS_VER)
|
||||||
|
|
||||||
|
if info.ver == _TOS_VER then
|
||||||
|
print("Running on latest firmware")
|
||||||
|
else
|
||||||
|
print("Performing an update...")
|
||||||
|
for i = 1, #info.files do
|
||||||
|
local file = info.files[i]
|
||||||
|
term.write(file.dst, "...")
|
||||||
|
if fs.exists(file.dst) then
|
||||||
|
term.write(" [DEL] ...")
|
||||||
|
fs.delete(file.dst)
|
||||||
|
end
|
||||||
|
getFile(base_path .. file.src, file.dst)
|
||||||
|
end
|
||||||
|
print("Writing new version info")
|
||||||
|
io.open("/.tos-ver", "w"):write(info.ver):close()
|
||||||
|
print("Rebooting")
|
||||||
|
return os.reboot()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shell.run("fg", "main.lua")
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"ver": "0.0.3",
|
||||||
|
"files": [
|
||||||
|
{ "src": "/startup.lua", "dst": "/startup.lua" },
|
||||||
|
{ "src": "/main.lua", "dst": "/main.lua" }
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,275 @@
|
||||||
|
local args = { ... }
|
||||||
|
local dfpwm = require("cc.audio.dfpwm")
|
||||||
|
local ccpi = require("ccpi")
|
||||||
|
|
||||||
|
local EV_NONCE = math.floor(0xFFFFFFFF * math.random())
|
||||||
|
|
||||||
|
settings.define("video.speaker.left", {
|
||||||
|
description = "Speaker ID for left audio channel",
|
||||||
|
default = peripheral.getName(peripheral.find("speaker")),
|
||||||
|
type = "string"
|
||||||
|
})
|
||||||
|
|
||||||
|
settings.define("video.speaker.right", {
|
||||||
|
description = "Speaker ID for right audio channel",
|
||||||
|
default = nil,
|
||||||
|
type = "string"
|
||||||
|
})
|
||||||
|
|
||||||
|
settings.define("video.monitor", {
|
||||||
|
description = "Monitor to draw frames on",
|
||||||
|
default = peripheral.getName(peripheral.find("monitor")),
|
||||||
|
type = "string"
|
||||||
|
})
|
||||||
|
|
||||||
|
local monitor = peripheral.wrap(settings.get("video.monitor"))
|
||||||
|
local speakers = {
|
||||||
|
l = peripheral.wrap(settings.get("video.speaker.left")),
|
||||||
|
r = nil
|
||||||
|
}
|
||||||
|
local r_spk_id = settings.get("video.speaker.right")
|
||||||
|
if r_spk_id then
|
||||||
|
speakers.r = peripheral.wrap(r_spk_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
local delay = 0
|
||||||
|
local loading_concurrency = 8
|
||||||
|
local buffer_size = 8192
|
||||||
|
local wait_until_input = false
|
||||||
|
local n_frames, video_url, audio_url_l, audio_url_r
|
||||||
|
|
||||||
|
while args[1] ~= nil and string.sub(args[1], 1, 1) == "-" do
|
||||||
|
local k = table.remove(args, 1):sub(2)
|
||||||
|
if k == "m" or k == "monitor" then
|
||||||
|
monitor = peripheral.wrap(table.remove(args, 1))
|
||||||
|
elseif k == "l" or k == "speaker-left" then
|
||||||
|
speakers.l = peripheral.wrap(table.remove(args, 1))
|
||||||
|
elseif k == "r" or k == "speaker-right" then
|
||||||
|
speakers.r = peripheral.wrap(table.remove(args, 1))
|
||||||
|
elseif k == "d" or k == "delay" then
|
||||||
|
delay = tonumber(table.remove(args, 1))
|
||||||
|
elseif k == "t" or k == "threads" then
|
||||||
|
loading_concurrency = tonumber(table.remove(args, 1))
|
||||||
|
elseif k == "b" or k == "buffer-size" then
|
||||||
|
buffer_size = tonumber(table.remove(args, 1))
|
||||||
|
elseif k == "w" or k == "wait" then
|
||||||
|
wait_until_input = true
|
||||||
|
elseif k == "J" or k == "info-json" then
|
||||||
|
local req = assert(http.get(table.remove(args, 1)))
|
||||||
|
local info = textutils.unserializeJSON(req.readAll())
|
||||||
|
delay = info.frame_time
|
||||||
|
n_frames = info.frame_count
|
||||||
|
video_url = info.video
|
||||||
|
audio_url_l = info.audio.l
|
||||||
|
audio_url_r = info.audio.r
|
||||||
|
req.close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not monitor then
|
||||||
|
printError("No monitor connected or invalid one specified")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not speakers.l then
|
||||||
|
printError("No speaker connected or invalid one specified")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if not n_frames and not video_url and not audio_url_l then
|
||||||
|
if #args < 3 then
|
||||||
|
printError("Usage: video [-w] [-b BUFSZ] [-t THREADS] [-J URL] [-m MONITOR] [-l SPK_L] [-r SPK_R] [-d FRAME_T] <N_FRAMES> <VIDEO_TEMPLATE> <LEFT_CHANNEL> [RIGHT_CHANNEL]")
|
||||||
|
return
|
||||||
|
else
|
||||||
|
n_frames = tonumber(table.remove(args, 1))
|
||||||
|
video_url = table.remove(args, 1)
|
||||||
|
audio_url_l = table.remove(args, 1)
|
||||||
|
audio_url_r = #args > 0 and table.remove(args, 1) or nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local mon_w, mon_h = monitor.getSize()
|
||||||
|
print(string.format("Using monitor %s (%dx%d)", peripheral.getName(monitor), mon_w, mon_h))
|
||||||
|
if speakers.r then
|
||||||
|
print(string.format("Stereo sound: L=%s R=%s", peripheral.getName(speakers.l), peripheral.getName(speakers.r)))
|
||||||
|
else
|
||||||
|
print("Mono sound: " .. peripheral.getName(speakers.l))
|
||||||
|
end
|
||||||
|
|
||||||
|
if not speakers.r and audio_url_r then
|
||||||
|
printError("No right speaker found but right audio channel was specified")
|
||||||
|
printError("Right channel will not be played")
|
||||||
|
elseif speakers.r and not audio_url_r then
|
||||||
|
printError("No URL for right channel was specified but right speaker is set")
|
||||||
|
printError("Right speaker will remain silent")
|
||||||
|
end
|
||||||
|
|
||||||
|
print("\n\n")
|
||||||
|
local _, ty = term.getCursorPos()
|
||||||
|
local tw, _ = term.getSize()
|
||||||
|
|
||||||
|
local function draw_bar(y, c1, c2, p, fmt, ...)
|
||||||
|
local str = string.format(fmt, ...)
|
||||||
|
local w1 = math.ceil(p * tw)
|
||||||
|
local w2 = tw - w1
|
||||||
|
|
||||||
|
term.setCursorPos(1, y)
|
||||||
|
term.setBackgroundColor(c1)
|
||||||
|
term.write(str:sub(1, w1))
|
||||||
|
local rem = w1 - #str
|
||||||
|
if rem > 0 then
|
||||||
|
term.write(string.rep(" ", rem))
|
||||||
|
end
|
||||||
|
|
||||||
|
term.setBackgroundColor(c2)
|
||||||
|
term.write(str:sub(w1 + 1, w1 + w2))
|
||||||
|
|
||||||
|
rem = math.min(tw - #str, w2)
|
||||||
|
if rem > 0 then
|
||||||
|
term.write(string.rep(" ", rem))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function decode_s8(data)
|
||||||
|
local buffer = {}
|
||||||
|
for i = 1, #data do
|
||||||
|
local v = string.byte(data, i)
|
||||||
|
if bit32.band(v, 0x80) then
|
||||||
|
v = bit32.bxor(v, 0x7F) - 128
|
||||||
|
end
|
||||||
|
buffer[i] = v
|
||||||
|
end
|
||||||
|
return buffer
|
||||||
|
end
|
||||||
|
|
||||||
|
local frames = {}
|
||||||
|
local audio_frames = { l = {}, r = {} }
|
||||||
|
|
||||||
|
local subthreads = {}
|
||||||
|
local dl_channels = {
|
||||||
|
l = assert(http.get(audio_url_l, nil, true)),
|
||||||
|
r = audio_url_r and assert(http.get(audio_url_r, nil, true)) or nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
local n_audio_samples = math.ceil(dl_channels.l.seek("end") / buffer_size)
|
||||||
|
dl_channels.l.seek("set", 0)
|
||||||
|
|
||||||
|
table.insert(subthreads, function()
|
||||||
|
local chunk
|
||||||
|
repeat
|
||||||
|
chunk = dl_channels.l.read(buffer_size)
|
||||||
|
table.insert(audio_frames.l, chunk or {})
|
||||||
|
if (#audio_frames.l % 8) == 0 then os.sleep(0) end
|
||||||
|
until not chunk or #chunk == 0
|
||||||
|
end)
|
||||||
|
|
||||||
|
if dl_channels.r then
|
||||||
|
table.insert(subthreads, function()
|
||||||
|
local chunk
|
||||||
|
repeat
|
||||||
|
chunk = dl_channels.r.read(buffer_size)
|
||||||
|
table.insert(audio_frames.r, chunk or {})
|
||||||
|
if (#audio_frames.r % 8) == 0 then os.sleep(0) end
|
||||||
|
until not chunk or #chunk == 0
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, loading_concurrency do
|
||||||
|
table.insert(subthreads, function()
|
||||||
|
for ndx = i, n_frames, loading_concurrency do
|
||||||
|
local req = assert(http.get(string.format(video_url, ndx), nil, true))
|
||||||
|
local img = assert(ccpi.parse(req))
|
||||||
|
frames[ndx] = img
|
||||||
|
req.close()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(subthreads, function()
|
||||||
|
repeat
|
||||||
|
draw_bar(ty - 3, colors.blue, colors.gray, #frames / n_frames, "Loading video [%5d / %5d]", #frames, n_frames)
|
||||||
|
draw_bar(ty - 2, colors.red, colors.gray, #audio_frames.l / n_audio_samples, "Loading audio [%5d / %5d]", #audio_frames.l, n_audio_samples)
|
||||||
|
os.sleep(0.25)
|
||||||
|
until #frames >= n_frames and #audio_frames.l >= n_audio_samples
|
||||||
|
print()
|
||||||
|
end)
|
||||||
|
|
||||||
|
local playback_done = false
|
||||||
|
|
||||||
|
table.insert(subthreads, function()
|
||||||
|
local tmr = os.startTimer(0.25)
|
||||||
|
while true do
|
||||||
|
local ev = { os.pullEvent() }
|
||||||
|
if ev[1] == "key" and ev[2] == keys.enter then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
term.setCursorPos(1, ty - 1)
|
||||||
|
term.setBackgroundColor(colors.gray)
|
||||||
|
term.clearLine()
|
||||||
|
term.write(string.format("Waiting for frames... (V:%d, A:%d)", #frames, #audio_frames.l))
|
||||||
|
if #frames > 60 and #audio_frames.l >= n_audio_samples and not wait_until_input then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
os.queueEvent("playback_ready", EV_NONCE)
|
||||||
|
end)
|
||||||
|
|
||||||
|
table.insert(subthreads, function()
|
||||||
|
local is_dfpwm = ({ audio_url_l:find("%.dfpwm") })[2] == #audio_url_l
|
||||||
|
local decode = use_dfpwm and dfpwm.make_decoder() or decode_s8
|
||||||
|
|
||||||
|
repeat
|
||||||
|
local _, nonce = os.pullEvent("playback_ready")
|
||||||
|
until nonce == EV_NONCE
|
||||||
|
|
||||||
|
for i = 1, n_audio_samples do
|
||||||
|
local buffer = decode(audio_frames.l[i])
|
||||||
|
while not speakers.l.playAudio(buffer) do
|
||||||
|
os.pullEvent("speaker_audio_empty")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
playback_done = true
|
||||||
|
end)
|
||||||
|
|
||||||
|
table.insert(subthreads, function()
|
||||||
|
if not audio_url_r then return end
|
||||||
|
local is_dfpwm = ({ audio_url_r:find("%.dfpwm") })[2] == #audio_url_r
|
||||||
|
local decode = use_dfpwm and dfpwm.make_decoder() or decode_s8
|
||||||
|
|
||||||
|
repeat
|
||||||
|
local _, nonce = os.pullEvent("playback_ready")
|
||||||
|
until nonce == EV_NONCE
|
||||||
|
|
||||||
|
for i = 1, n_audio_samples do
|
||||||
|
local buffer = decode(audio_frames.r[i])
|
||||||
|
while not speakers.r.playAudio(buffer) do
|
||||||
|
os.pullEvent("speaker_audio_empty")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
playback_done = true
|
||||||
|
end)
|
||||||
|
|
||||||
|
table.insert(subthreads, function()
|
||||||
|
repeat
|
||||||
|
local _, nonce = os.pullEvent("playback_ready")
|
||||||
|
until nonce == EV_NONCE
|
||||||
|
local start_t = os.clock()
|
||||||
|
while not playback_done do
|
||||||
|
local frame = math.floor((os.clock() - start_t) / math.max(0.05, delay))
|
||||||
|
term.setCursorPos(1, ty - 1)
|
||||||
|
term.setBackgroundColor(frame >= #frames and colors.red or colors.gray)
|
||||||
|
term.clearLine()
|
||||||
|
term.write(string.format("Playing frame: %d/%d", frame + 1, #frames))
|
||||||
|
local img = frames[frame + 1]
|
||||||
|
if img ~= nil then
|
||||||
|
local x = math.max(math.floor((mon_w - img.w) / 2), 1)
|
||||||
|
local y = math.max(math.floor((mon_h - img.h) / 2), 1)
|
||||||
|
ccpi.draw(img, x, y, monitor)
|
||||||
|
end
|
||||||
|
os.sleep(delay)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
parallel.waitForAll(table.unpack(subthreads))
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
local tape = peripheral.find("tape_drive")
|
||||||
|
local mon = peripheral.find("monitor")
|
||||||
|
|
||||||
|
tape.seek(-tape.getSize())
|
||||||
|
mon.setTextScale(1)
|
||||||
|
|
||||||
|
local w, h = string.byte(tape.read(2), 1, 2)
|
||||||
|
|
||||||
|
local b = ""
|
||||||
|
for _ = 1, w do b = b .. "0" end
|
||||||
|
local f = b
|
||||||
|
|
||||||
|
repeat
|
||||||
|
for y = 1, h do
|
||||||
|
mon.setCursorPos(1, y)
|
||||||
|
local buf = { string.byte(tape.read(w), 1, -1) }
|
||||||
|
local s = "" -- , f, b = "", "", ""
|
||||||
|
for x = 1, w do
|
||||||
|
s = s .. string.char(0x80 + bit32.band(0x7F, buf[x]))
|
||||||
|
-- local inv = bit32.band(0x80, buf[x])
|
||||||
|
-- f = f .. (inv and "f" or "0")
|
||||||
|
-- b = b .. (inv and "0" or "f")
|
||||||
|
end
|
||||||
|
mon.blit(s, f, b)
|
||||||
|
end
|
||||||
|
os.sleep(0.01)
|
||||||
|
until tape.isEnd()
|
|
@ -0,0 +1,385 @@
|
||||||
|
// x-run: ~/scripts/runc.sh % -lmongoose -Wall -Wextra
|
||||||
|
#include <mongoose.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define MAX_CLIENTS 256
|
||||||
|
#define MAX_OPEN_CHANNELS 128
|
||||||
|
#define MAX_PACKET_SIZE 32767
|
||||||
|
|
||||||
|
/*
|
||||||
|
```c
|
||||||
|
// ALL integers are network-endian
|
||||||
|
|
||||||
|
struct msg_info { // side: both. safe to ignore
|
||||||
|
uint8_t code; // 0x49, 'I'
|
||||||
|
uint16_t len;
|
||||||
|
char msg[1024]; // $.len bytes sent
|
||||||
|
};
|
||||||
|
|
||||||
|
struct msg_res_error {
|
||||||
|
uint8_t code; // 0x45, 'E'
|
||||||
|
uint16_t req_id; // request ID that caused that error
|
||||||
|
uint16_t len;
|
||||||
|
char msg[1024]; // $.len bytes sent
|
||||||
|
};
|
||||||
|
|
||||||
|
struct msg_address { // side: server
|
||||||
|
uint8_t code; // 0x41, 'A'
|
||||||
|
uint8_t size;
|
||||||
|
char name[256]; // $.size long
|
||||||
|
};
|
||||||
|
|
||||||
|
struct msg_res_success { // side: server
|
||||||
|
uint8_t code; // 0x52, 'R'
|
||||||
|
uint16_t req_id; // request ID we're replying to
|
||||||
|
void *data; // packet-specific
|
||||||
|
};
|
||||||
|
|
||||||
|
struct msg_transmission { // side: server
|
||||||
|
uint8_t code; // 0x54, 'T'
|
||||||
|
uint16_t channel;
|
||||||
|
uint16_t replyChannel;
|
||||||
|
uint16_t size;
|
||||||
|
void *data;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct msg_req_open { // side: client
|
||||||
|
uint8_t code; // 0x4f, 'O'
|
||||||
|
uint16_t req_id; // incremental request ID
|
||||||
|
uint16_t channel; // channel to be open
|
||||||
|
};
|
||||||
|
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
|
||||||
|
struct client {
|
||||||
|
struct mg_connection *connection;
|
||||||
|
uint16_t open_channels[MAX_OPEN_CHANNELS];
|
||||||
|
int next_open_channel_index;
|
||||||
|
bool receive_all;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct client clients[MAX_CLIENTS] = { 0 };
|
||||||
|
|
||||||
|
static void handle_client(struct mg_connection *connection, int event_type, void *ev_data, void *fn_data);
|
||||||
|
static void on_ws_connect(struct mg_connection *connection, struct mg_http_message *message, void *data);
|
||||||
|
static void on_ws_message(struct mg_connection *connection, struct mg_ws_message *message, void *data);
|
||||||
|
static void on_ws_disconnect(struct mg_connection *connection, void *data);
|
||||||
|
|
||||||
|
bool client_is_open(struct client *client, uint16_t channel);
|
||||||
|
static void modem_open(struct client *client, uint16_t request_id, uint16_t channel);
|
||||||
|
static void modem_isOpen(struct client *client, uint16_t request_id, uint16_t channel);
|
||||||
|
static void modem_close(struct client *client, uint16_t request_id, uint16_t channel);
|
||||||
|
static void modem_closeAll(struct client *client, uint16_t request_id);
|
||||||
|
static void modem_transmit(struct client *client, uint16_t request_id, uint16_t channel, uint16_t reply_channel, void *data, uint16_t size);
|
||||||
|
|
||||||
|
struct metrics {
|
||||||
|
uint64_t sent_bytes;
|
||||||
|
uint64_t sent_messages;
|
||||||
|
uint64_t received_bytes;
|
||||||
|
uint64_t received_messages;
|
||||||
|
uint64_t errors;
|
||||||
|
|
||||||
|
uint64_t method_calls[5];
|
||||||
|
} metrics = { 0 };
|
||||||
|
|
||||||
|
const char method_names[5][8] = {
|
||||||
|
"open", "isOpen", "close", "closeAll", "transmit"
|
||||||
|
};
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
const char *address = "ws://0.0.0.0:8667";
|
||||||
|
struct mg_mgr manager;
|
||||||
|
mg_mgr_init(&manager);
|
||||||
|
mg_http_listen(&manager, address, handle_client, NULL);
|
||||||
|
printf("Listening on %s\n", address);
|
||||||
|
while (1) mg_mgr_poll(&manager, 1000);
|
||||||
|
mg_mgr_free(&manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_client(struct mg_connection *connection, int event_type, void *event_data, void *fn_data) {
|
||||||
|
if (event_type == MG_EV_OPEN) {
|
||||||
|
if (connection->rem.port == 0) return;
|
||||||
|
memset(connection->data, 0, 32);
|
||||||
|
} else if (event_type == MG_EV_HTTP_MSG) {
|
||||||
|
struct mg_http_message *http_message = (struct mg_http_message *) event_data;
|
||||||
|
if (mg_http_match_uri(http_message, "/open")) {
|
||||||
|
mg_ws_upgrade(connection, http_message, NULL);
|
||||||
|
} else if (mg_http_match_uri(http_message, "/metrics")) {
|
||||||
|
mg_printf(connection, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n");
|
||||||
|
mg_http_printf_chunk(connection, "# HELP ws_bytes_sent_total Number of bytes sent to clients\n");
|
||||||
|
mg_http_printf_chunk(connection, "# TYPE ws_bytes_sent_total counter\n");
|
||||||
|
mg_http_printf_chunk(connection, "ws_bytes_sent_total %ld\n", metrics.sent_bytes);
|
||||||
|
mg_http_printf_chunk(connection, "# HELP ws_bytes_received_total Number of bytes received to clients\n");
|
||||||
|
mg_http_printf_chunk(connection, "# TYPE ws_bytes_received_total counter\n");
|
||||||
|
mg_http_printf_chunk(connection, "ws_bytes_received_total %ld\n", metrics.received_bytes);
|
||||||
|
|
||||||
|
mg_http_printf_chunk(connection, "# HELP ws_messages_sent_total Number of messages sent to clients\n");
|
||||||
|
mg_http_printf_chunk(connection, "# TYPE ws_messages_sent_total counter\n");
|
||||||
|
mg_http_printf_chunk(connection, "ws_messages_sent_total %ld\n", metrics.sent_messages);
|
||||||
|
mg_http_printf_chunk(connection, "# HELP ws_messages_received_total Number of messages received to clients\n");
|
||||||
|
mg_http_printf_chunk(connection, "# TYPE ws_messages_received_total counter\n");
|
||||||
|
mg_http_printf_chunk(connection, "ws_messages_received_total %ld\n", metrics.received_messages);
|
||||||
|
|
||||||
|
mg_http_printf_chunk(connection, "# HELP ws_clients Number of active websocket clients\n");
|
||||||
|
mg_http_printf_chunk(connection, "# TYPE ws_clients gauge\n");
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
for (struct mg_connection *conn = connection->mgr->conns; conn != NULL; conn = conn->next) {
|
||||||
|
if (conn->is_websocket) { n++; }
|
||||||
|
}
|
||||||
|
mg_http_printf_chunk(connection, "ws_clients %d\n", n);
|
||||||
|
}
|
||||||
|
|
||||||
|
mg_http_printf_chunk(connection, "# HELP method_calls Times each method was called\n");
|
||||||
|
mg_http_printf_chunk(connection, "# TYPE method_calls counter\n");
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
mg_http_printf_chunk(connection, "method_calls{method=\"%s\"} %ld\n", method_names[i], metrics.method_calls[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
mg_http_printf_chunk(connection, "");
|
||||||
|
} else {
|
||||||
|
mg_http_reply(connection, 404, "", "uwu");
|
||||||
|
}
|
||||||
|
} else if (event_type == MG_EV_WS_OPEN) {
|
||||||
|
struct mg_http_message *http_message = (struct mg_http_message *) event_data;
|
||||||
|
on_ws_connect(connection, http_message, fn_data);
|
||||||
|
} else if (event_type == MG_EV_WS_MSG) {
|
||||||
|
struct mg_ws_message *ws_message = (struct mg_ws_message *)event_data;
|
||||||
|
on_ws_message(connection, ws_message, fn_data);
|
||||||
|
} else if (event_type == MG_EV_CLOSE) {
|
||||||
|
if (connection->is_websocket) {
|
||||||
|
on_ws_disconnect(connection, fn_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ws_send_error(struct client *client, uint16_t request_id, const char *fmt, ...) {
|
||||||
|
static char buffer[1024];
|
||||||
|
memset(buffer, 0, 1024);
|
||||||
|
|
||||||
|
va_list args;
|
||||||
|
va_start(args, fmt);
|
||||||
|
int text_size = vsnprintf(&buffer[5], 1019, fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
if (text_size < 0) return;
|
||||||
|
|
||||||
|
buffer[0] = 'E';
|
||||||
|
buffer[1] = (request_id >> 8) & 0xFF;
|
||||||
|
buffer[2] = request_id & 0xFF;
|
||||||
|
buffer[3] = (text_size >> 8) & 0xFF;
|
||||||
|
buffer[4] = text_size & 0xFF;
|
||||||
|
|
||||||
|
metrics.sent_bytes += 5 + text_size;
|
||||||
|
metrics.sent_messages++;
|
||||||
|
metrics.errors++;
|
||||||
|
mg_ws_send(client->connection, buffer, 5 + text_size, WEBSOCKET_OP_BINARY);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ws_send_info(struct client *client, const char *fmt, ...) {
|
||||||
|
static char buffer[1024];
|
||||||
|
memset(buffer, 0, 1024);
|
||||||
|
|
||||||
|
va_list args;
|
||||||
|
va_start(args, fmt);
|
||||||
|
int text_size = vsnprintf(&buffer[3], 1021, fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
if (text_size < 0) return;
|
||||||
|
|
||||||
|
buffer[0] = 'I';
|
||||||
|
buffer[1] = (text_size >> 8) & 0xFF;
|
||||||
|
buffer[2] = text_size & 0xFF;
|
||||||
|
|
||||||
|
metrics.sent_bytes += 3 + text_size;
|
||||||
|
metrics.sent_messages++;
|
||||||
|
mg_ws_send(client->connection, buffer, 3 + text_size, WEBSOCKET_OP_BINARY);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ws_respond(struct client *client, uint16_t request_id, void *data, uint32_t size) {
|
||||||
|
static char buffer[MAX_PACKET_SIZE];
|
||||||
|
assert(size < MAX_PACKET_SIZE);
|
||||||
|
buffer[0] = 'R';
|
||||||
|
buffer[1] = (request_id >> 8) & 0xFF;
|
||||||
|
buffer[2] = request_id & 0xFF;
|
||||||
|
if (size != 0) memcpy(&buffer[3], data, size);
|
||||||
|
metrics.sent_bytes += 3 + size;
|
||||||
|
metrics.sent_messages++;
|
||||||
|
mg_ws_send(client->connection, buffer, size + 3, WEBSOCKET_OP_BINARY);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_ws_connect(struct mg_connection *connection, struct mg_http_message *message, void *data) {
|
||||||
|
(void)message;
|
||||||
|
(void)data;
|
||||||
|
struct client *client = malloc(sizeof(struct client));
|
||||||
|
memcpy(&connection->data[0], &client, sizeof(struct client *));
|
||||||
|
client->connection = connection;
|
||||||
|
|
||||||
|
static char buffer[256];
|
||||||
|
buffer[0] = 'A';
|
||||||
|
buffer[1] = snprintf(&buffer[2], 250, "wsvpn_%ld", connection->id);
|
||||||
|
metrics.sent_bytes += 2 + buffer[1];
|
||||||
|
metrics.sent_messages++;
|
||||||
|
mg_ws_send(connection, buffer, 2 + buffer[1], WEBSOCKET_OP_BINARY);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_ws_message(struct mg_connection *connection, struct mg_ws_message *message, void *data) {
|
||||||
|
(void)data;
|
||||||
|
|
||||||
|
if ((message->flags & 15) != WEBSOCKET_OP_BINARY) {
|
||||||
|
const char *err_str = "This server only works in binary mode. Sorry!";
|
||||||
|
mg_ws_send(connection, err_str, strlen(err_str), WEBSOCKET_OP_TEXT);
|
||||||
|
connection->is_draining = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct client *client = *(struct client **)&connection->data[0];
|
||||||
|
assert(client->connection == connection);
|
||||||
|
|
||||||
|
metrics.received_bytes += message->data.len;
|
||||||
|
metrics.received_messages++;
|
||||||
|
|
||||||
|
if (message->data.len == 0) return;
|
||||||
|
|
||||||
|
uint16_t request_id = ntohs(*(uint16_t*)&message->data.ptr[1]);
|
||||||
|
|
||||||
|
switch (message->data.ptr[0]) {
|
||||||
|
case 'I': // info. We can safely ignore that message
|
||||||
|
break;
|
||||||
|
case 'O': // open
|
||||||
|
{
|
||||||
|
metrics.method_calls[0]++;
|
||||||
|
uint16_t channel = ntohs(*(uint16_t*)&message->data.ptr[3]);
|
||||||
|
printf("%p[%04x] modem.open(%d)\n", (void*)client, request_id, channel);
|
||||||
|
modem_open(client, request_id, channel);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case 'o': // isOpen
|
||||||
|
{
|
||||||
|
metrics.method_calls[1]++;
|
||||||
|
uint16_t channel = ntohs(*(uint16_t*)&message->data.ptr[3]);
|
||||||
|
printf("%p[%04x] modem.isOpen(%d)\n", (void*)client, request_id, channel);
|
||||||
|
modem_isOpen(client, request_id, channel);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case 'c': // close
|
||||||
|
{
|
||||||
|
metrics.method_calls[2]++;
|
||||||
|
uint16_t channel = ntohs(*(uint16_t*)&message->data.ptr[3]);
|
||||||
|
printf("%p[%04x] modem.close(%d)\n", (void*)client, request_id, channel);
|
||||||
|
modem_close(client, request_id, channel);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case 'C': // closeAll
|
||||||
|
{
|
||||||
|
metrics.method_calls[3]++;
|
||||||
|
printf("%p[%04x] modem.closeAll()\n", (void*)client, request_id);
|
||||||
|
modem_closeAll(client, request_id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case 'T': // transmit
|
||||||
|
{
|
||||||
|
metrics.method_calls[4]++;
|
||||||
|
uint16_t channel = ntohs(*(uint16_t*)&message->data.ptr[3]);
|
||||||
|
uint16_t reply_channel = ntohs(*(uint16_t*)&message->data.ptr[5]);
|
||||||
|
uint16_t data_length = ntohs(*(uint16_t*)&message->data.ptr[7]);
|
||||||
|
modem_transmit(client, request_id, channel, reply_channel, (void*)&message->data.ptr[9], data_length);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
ws_send_error(client, request_id, "Unknown opcode: 0x%02x", message->data.ptr[0]);
|
||||||
|
connection->is_draining = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_ws_disconnect(struct mg_connection *connection, void *data) {
|
||||||
|
(void)data;
|
||||||
|
|
||||||
|
struct client *client = *(struct client **)&connection->data[0];
|
||||||
|
if (client->connection == connection) {
|
||||||
|
free(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void modem_open(struct client *client, uint16_t request_id, uint16_t channel) {
|
||||||
|
if (client_is_open(client, channel)) {
|
||||||
|
ws_respond(client, request_id, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client->next_open_channel_index == MAX_OPEN_CHANNELS) {
|
||||||
|
ws_send_error(client, request_id, "Too many open channels");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client->open_channels[client->next_open_channel_index] = channel;
|
||||||
|
client->next_open_channel_index++;
|
||||||
|
ws_respond(client, request_id, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void modem_isOpen(struct client *client, uint16_t request_id, uint16_t channel) {
|
||||||
|
unsigned char is_open = client_is_open(client, channel) ? 42 : 0;
|
||||||
|
ws_respond(client, request_id, &is_open, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void modem_close(struct client *client, uint16_t request_id, uint16_t channel) {
|
||||||
|
for (int i = 0; i < client->next_open_channel_index; i++) {
|
||||||
|
if (client->open_channels[i] == channel) {
|
||||||
|
client->open_channels[i] = client->open_channels[client->next_open_channel_index - 1];
|
||||||
|
client->next_open_channel_index--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ws_respond(client, request_id, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void modem_closeAll(struct client *client, uint16_t request_id) {
|
||||||
|
client->next_open_channel_index = 0;
|
||||||
|
memset(client->open_channels, 0, sizeof(uint16_t) * MAX_OPEN_CHANNELS);
|
||||||
|
ws_respond(client, request_id, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void modem_transmit(struct client *client, uint16_t request_id, uint16_t channel, uint16_t reply_channel, void *data, uint16_t size) {
|
||||||
|
static uint8_t buffer[MAX_PACKET_SIZE + 7];
|
||||||
|
|
||||||
|
if (size > MAX_PACKET_SIZE) {
|
||||||
|
ws_send_error(client, request_id, "Packet too big: %d > %d", size, MAX_PACKET_SIZE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer[0] = 'T';
|
||||||
|
buffer[1] = (channel >> 8) & 0xFF;
|
||||||
|
buffer[2] = channel & 0xFF;
|
||||||
|
buffer[3] = (reply_channel >> 8) & 0xFF;
|
||||||
|
buffer[4] = reply_channel & 0xFF;
|
||||||
|
buffer[5] = (size >> 8) & 0xFF;
|
||||||
|
buffer[6] = size & 0xFF;
|
||||||
|
memcpy(&buffer[7], data, size);
|
||||||
|
|
||||||
|
for (struct mg_connection *conn = client->connection->mgr->conns; conn != NULL; conn = conn->next) {
|
||||||
|
if (conn->is_websocket) {
|
||||||
|
struct client *other_client = *(struct client **)&conn->data[0];
|
||||||
|
if (other_client->connection == conn && other_client->connection != client->connection) {
|
||||||
|
if (client_is_open(other_client, channel)) {
|
||||||
|
metrics.sent_bytes += size + 7;
|
||||||
|
metrics.sent_messages++;
|
||||||
|
mg_ws_send(other_client->connection, buffer, size + 7, WEBSOCKET_OP_BINARY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ws_respond(client, request_id, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool client_is_open(struct client *client, uint16_t channel) {
|
||||||
|
for (int i = 0; i < client->next_open_channel_index; i++) {
|
||||||
|
if (client->open_channels[i] == channel) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
local expect = require("cc.expect")
|
||||||
|
|
||||||
|
local WSModem = {
|
||||||
|
open = function(self, channel)
|
||||||
|
expect.expect(1, channel, "number")
|
||||||
|
expect.range(channel, 0, 65535)
|
||||||
|
self._request(0x4f, {
|
||||||
|
bit.band(0xFF, bit.brshift(channel, 8)),
|
||||||
|
bit.band(0xFF, channel)
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
isOpen = function(self, channel)
|
||||||
|
expect.expect(1, channel, "number")
|
||||||
|
expect.range(channel, 0, 65535)
|
||||||
|
return self._request(0x6f, {
|
||||||
|
bit.band(0xFF, bit.brshift(channel, 8)),
|
||||||
|
bit.band(0xFF, channel)
|
||||||
|
})[1] ~= 0
|
||||||
|
end,
|
||||||
|
close = function(self, channel)
|
||||||
|
expect.expect(1, channel, "number")
|
||||||
|
expect.range(channel, 0, 65535)
|
||||||
|
self._request(0x63, {
|
||||||
|
bit.band(0xFF, bit.brshift(channel, 8)),
|
||||||
|
bit.band(0xFF, channel)
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
closeAll = function(self)
|
||||||
|
self._request(0x43)
|
||||||
|
end,
|
||||||
|
transmit = function(self, channel, replyChannel, data)
|
||||||
|
expect.expect(1, channel, "number")
|
||||||
|
expect.expect(2, replyChannel, "number")
|
||||||
|
expect.expect(3, data, "nil", "string", "number", "table")
|
||||||
|
expect.range(channel, 0, 65535)
|
||||||
|
expect.range(replyChannel, 0, 65535)
|
||||||
|
|
||||||
|
local serialized = textutils.serializeJSON(data)
|
||||||
|
expect.range(#serialized, 0, 65535)
|
||||||
|
serialized = { serialized:byte(1, 65536) }
|
||||||
|
self._request(0x54, {
|
||||||
|
bit.band(0xFF, bit.brshift(channel, 8)),
|
||||||
|
bit.band(0xFF, channel),
|
||||||
|
bit.band(0xFF, bit.brshift(replyChannel, 8)),
|
||||||
|
bit.band(0xFF, replyChannel),
|
||||||
|
bit.band(0xFF, bit.brshift(#serialized, 8)),
|
||||||
|
bit.band(0xFF, #serialized),
|
||||||
|
table.unpack(serialized, 1, #serialized)
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
isWireless = function(self) return true end,
|
||||||
|
run = function(self)
|
||||||
|
while true do
|
||||||
|
local data, binary = self._socket.receive()
|
||||||
|
if not data then return true end
|
||||||
|
if binary == false then return false, "Not a binary message" end
|
||||||
|
data = { string.byte(data, 1, #data) }
|
||||||
|
local opcode = table.remove(data, 1)
|
||||||
|
if opcode == 0x49 then -- info
|
||||||
|
local len, msg = self._read_u16ne(data)
|
||||||
|
msg = string.char(table.unpack(msg))
|
||||||
|
os.queueEvent("wsvpn:info", msg)
|
||||||
|
elseif opcode == 0x41 then -- Set address/side
|
||||||
|
local len = table.remove(data, 1)
|
||||||
|
self.side = string.char(table.unpack(data, 1, len))
|
||||||
|
elseif opcode == 0x45 then -- Error
|
||||||
|
local request_id, error_length
|
||||||
|
request_id, data = self._read_u16ne(data)
|
||||||
|
error_length, data = self._read_u16ne(data)
|
||||||
|
local message = string.char(table.unpack(data, 1, error_length))
|
||||||
|
os.queueEvent("wsvpn:response", false, request_id, message)
|
||||||
|
elseif opcode == 0x52 then -- Response
|
||||||
|
local request_id, response = self._read_u16ne(data)
|
||||||
|
os.queueEvent("wsvpn:response", true, request_id, response)
|
||||||
|
elseif opcode == 0x54 then -- Transmission
|
||||||
|
local channel, replyChannel, dataSize, packet
|
||||||
|
channel, data = self._read_u16ne(data)
|
||||||
|
replyChannel, data = self._read_u16ne(data)
|
||||||
|
dataSize, packet = self._read_u16ne(data)
|
||||||
|
os.queueEvent("modem_message", self.side or "wsmodem_0", channel, replyChannel, textutils.unserializeJSON(string.char(table.unpack(data, 1, dataSize))), nil)
|
||||||
|
else
|
||||||
|
return false, string.format("Invalid opcode 0x%02x", opcode)
|
||||||
|
end
|
||||||
|
os.sleep(0)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
-- low-level part
|
||||||
|
|
||||||
|
_read_u16ne = function(self, data)
|
||||||
|
local v = bit.blshift(table.remove(data, 1), 8)
|
||||||
|
v = bit.bor(v, table.remove(data, 1))
|
||||||
|
return v, data
|
||||||
|
end,
|
||||||
|
|
||||||
|
_wait_response = function(self, request_id)
|
||||||
|
while true do
|
||||||
|
local ev, status, id, data = os.pullEvent("wsvpn:response")
|
||||||
|
if ev == "wsvpn:response" and id == request_id then
|
||||||
|
return status, data
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
_request = function(self, opcode, data)
|
||||||
|
local request_id = self._get_id()
|
||||||
|
self._socket.send(
|
||||||
|
string.char(
|
||||||
|
opcode,
|
||||||
|
bit.band(0xFF, bit.brshift(request_id, 8)),
|
||||||
|
bit.band(0xFF, request_id),
|
||||||
|
table.unpack(data or {})
|
||||||
|
),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
local status, response = self._wait_response(request_id)
|
||||||
|
if not status then
|
||||||
|
error(response)
|
||||||
|
end
|
||||||
|
return response
|
||||||
|
end,
|
||||||
|
|
||||||
|
_get_id = function(self)
|
||||||
|
self._req_id = bit.band(0xFFFF, self._req_id + 1)
|
||||||
|
return self._req_id
|
||||||
|
end,
|
||||||
|
|
||||||
|
_send_text = function(self, code, fmt, ...)
|
||||||
|
local msg = { fmt:format(...):byte(1, 1020) }
|
||||||
|
self._socket.send(
|
||||||
|
string.char(
|
||||||
|
code,
|
||||||
|
bit.band(0xFF, bit.brshift(#msg, 8)),
|
||||||
|
bit.band(0xFF, #msg),
|
||||||
|
table.unpack(msg, 1, #msg)
|
||||||
|
),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
|
||||||
|
_init = function(self)
|
||||||
|
self._send_text(0x49, "Hello! I'm computer %d", os.getComputerID())
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
return function(addr)
|
||||||
|
local ws = assert(http.websocket(addr))
|
||||||
|
local sock = setmetatable({ _socket = ws, _req_id = 0, side = "wsmodem_unknown" }, { __index = WSModem })
|
||||||
|
for name, method in pairs(WSModem) do
|
||||||
|
sock[name] = function(...) return method(sock, ...) end
|
||||||
|
end
|
||||||
|
sock._init()
|
||||||
|
return sock
|
||||||
|
end
|
Loading…
Reference in New Issue