forked from hkc/cc-stuff
Compare commits
7 Commits
master
...
augment/de
Author | SHA1 | Date |
---|---|---|
Casey | 68717eaa04 | |
Casey | 631abc9ab9 | |
Casey | a37ba81545 | |
Casey | b3515c737f | |
Casey | 3cdeac3988 | |
Casey | 00405645f7 | |
Casey | 1dbf9b56a5 |
41
anim-web.lua
41
anim-web.lua
|
@ -1,41 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,12 +1,8 @@
|
|||
{
|
||||
"repository": "https://git.salushnes.solutions/hkc/cc-stuff/raw/branch/master/augment",
|
||||
"repository": "https://git.salushnes.solutions/hkc/cc-stuff/raw/branch/augment/dev/augment",
|
||||
"files": [
|
||||
{
|
||||
"path": "wsvpn.lua",
|
||||
"src": "wsvpn.lua"
|
||||
},
|
||||
{
|
||||
"path": "startup.lua",
|
||||
"path": "startup",
|
||||
"src": "startup.lua"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -8,8 +8,35 @@ _G.canvas3d_src.clear()
|
|||
_G.canvas3d = canvas3d_src.create()
|
||||
|
||||
_G.player = nil
|
||||
_G.nearbyEntities = {}
|
||||
_G.nearbyEntitiesByUUID = {}
|
||||
_G.surroundings = {
|
||||
entities = {},
|
||||
entitiesByUUID = {}
|
||||
}
|
||||
|
||||
table.contains = function(tbl, value)
|
||||
for k, v in pairs(tbl) do
|
||||
if v == value then return true, k end
|
||||
end
|
||||
return false, nil
|
||||
end
|
||||
|
||||
table.keys = function(tbl)
|
||||
local gen = pairs(tbl)
|
||||
local k = nil
|
||||
return function()
|
||||
k = gen(tbl, k)
|
||||
return k
|
||||
end
|
||||
end
|
||||
|
||||
table.values = function(tbl)
|
||||
local gen = pairs(tbl)
|
||||
local k, v
|
||||
return function()
|
||||
k, v = gen(tbl, k)
|
||||
return v
|
||||
end
|
||||
end
|
||||
|
||||
local function run_wrapped(func, filename)
|
||||
return function()
|
||||
|
@ -41,16 +68,16 @@ end
|
|||
|
||||
print("Loaded " .. #modules .. " modules")
|
||||
|
||||
local function safeset(func, name, old)
|
||||
local function safeget(func, name, fallback, ...)
|
||||
if func then
|
||||
local s, res = pcall(func)
|
||||
local s, res = pcall(func, ...)
|
||||
if not s then
|
||||
printError("ERR: " .. name .. " failed: " .. res)
|
||||
printError(name .. " failed: " .. res)
|
||||
else
|
||||
return res
|
||||
end
|
||||
end
|
||||
return old
|
||||
return fallback
|
||||
end
|
||||
|
||||
print("Running...")
|
||||
|
@ -68,15 +95,28 @@ end,
|
|||
function() -- Neural Interface coroutine
|
||||
print("NI routine started")
|
||||
while _G._running do
|
||||
_G.player = safeset(NI.getMetaOwner, "getMetaOwner()", _G.player)
|
||||
_G.nearbyEntities = safeset(NI.sense, "sense()", _G.nearbyEntities or {})
|
||||
_G.nearbyEntitiesByUUID = {}
|
||||
for i = 1, #_G.nearbyEntities do
|
||||
_G.nearbyEntitiesByUUID[_G.nearbyEntities[i].id] = _G.nearbyEntities[i]
|
||||
_G.player = safeget(NI.getMetaOwner, "getMetaOwner()", _G.player)
|
||||
|
||||
surroundings.entities = safeget(NI.sense, "sense()", surroundings.entities)
|
||||
|
||||
local knownUUIDs = {}
|
||||
for entity in table.values(surroundings.entities) do
|
||||
local s, res = pcall(NI.getMetaByID, entity.id)
|
||||
surroundings.entitiesByUUID[entity.id] = s and res or entity
|
||||
table.insert(knownUUIDs, entity.id)
|
||||
end
|
||||
|
||||
for uuid in table.keys(surroundings.entitiesByUUID) do
|
||||
if not table.contains(knownUUIDs, uuid) then
|
||||
surroundings.entitiesByUUID[uuid] = nil
|
||||
end
|
||||
end
|
||||
|
||||
_G.canvas3d.recenter()
|
||||
|
||||
os.sleep(0.05)
|
||||
end
|
||||
|
||||
_G.canvas3d_src.clear()
|
||||
_G.canvas2d.clear()
|
||||
end, table.unpack(modules))
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
return function()
|
||||
while _G._running do
|
||||
local ev = { os.pullEvent() }
|
||||
if ev[1] == "key" and ev[2] == keys.f4 then
|
||||
if ev[1] == "key" and ev[2] == keys.q then
|
||||
os.queueEvent("exit")
|
||||
break
|
||||
elseif ev[1] == "timer" or ev[1] == "plethora_task" then
|
||||
|
|
|
@ -1,34 +1,72 @@
|
|||
|
||||
local options = {
|
||||
reverse_projection = false,
|
||||
hpbar = {
|
||||
bg = 0x13131355,
|
||||
fg = 0xef787878,
|
||||
txt = 0xffffffff
|
||||
}
|
||||
}
|
||||
|
||||
local function entityBox(ent)
|
||||
local cube = canvas3d.addBox(ent.x - 0.25, ent.y - 0.25, ent.z - 0.25)
|
||||
local hpbar = canvas3d.addFrame({ ent.x - 0.25, ent.y + 0.25, ent.z - 0.25 })
|
||||
|
||||
cube.setAlpha(0x20)
|
||||
cube.setDepthTested(false)
|
||||
hpbar.setDepthTested(false)
|
||||
|
||||
local hp_rect_bg = hpbar.addRectangle(0, 0, 100, 15, options.hpbar.bg)
|
||||
local hp_rect_fg = hpbar.addRectangle(0, 0, 0, 15, options.hpbar.fg)
|
||||
local hp_txt = hpbar.addText({ 0, 2 }, ent.name, options.hpbar.txt)
|
||||
|
||||
return {
|
||||
_cube = cube,
|
||||
_hpbar = hpbar,
|
||||
_hp_rect_bg = hp_rect_bg,
|
||||
_hp_rect_fg = hp_rect_fg,
|
||||
_hp_txt = hp_txt,
|
||||
update = function(self, entity)
|
||||
self._cube.setPosition(entity.x - 0.25, entity.y - 0.25, entity.z - 0.25)
|
||||
self._hpbar.setPosition(entity.x, entity.y + 0.5, entity.z)
|
||||
if entity.health ~= nil and entity.maxHealth ~= nil then
|
||||
self._hp_txt.setText(string.format("%s (%.1f/%.1f)", entity.name, entity.health, entity.maxHealth))
|
||||
self._hp_rect_fg.setSize(100 * entity.health / entity.maxHealth, 15)
|
||||
else
|
||||
self._hp_txt.setText(string.format("%s", entity.name))
|
||||
self._hp_rect_fg.setSize(0, 0)
|
||||
end
|
||||
end,
|
||||
destroy = function(self)
|
||||
self._cube.remove()
|
||||
self._hpbar.remove()
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
return function()
|
||||
local cache = {}
|
||||
while _G._running do
|
||||
for id, entry in pairs(cache) do
|
||||
if nearbyEntitiesByUUID[id] == nil then
|
||||
entry.cube.remove()
|
||||
entry.frame.remove()
|
||||
cache[id] = nil
|
||||
for uuid in table.keys(cache) do
|
||||
if surroundings.entitiesByUUID[uuid] == nil then
|
||||
cache[uuid]:destroy()
|
||||
cache[uuid] = nil
|
||||
end
|
||||
end
|
||||
|
||||
for id, entity in pairs(nearbyEntitiesByUUID) do
|
||||
if id ~= player.id then
|
||||
if cache[id] == nil then
|
||||
cache[id] = {}
|
||||
cache[id].cube = canvas3d.addBox(0, 0, 0)
|
||||
cache[id].cube.setSize(0.5, 0.5, 0.5)
|
||||
cache[id].frame = canvas3d.addFrame({ 0, 0, 0 })
|
||||
cache[id].text = cache[id].frame.addText({ 0, 0 }, "")
|
||||
end
|
||||
cache[id].cube.setAlpha(0x20)
|
||||
cache[id].cube.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].frame.setPosition(entity.x, entity.y, entity.z)
|
||||
cache[id].text.setAlpha(0xFF)
|
||||
cache[id].text.setText(entity.name .. "\n" .. textutils.serialize(entity))
|
||||
cache[id].text.setColor(0xFF0000FF)
|
||||
for uuid, entity in pairs(surroundings.entitiesByUUID) do
|
||||
if uuid == player.id then
|
||||
-- nothing
|
||||
elseif cache[uuid] == nil then
|
||||
cache[uuid] = entityBox(entity)
|
||||
else
|
||||
cache[uuid]:update(entity)
|
||||
end
|
||||
end
|
||||
os.sleep(0.05)
|
||||
end
|
||||
|
||||
for uuid in table.keys(cache) do
|
||||
cache[uuid]:destroy()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
local repository = "https://git.salushnes.solutions/hkc/cc-stuff/raw/branch/master/augment/files.json"
|
||||
local repository = "https://git.salushnes.solutions/hkc/cc-stuff/raw/branch/augment/dev/augment/files.json"
|
||||
local files = textutils.unserializeJSON(http.get(repository).readAll())
|
||||
|
||||
local function getFile(url, path)
|
||||
|
|
|
@ -1,154 +0,0 @@
|
|||
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
|
|
@ -1,28 +0,0 @@
|
|||
#!/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}")
|
BIN
badapple.bin
BIN
badapple.bin
Binary file not shown.
|
@ -1,99 +0,0 @@
|
|||
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
|
||||
|
231
bigterm.lua
231
bigterm.lua
|
@ -1,231 +0,0 @@
|
|||
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
|
26
btccshow.lua
26
btccshow.lua
|
@ -1,26 +0,0 @@
|
|||
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
|
|
@ -1,47 +0,0 @@
|
|||
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
|
412
cc-pic.py
412
cc-pic.py
|
@ -1,412 +0,0 @@
|
|||
#!/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()
|
129
ccpi.lua
129
ccpi.lua
|
@ -1,129 +0,0 @@
|
|||
|
||||
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
|
||||
}
|
17
ccshow.lua
17
ccshow.lua
|
@ -1,17 +0,0 @@
|
|||
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)
|
|
@ -1,58 +0,0 @@
|
|||
#!/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.
|
@ -1,21 +0,0 @@
|
|||
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
|
71
dh/dh.lua
71
dh/dh.lua
|
@ -1,71 +0,0 @@
|
|||
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)
|
1764
dh/ecc.lua
1764
dh/ecc.lua
File diff suppressed because it is too large
Load Diff
15
events.lua
15
events.lua
|
@ -1,15 +0,0 @@
|
|||
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
|
|
@ -1,75 +0,0 @@
|
|||
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)
|
906
img2cpi.c
906
img2cpi.c
|
@ -1,906 +0,0 @@
|
|||
// 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, },
|
||||
};
|
|
@ -1,43 +0,0 @@
|
|||
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
|
|
@ -1,42 +0,0 @@
|
|||
|
||||
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")
|
637
mess/tmpc.lua
637
mess/tmpc.lua
|
@ -1,637 +0,0 @@
|
|||
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!")
|
62
nfpview.py
62
nfpview.py
|
@ -1,62 +0,0 @@
|
|||
#!/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()
|
99
obcb-cc.lua
99
obcb-cc.lua
|
@ -1,99 +0,0 @@
|
|||
|
||||
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()
|
|
@ -1,43 +0,0 @@
|
|||
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
|
30
stream.lua
30
stream.lua
|
@ -1,6 +1,6 @@
|
|||
local args = { ... }
|
||||
|
||||
local dfpwm = require("cc.audio.dfpwm")
|
||||
local buffer_size = 8192
|
||||
|
||||
if not http then
|
||||
print("no http, check config")
|
||||
|
@ -21,35 +21,21 @@ if not req then
|
|||
return
|
||||
end
|
||||
|
||||
local headers = req.getResponseHeaders()
|
||||
local length = tonumber(headers["Content-Length"]) or 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
|
||||
for i = 1, buffer_size do
|
||||
buffer[i] = 0
|
||||
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
|
||||
local chunk = req.read(16384)
|
||||
local chunk = req.read(buffer_size)
|
||||
if not chunk then
|
||||
break
|
||||
end
|
||||
|
||||
local buffer = decode(chunk)
|
||||
buffer = {}
|
||||
for i = 1, #chunk do
|
||||
buffer[i] = string.byte(chunk, i) - 128
|
||||
end
|
||||
while not speaker.playAudio(buffer) do
|
||||
os.pullEvent("speaker_audio_empty")
|
||||
end
|
||||
end
|
||||
|
||||
print()
|
||||
|
|
|
@ -1,143 +0,0 @@
|
|||
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)
|
|
@ -1,82 +0,0 @@
|
|||
#!/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)")
|
91
tape-rip.lua
91
tape-rip.lua
|
@ -1,91 +0,0 @@
|
|||
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()
|
112
tapeget.lua
112
tapeget.lua
|
@ -1,123 +1,49 @@
|
|||
local args = { ... }
|
||||
|
||||
local seekTo = 0
|
||||
local driveName = nil
|
||||
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
|
||||
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())
|
||||
local tape = peripheral.find("tape_drive")
|
||||
if not tape then
|
||||
printError("wrong name")
|
||||
print("tape where")
|
||||
return
|
||||
end
|
||||
else
|
||||
tape = drives[1]
|
||||
end
|
||||
end
|
||||
|
||||
if not http then
|
||||
print("no http, check config")
|
||||
return
|
||||
end
|
||||
|
||||
local req, err = http.get(args[1], {}, true)
|
||||
tape.stop()
|
||||
tape.seek(-tape.getSize())
|
||||
tape.stop()
|
||||
|
||||
local req = http.get(args[1], {}, true)
|
||||
if not req then
|
||||
print("oopsie: "..err)
|
||||
print("oopsie")
|
||||
return
|
||||
end
|
||||
|
||||
local headers = req.getResponseHeaders()
|
||||
local length = tonumber(headers["content-length"]) or 1
|
||||
local length = headers["content-length"]
|
||||
|
||||
if length > tape.getSize() then
|
||||
printError("Tape is smaller than the file you're trying to write")
|
||||
printError("Are you sure?")
|
||||
|
||||
io.write("Write anyways? [y/N]: ")
|
||||
local r = read()
|
||||
if r ~= "y" and r ~= "Y" then
|
||||
return
|
||||
end
|
||||
local function n_to_kib(value)
|
||||
return string.format("%6.1f kiB", value / 1024)
|
||||
end
|
||||
|
||||
tape.stop()
|
||||
tape.seek(-tape.getSize())
|
||||
tape.seek(seekTo)
|
||||
tape.stop()
|
||||
|
||||
local written = 1
|
||||
local _, y = term.getCursorPos()
|
||||
local written = 0
|
||||
|
||||
local i = 1
|
||||
while true do
|
||||
local chunk = req.read(256)
|
||||
local chunk = req.read()
|
||||
if not chunk then
|
||||
print("EOF")
|
||||
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))
|
||||
written = written + 1
|
||||
if (written % 8192) == 0 then
|
||||
os.sleep(0.01)
|
||||
end
|
||||
end
|
||||
term.setCursorPos(1, y)
|
||||
term.clearLine()
|
||||
print("Written "..n_to_kib(written))
|
||||
term.write(n_to_kib(written) .. " / " .. n_to_kib(length) .. string.format(" %7.3f%%", 100 * written / length))
|
||||
end
|
||||
end
|
||||
|
||||
tape.stop()
|
||||
tape.seek(-tape.getSize())
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
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
|
||||
)
|
|
@ -1,60 +0,0 @@
|
|||
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)
|
|
@ -1,57 +0,0 @@
|
|||
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")
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"ver": "0.0.3",
|
||||
"files": [
|
||||
{ "src": "/startup.lua", "dst": "/startup.lua" },
|
||||
{ "src": "/main.lua", "dst": "/main.lua" }
|
||||
]
|
||||
}
|
275
video.lua
275
video.lua
|
@ -1,275 +0,0 @@
|
|||
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))
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
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()
|
385
wsvpn.c
385
wsvpn.c
|
@ -1,385 +0,0 @@
|
|||
// 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;
|
||||
}
|
154
wsvpn.lua
154
wsvpn.lua
|
@ -1,154 +0,0 @@
|
|||
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