Compare commits
7 Commits
master
...
augment/de
Author | SHA1 | Date |
---|---|---|
Casey | 68717eaa04 | |
Casey | 631abc9ab9 | |
Casey | a37ba81545 | |
Casey | b3515c737f | |
Casey | 3cdeac3988 | |
Casey | 00405645f7 | |
Casey | 1dbf9b56a5 |
|
@ -1,8 +0,0 @@
|
|||
img2cpi
|
||||
img2cpi.o
|
||||
wsvpn
|
||||
wsvpn.o
|
||||
cpi2png
|
||||
cc-common.o
|
||||
vim.state
|
||||
__pycache__
|
|
@ -1,6 +0,0 @@
|
|||
[submodule "dependencies/stb"]
|
||||
path = dependencies/stb
|
||||
url = https://github.com/nothings/stb
|
||||
[submodule "dependencies/mongoose"]
|
||||
path = dependencies/mongoose
|
||||
url = https://github.com/cesanta/mongoose
|
24
Makefile
|
@ -1,24 +0,0 @@
|
|||
CPPFLAGS += -Idependencies -Idependencies/mongoose
|
||||
LDLIBS += -lm
|
||||
|
||||
all: img2cpi cpi2png wsvpn
|
||||
|
||||
test-cpi2png: cpi2png
|
||||
./cpi2png ./cpi-images/rat.cpi /tmp/rat.png
|
||||
|
||||
img2cpi: img2cpi.c cc-common.o dependencies/stb/stb_image.o dependencies/stb/stb_image_resize2.o
|
||||
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o "$@" $(CPPFLAGS)
|
||||
|
||||
cpi2png: cpi2png.c cc-common.o dependencies/stb/stb_image_write.o
|
||||
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o "$@" $(CPPFLAGS)
|
||||
|
||||
dependencies/stb/%.o: dependencies/stb/%.h
|
||||
$(CC) -DSTB_IMAGE_IMPLEMENTATION -DSTB_IMAGE_RESIZE_IMPLEMENTATION -DSTB_IMAGE_WRITE_IMPLEMENTATION -x c $^ -c -o "$@"
|
||||
|
||||
wsvpn: wsvpn.o dependencies/mongoose/mongoose.o
|
||||
$(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o "$@"
|
||||
|
||||
clean:
|
||||
$(RM) -v img2cpi cpi2png wsvpn wsvpn.o cc-common.o dependencies/stb/*.o
|
||||
|
||||
.PHONY: all
|
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
|
@ -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
|
@ -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
|
@ -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
|
327
cc-common.c
|
@ -1,327 +0,0 @@
|
|||
#include "cc-common.h"
|
||||
|
||||
int read_varint(FILE *fp, unsigned int *out) {
|
||||
int position = 0;
|
||||
|
||||
while (true) {
|
||||
unsigned char curr = fgetc(fp);
|
||||
*out |= (curr & 0x7F) << position;
|
||||
|
||||
if ((curr & 0x80) == 0) break;
|
||||
|
||||
position += 7;
|
||||
|
||||
if (position >= 32) return -position / 7;
|
||||
}
|
||||
|
||||
return (position + 7) / 7;
|
||||
}
|
||||
|
||||
int write_varint(FILE *fp, unsigned int in) {
|
||||
unsigned mask = 0xFFFFFF80;
|
||||
int written = 0;
|
||||
while (true) {
|
||||
if ((in & mask) == 0) {
|
||||
fputc(in & 0xff, fp);
|
||||
return written + 1;
|
||||
}
|
||||
fputc((in & 0x7F) | 0x80, fp);
|
||||
written++;
|
||||
in >>= 7;
|
||||
}
|
||||
}
|
||||
|
||||
const struct palette cc_default_palette = PALETTE(
|
||||
{ { 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 } }
|
||||
), cc_default_gray_palette = PALETTE(
|
||||
{ { 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 GlyphBitmap cc_font_atlas[256] = {
|
||||
{ 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, },
|
||||
};
|
31
cc-common.h
|
@ -1,31 +0,0 @@
|
|||
#ifndef _CC_COMMON_H_
|
||||
#define _CC_COMMON_H_
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef uint8_t GlyphBitmap[11];
|
||||
|
||||
struct rgba { uint8_t r, g, b, a; };
|
||||
union color {
|
||||
struct rgba rgba;
|
||||
uint32_t v;
|
||||
};
|
||||
|
||||
struct palette {
|
||||
const uint8_t count;
|
||||
union color colors[] __attribute__((counted_by(count)));
|
||||
};
|
||||
|
||||
#define LENGTHOF(...) (sizeof(__VA_ARGS__) / sizeof(*(__VA_ARGS__)))
|
||||
#define PALETTE(...) { .count = LENGTHOF((union color[]){__VA_ARGS__}), .colors = {__VA_ARGS__} }
|
||||
|
||||
const extern GlyphBitmap cc_font_atlas[256];
|
||||
const extern struct palette cc_default_palette, cc_default_gray_palette;
|
||||
|
||||
|
||||
int read_varint(FILE *fp, unsigned int *out);
|
||||
int write_varint(FILE *fp, unsigned int in);
|
||||
|
||||
#endif
|
415
cc-pic.py
|
@ -1,415 +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 = [
|
||||
0xf0, 0xf0, 0xf0,
|
||||
0xf2, 0xb2, 0x33,
|
||||
0xe5, 0x7f, 0xd8,
|
||||
0x99, 0xb2, 0xf2,
|
||||
|
||||
0xde, 0xde, 0x6c,
|
||||
0x7f, 0xcc, 0x19,
|
||||
0xf2, 0xb2, 0xcc,
|
||||
0x4c, 0x4c, 0x4c,
|
||||
|
||||
0x99, 0x99, 0x99,
|
||||
0x4c, 0x99, 0xb2,
|
||||
0xb2, 0x66, 0xe5,
|
||||
0x33, 0x66, 0xcc,
|
||||
|
||||
0x7f, 0x66, 0x4c,
|
||||
0x57, 0xa6, 0x4e,
|
||||
0xcc, 0x4c, 0x4c,
|
||||
0x11, 0x11, 0x11
|
||||
]
|
||||
|
||||
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()
|
149
ccpi.lua
|
@ -1,149 +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
|
||||
|
||||
decoders[128] = function(image, fp)
|
||||
image.w = read_varint(fp)
|
||||
image.h = read_varint(fp)
|
||||
image.extras.n_frames = read_varint(fp)
|
||||
read_palette_full(image.palette, fp)
|
||||
local success, err = read_pixeldata_v0(image, fp)
|
||||
if not success then return false, err end
|
||||
image.extras.frames = {}
|
||||
for i = 1, image.extras.n_frames - 1 do
|
||||
local frame = { palette = {}, lines = {} }
|
||||
frame.w = image.w
|
||||
frame.h = image.h
|
||||
frame.scale = image.scale
|
||||
read_palette_full(frame.palette)
|
||||
local success, err = read_pixeldata_v0(frame, fp)
|
||||
if not success then return false, err end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function parse(fp)
|
||||
local res
|
||||
local image = { w = 0, h = 0, scale = 1.0, palette = {}, lines = {}, extras = {} }
|
||||
|
||||
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
|
@ -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,39 +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}"
|
||||
|
||||
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
|
||||
|
||||
ls frame*.png | parallel 'echo {}; python3 ${ORIG}/cc-pic.py -W 164 -H 81 -p cover {} ${OUTPUT}/{.}.cpi'
|
||||
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"
|
||||
|
Before Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 94 KiB |
Before Width: | Height: | Size: 107 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 52 KiB |
91
cpi2png.c
|
@ -1,91 +0,0 @@
|
|||
// x-run: make test-cpi2png
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stb/stb_image.h>
|
||||
#include <stb/stb_image_write.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cc-common.h"
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 3) {
|
||||
fprintf(stderr, "Usage: %s [input.cpi] [output.png]\n", argv[0]);
|
||||
}
|
||||
|
||||
FILE *fp_in = fopen(argv[1], "rb");
|
||||
assert(fp_in != NULL && "Failed to open input file");
|
||||
|
||||
unsigned char header[4];
|
||||
|
||||
unsigned char version = 0;
|
||||
assert(fread(header, 1, 4, fp_in) == 4 && "Failed to read header: not enough bytes");
|
||||
if (0 == memcmp(header, "CCPI", 4)) { // Original CCPI (CPIv0)
|
||||
version = 0;
|
||||
} else if (0 == memcmp(header, "CPI", 3)) { // Newer CCPI (CPIvX)
|
||||
version = header[3];
|
||||
} else {
|
||||
assert(false && "Not a CPI/CCPI image: invalid header");
|
||||
}
|
||||
|
||||
if (version & 0x80) {
|
||||
fprintf(stderr, "Draft version: 0x%02x may not be supported properly! Here be dragons!\n", version);
|
||||
}
|
||||
|
||||
unsigned int width = 0, height = 0;
|
||||
|
||||
if (version == 0) {
|
||||
width = fgetc(fp_in);
|
||||
height = fgetc(fp_in);
|
||||
(void)fgetc(fp_in); // XXX: ignore scale
|
||||
} else if (version == 1) {
|
||||
assert(read_varint(fp_in, &width) > 0 && "Failed to read width varint");
|
||||
assert(read_varint(fp_in, &height) > 0 && "Failed to read height varint");
|
||||
} else {
|
||||
assert(false && "Failed to read size: unsupported version");
|
||||
}
|
||||
|
||||
union color *canvas = malloc(width * height * 6 * 9 * sizeof(union color));
|
||||
|
||||
|
||||
// XXX: may change in future when we introduce variable-size palettes
|
||||
// though, it may never change, if I'm being honest. Why would I choose
|
||||
// worse image quality with less colors when I can use all of them?
|
||||
union color colors[16] = { 0 };
|
||||
|
||||
// NOTE: our `union color` type is 4 bytes long, while palette stored in the
|
||||
// file itself uses 3 bytes per color, so we can't just `fread` them at once,
|
||||
// sadly.
|
||||
for (int i = 0; i < 16; i++) {
|
||||
colors[i].rgba.r = fgetc(fp_in);
|
||||
colors[i].rgba.g = fgetc(fp_in);
|
||||
colors[i].rgba.b = fgetc(fp_in);
|
||||
colors[i].rgba.a = 0xff;
|
||||
}
|
||||
|
||||
unsigned char *buffer = calloc(width * height, 2);
|
||||
fread(buffer, 2, width * height, fp_in);
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
unsigned char sym = buffer[(x + y * width) * 2];
|
||||
unsigned char color = buffer[(x + y * width) * 2 + 1];
|
||||
union color background = colors[color & 0xF];
|
||||
union color foreground = colors[color >> 4];
|
||||
for (int oy = 0; oy < 9; oy++) {
|
||||
for (int ox = 0; ox < 6; ox++) {
|
||||
union color pix = ((0x80 >> (ox + 1)) & cc_font_atlas[sym][oy + 1]) ? foreground : background;
|
||||
canvas[ox + (x + (y * 9 + oy) * width) * 6] = pix;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stbi_write_png(argv[2], width * 6, height * 9, 4, canvas, 0);
|
||||
|
||||
free(canvas);
|
||||
fclose(fp_in);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 3525f044f551816dc1469f445fc16b94d51a1e78
|
|
@ -1 +0,0 @@
|
|||
Subproject commit f75e8d1cad7d90d72ef7a4661f1b994ef78b4e31
|
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
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)
|
887
img2cpi.c
|
@ -1,887 +0,0 @@
|
|||
// x-run: make img2cpi CC=clang
|
||||
// x-run: ~/scripts/runc.sh % -Wall -Wextra -lm --- ~/images/boykisser.png cpi-images/boykisser.cpi
|
||||
#include <math.h>
|
||||
#include <assert.h>
|
||||
#include <stb/stb_image.h>
|
||||
#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>
|
||||
#include "cc-common.h"
|
||||
#ifdef USE_OPENMP
|
||||
#include <omp.h>
|
||||
#endif
|
||||
|
||||
#define MAX_COLOR_DIFFERENCE 768
|
||||
#define K_MEANS_ITERATIONS 4
|
||||
#define PROGRESS_BAR_WIDTH 24
|
||||
|
||||
#define TOSTRNAME(M) #M
|
||||
#define TOSTR(M) TOSTRNAME(M)
|
||||
|
||||
struct cc_char {
|
||||
unsigned char character;
|
||||
unsigned char bg, fg;
|
||||
};
|
||||
|
||||
struct arguments {
|
||||
bool fast_mode;
|
||||
bool verbose;
|
||||
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,
|
||||
.verbose = 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_AUTO
|
||||
};
|
||||
|
||||
struct image {
|
||||
int w, h;
|
||||
union color *pixels;
|
||||
};
|
||||
|
||||
struct image_pal {
|
||||
int w, h;
|
||||
uint8_t *pixels;
|
||||
const struct palette *palette;
|
||||
};
|
||||
|
||||
struct k_means_state {
|
||||
const struct image *items;
|
||||
struct palette *clusters;
|
||||
uint8_t *predicted_cluster;
|
||||
struct k_means_centroid_intermediate {
|
||||
struct {
|
||||
float r, g, b;
|
||||
} sums;
|
||||
size_t count;
|
||||
union color closest_present_item;
|
||||
float closest_present_distance;
|
||||
} *centroid_intermediate;
|
||||
size_t item_count;
|
||||
};
|
||||
|
||||
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 struct palette *palette);
|
||||
void image_unload(struct image *img);
|
||||
|
||||
float get_color_difference(union color a, union color b);
|
||||
float get_color_brightness(union color clr);
|
||||
|
||||
void get_size_keep_aspect(int w, int h, int dw, int dh, int *ow, int *oh);
|
||||
|
||||
void convert_2x3(const struct image_pal *img, struct cc_char *characters);
|
||||
void convert_6x9(const struct image_pal *img, struct cc_char *characters);
|
||||
|
||||
int save_cpi_0(FILE *fp, const struct palette *pal, const struct cc_char *chars, int w, int h);
|
||||
int save_cpi_1(FILE *fp, const struct palette *pal, const struct cc_char *chars, int w, int h);
|
||||
|
||||
// Only one global custom palette is maintained
|
||||
struct palette *custom_palette_resize(uint8_t size);
|
||||
struct palette *custom_palette_from(const struct palette *orig);
|
||||
|
||||
struct k_means_state k_means_init(const struct image *image, struct palette *starting_palette);
|
||||
bool k_means_iteration(struct k_means_state *state);
|
||||
void k_means_end(struct k_means_state *state);
|
||||
struct palette *palette_k_means(const struct image *image, const struct palette *prototype);
|
||||
|
||||
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 },
|
||||
{ 'v', "verbose", 0, "Increase verbosity", 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 }
|
||||
};
|
||||
|
||||
static char progress_bar[PROGRESS_BAR_WIDTH];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (args.verbose) {
|
||||
memset(progress_bar, '#', PROGRESS_BAR_WIDTH);
|
||||
printf("Input image: %dx%d\n", src_image->w, src_image->h);
|
||||
}
|
||||
|
||||
struct image *canvas;
|
||||
if (args.fast_mode) {
|
||||
canvas = image_new(args.width * 2, args.height * 3);
|
||||
} else {
|
||||
canvas = image_new(args.width * 6, args.height * 9);
|
||||
}
|
||||
|
||||
if (!canvas) {
|
||||
fprintf(stderr, "Error: failed to allocate second image buffer\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (args.verbose) {
|
||||
printf("Output image canvas: %dx%d\n", canvas->w, canvas->h);
|
||||
}
|
||||
|
||||
// TODO: load palette, maybe calculate it too? k-means?
|
||||
const struct palette *palette = &cc_default_palette;
|
||||
switch (args.palette_type) {
|
||||
case PALETTE_DEFAULT: palette = &cc_default_palette; break;
|
||||
case PALETTE_DEFAULT_GRAY: palette = &cc_default_gray_palette; break;
|
||||
case PALETTE_AUTO: palette = palette_k_means(src_image, &cc_default_palette); break;
|
||||
case PALETTE_LIST: assert(0 && "Not implemented"); break; // TODO
|
||||
case PALETTE_PATH: assert(0 && "Not implemented"); break; // TODO
|
||||
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);
|
||||
|
||||
if (args.verbose) {
|
||||
printf("Scaling down to: %dx%d\n", 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);
|
||||
if (!quantized_image) {
|
||||
fprintf(stderr, "Error: failed to open the file\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (args.verbose) {
|
||||
printf("Converting image ");
|
||||
}
|
||||
|
||||
if (args.fast_mode) {
|
||||
if (args.verbose) {
|
||||
printf(" using fast method\n");
|
||||
}
|
||||
convert_2x3(quantized_image, characters);
|
||||
} else {
|
||||
if (args.verbose) {
|
||||
printf(" using slow method\n");
|
||||
}
|
||||
convert_6x9(quantized_image, characters);
|
||||
}
|
||||
|
||||
if (args.verbose) {
|
||||
printf("Conversion done, saving image ");
|
||||
}
|
||||
|
||||
FILE *fp = fopen(args.output_path, "wb");
|
||||
if (args.width < 256 && args.height < 256) {
|
||||
printf(" using cpiv0\n");
|
||||
save_cpi_0(fp, palette, characters, args.width, args.height);
|
||||
} else {
|
||||
printf(" using cpiv1\n");
|
||||
save_cpi_1(fp, palette, characters, args.width, args.height);
|
||||
}
|
||||
fclose(fp);
|
||||
|
||||
image_unload(src_image);
|
||||
image_unload(canvas);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
int _write_palette_full(FILE *fp, const struct palette *pal) {
|
||||
int written = 0;
|
||||
assert(pal->count == 16 && "Invalid palette size");
|
||||
for (int i = 0; i < 16; i++) {
|
||||
written += fputc(pal->colors[i].rgba.r, fp);
|
||||
written += fputc(pal->colors[i].rgba.g, fp);
|
||||
written += fputc(pal->colors[i].rgba.b, fp);
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
int _write_pixeldata_v0(FILE *fp, const struct cc_char *chars, int w, int h) {
|
||||
int written = 0;
|
||||
for (int i = 0; i < w * h; i++) {
|
||||
written += fputc(chars[i].character, fp);
|
||||
written += fputc(chars[i].bg | (chars[i].fg << 4), fp);
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
int save_cpi_0(FILE *fp, const struct palette *pal, const struct cc_char *chars, int w, int h) {
|
||||
int written = 0;
|
||||
written += fwrite("CCPI", 1, 4, fp);
|
||||
written += fputc(w, fp);
|
||||
written += fputc(h, fp);
|
||||
written += fputc(0x00, fp);
|
||||
written += _write_palette_full(fp, pal);
|
||||
written += _write_pixeldata_v0(fp, chars, w, h);
|
||||
return written;
|
||||
}
|
||||
|
||||
int save_cpi_1(FILE *fp, const struct palette *pal, const struct cc_char *chars, int w, int h) {
|
||||
int written = 0;
|
||||
written += fwrite("CPI\x01", 1, 4, fp);
|
||||
written += write_varint(fp, w);
|
||||
written += write_varint(fp, h);
|
||||
written += _write_palette_full(fp, pal);
|
||||
written += _write_pixeldata_v0(fp, chars, w, h);
|
||||
return written;
|
||||
}
|
||||
|
||||
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, "hvfW: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 'v': // --verbose
|
||||
args.verbose = true;
|
||||
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 struct palette *palette) {
|
||||
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);
|
||||
out->palette = palette;
|
||||
|
||||
for (int i = 0; i < out->w * out->h; i++) {
|
||||
int closest_color = 0;
|
||||
float closest_distance = 1e20;
|
||||
for (int color = 0; color < palette->count; color++) {
|
||||
float dist = get_color_difference(palette->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 });
|
||||
}
|
||||
|
||||
void convert_2x3(const struct image_pal *img, struct cc_char *characters) {
|
||||
int w = img->w / 2, h = img->h / 3;
|
||||
for (int y = 0; y < h; y++) {
|
||||
if (args.verbose) {
|
||||
int sz = PROGRESS_BAR_WIDTH - (y * PROGRESS_BAR_WIDTH / h);
|
||||
printf("\r[%-" TOSTR(PROGRESS_BAR_WIDTH) ".*s|%7.3f%%|%4d/%4d]",
|
||||
PROGRESS_BAR_WIDTH - sz, progress_bar + sz,
|
||||
100.0 * (y + 1) / h,
|
||||
y + 1, h);
|
||||
fflush(stdout);
|
||||
}
|
||||
for (int x = 0; x < w; 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 = img->pixels[ox + (x + (y * 3 + oy) * w) * 2];
|
||||
float brightness = get_color_brightness(img->palette->colors[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 = img->pixels[ox + (x + (y * 3 + oy) * w) * 2];
|
||||
float diff_bg = get_color_difference(img->palette->colors[darkest_i], img->palette->colors[pix]);
|
||||
float diff_fg = get_color_difference(img->palette->colors[brightest_i], img->palette->colors[pix]);
|
||||
if (diff_fg < diff_bg) {
|
||||
bitmap |= pixel_bits[oy][ox];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
unsigned char pix = img->pixels[1 + (x + (y * 3 + 2) * w) * 2];
|
||||
float diff_bg = get_color_difference(img->palette->colors[darkest_i], img->palette->colors[pix]);
|
||||
float diff_fg = get_color_difference(img->palette->colors[brightest_i], img->palette->colors[pix]);
|
||||
if (diff_fg < diff_bg) {
|
||||
bitmap ^= 31;
|
||||
unsigned char tmp = darkest_i;
|
||||
darkest_i = brightest_i;
|
||||
brightest_i = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
characters[x + y * w].character = 0x80 + bitmap;
|
||||
characters[x + y * w].bg = darkest_i;
|
||||
characters[x + y * w].fg = brightest_i;
|
||||
}
|
||||
}
|
||||
if (args.verbose) {
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
|
||||
void convert_6x9(const struct image_pal *img, struct cc_char *characters) {
|
||||
int w = img->w / 6, h = img->h / 9;
|
||||
float palette_self_diffs[0x100][0x10] = {{(float) 0xffffff}};
|
||||
for (int input_color = 0x0; input_color < 0x100 && input_color < img->palette->count; input_color++) {
|
||||
for (int output_color = 0x0; output_color < 0x10 && output_color < img->palette->count; output_color++) {
|
||||
palette_self_diffs[input_color][output_color] = get_color_difference(img->palette->colors[input_color], img->palette->colors[output_color]);
|
||||
}
|
||||
}
|
||||
|
||||
for (int y = 0; y < h; y++) {
|
||||
if (args.verbose) {
|
||||
int sz = PROGRESS_BAR_WIDTH - (y * PROGRESS_BAR_WIDTH / h);
|
||||
printf("\r[%-" TOSTR(PROGRESS_BAR_WIDTH) ".*s|%7.3f%%|%4d/%4d]",
|
||||
PROGRESS_BAR_WIDTH - sz, progress_bar + sz,
|
||||
100.0 * (y + 1) / h,
|
||||
y + 1, h);
|
||||
fflush(stdout);
|
||||
}
|
||||
#ifdef USE_OPENMP
|
||||
#pragma omp parallel for
|
||||
#endif
|
||||
for (int x = 0; x < w; x++) {
|
||||
float chunk_palette_diffs[6][9][0x10] = {{{(float) 0xffffff}}};
|
||||
for (int ox = 0; ox < 6; ox++) {
|
||||
for (int oy = 0; oy < 9; oy++) {
|
||||
uint8_t pixel_unresolved = img->pixels[
|
||||
ox + (x + (y * 9 + oy) * w) * 6
|
||||
];
|
||||
for (int color = 0x0; color < 0x10 && color < img->palette->count; color++) {
|
||||
chunk_palette_diffs[ox][oy][color] = palette_self_diffs[pixel_unresolved][color];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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++) {
|
||||
float difference = 0;
|
||||
for (int oy = 0; oy < 9; oy++) {
|
||||
unsigned char sym_line = cc_font_atlas[sym][oy];
|
||||
for (int ox = 0; ox < 6; ox++) {
|
||||
bool lit = sym_line & (0x80 >> ox);
|
||||
difference += chunk_palette_diffs[ox][oy][lit ? color >> 4 : color & 0xF];
|
||||
}
|
||||
}
|
||||
if (difference <= min_diff) {
|
||||
min_diff = difference;
|
||||
closest_sym = sym;
|
||||
closest_color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
characters[x + y * w].character = closest_sym;
|
||||
characters[x + y * w].bg = closest_color & 0xF;
|
||||
characters[x + y * w].fg = closest_color >> 4;
|
||||
}
|
||||
}
|
||||
if (args.verbose) {
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
|
||||
struct {
|
||||
uint8_t count;
|
||||
union color colors[256];
|
||||
} custom_palette_data;
|
||||
|
||||
struct palette *custom_palette_resize(uint8_t size) {
|
||||
custom_palette_data.count = size;
|
||||
return (struct palette*)&custom_palette_data;
|
||||
}
|
||||
|
||||
struct palette *custom_palette_from(const struct palette *orig) {
|
||||
custom_palette_data.count = orig->count;
|
||||
for (int i = 0; i < custom_palette_data.count; i++) {
|
||||
custom_palette_data.colors[i] = orig->colors[i];
|
||||
}
|
||||
return (struct palette*)&custom_palette_data;
|
||||
}
|
||||
|
||||
struct k_means_state k_means_init(const struct image *image, struct palette *starting_palette) {
|
||||
size_t item_count = image->w * image->h;
|
||||
uint8_t cluster_count = starting_palette->count;
|
||||
struct k_means_state state = {
|
||||
.items = image,
|
||||
.clusters = starting_palette,
|
||||
.predicted_cluster = calloc(image->w, image->h),
|
||||
.centroid_intermediate = calloc(cluster_count, sizeof(struct k_means_centroid_intermediate)),
|
||||
.item_count = item_count,
|
||||
};
|
||||
if (state.centroid_intermediate) {
|
||||
for (size_t i = 0; i < cluster_count; i++) {
|
||||
state.centroid_intermediate[i].closest_present_item = starting_palette->colors[i];
|
||||
state.centroid_intermediate[i].closest_present_distance = 1e20;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
bool k_means_iteration(struct k_means_state *state) {
|
||||
if (!state->predicted_cluster || !state->centroid_intermediate) {
|
||||
return false;
|
||||
}
|
||||
bool changed = false;
|
||||
|
||||
// Find closest cluster
|
||||
for (int i = 0; i < state->item_count; i++) {
|
||||
int closest_cluster = 0;
|
||||
float closest_distance = 1e20;
|
||||
for (int cluster = 0; cluster < state->clusters->count; cluster++) {
|
||||
union color item = state->items->pixels[i];
|
||||
float dist = get_color_difference(state->clusters->colors[cluster], item);
|
||||
if (dist <= closest_distance) {
|
||||
closest_distance = dist;
|
||||
closest_cluster = cluster;
|
||||
}
|
||||
if (dist < state->centroid_intermediate[cluster].closest_present_distance) {
|
||||
bool can_update = true;
|
||||
for (int other_cluster = 0; other_cluster < state->clusters->count; other_cluster++) {
|
||||
if (other_cluster == cluster) {
|
||||
continue;
|
||||
}
|
||||
if (state->centroid_intermediate[other_cluster].closest_present_item.v == item.v) {
|
||||
can_update = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (can_update) {
|
||||
state->centroid_intermediate[cluster].closest_present_item = item;
|
||||
state->centroid_intermediate[cluster].closest_present_distance = dist;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!changed) {
|
||||
changed = state->predicted_cluster[i] != closest_cluster;
|
||||
}
|
||||
state->predicted_cluster[i] = closest_cluster;
|
||||
state->centroid_intermediate[closest_cluster].count += 1;
|
||||
state->centroid_intermediate[closest_cluster].sums.r += state->items->pixels[i].rgba.r;
|
||||
state->centroid_intermediate[closest_cluster].sums.g += state->items->pixels[i].rgba.g;
|
||||
state->centroid_intermediate[closest_cluster].sums.b += state->items->pixels[i].rgba.b;
|
||||
}
|
||||
|
||||
// Update centroids
|
||||
for (int i = 0; i < state->clusters->count; ++i) {
|
||||
struct k_means_centroid_intermediate intermediate = state->centroid_intermediate[i];
|
||||
if (intermediate.count) {
|
||||
union color centroid = {
|
||||
.rgba = {
|
||||
.r = intermediate.sums.r / intermediate.count,
|
||||
.g = intermediate.sums.g / intermediate.count,
|
||||
.b = intermediate.sums.b / intermediate.count,
|
||||
.a = 0xff,
|
||||
}
|
||||
};
|
||||
if (!changed) {
|
||||
changed = state->clusters->colors[i].v != centroid.v;
|
||||
}
|
||||
state->clusters->colors[i] = centroid;
|
||||
} else {
|
||||
// No pixels are closest to this color
|
||||
// Warp the centroid onto the closest item
|
||||
state->clusters->colors[i] = intermediate.closest_present_item;
|
||||
}
|
||||
state->centroid_intermediate[i] = (struct k_means_centroid_intermediate) { .sums = {0, 0, 0}, .count = 0, .closest_present_item = state->clusters->colors[i], .closest_present_distance = 1e20 };
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
void k_means_end(struct k_means_state *state) {
|
||||
if (state->predicted_cluster) {
|
||||
free(state->predicted_cluster);
|
||||
}
|
||||
if (state->centroid_intermediate) {
|
||||
free(state->centroid_intermediate);
|
||||
}
|
||||
}
|
||||
|
||||
struct palette *palette_k_means(const struct image *image, const struct palette *prototype) {
|
||||
if (!prototype) {
|
||||
prototype = &cc_default_palette;
|
||||
}
|
||||
struct palette *palette = custom_palette_from(prototype);
|
||||
|
||||
struct k_means_state state = k_means_init(image, palette);
|
||||
for (int i = 0; i < K_MEANS_ITERATIONS; i++) {
|
||||
if (!k_means_iteration(&state)) {
|
||||
fprintf(stderr, "early k-means stop at iteration %d\n", i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
k_means_end(&state);
|
||||
|
||||
return palette;
|
||||
}
|
BIN
mess/avg.png
Before Width: | Height: | Size: 131 B |
|
@ -1,82 +0,0 @@
|
|||
local args = { ... }
|
||||
local ccpi = require("ccpi")
|
||||
|
||||
local bit = {
|
||||
band = function(a, b) return a & b end,
|
||||
bor = function(a, b) return a | b end,
|
||||
blshift = function(a, b) return a << b end,
|
||||
brshift = function(a, b) return a >> b end,
|
||||
}
|
||||
|
||||
local function write_varint(fp, value)
|
||||
value = bit.band(value, 0xFFFFFFFF)
|
||||
mask = 0xFFFFFF80
|
||||
while true do
|
||||
if bit.band(value, mask) == 0 then
|
||||
fp:write(string.char(bit.band(value, 0xff)))
|
||||
return
|
||||
end
|
||||
|
||||
fp:write(string.char(bit.bor(0x80, bit.band(value, 0x7f))))
|
||||
value = bit.brshift(value, 7)
|
||||
end
|
||||
end
|
||||
|
||||
local function write_palette(fp, pal)
|
||||
for i = 1, 16 do
|
||||
fp:write(string.char(
|
||||
bit.brshift(pal[i], 16),
|
||||
bit.band(bit.brshift(pal[i], 8), 0xff),
|
||||
bit.band(pal[i], 0xff)
|
||||
))
|
||||
end
|
||||
end
|
||||
|
||||
local function write_pixeldata_v0(img, fp)
|
||||
for y = 1, img.h do
|
||||
for x = 1, img.w do
|
||||
fp:write(img.lines[y].s:sub(x, x))
|
||||
local bg = tonumber(img.lines[y].bg:sub(x, x), 16)
|
||||
local fg = tonumber(img.lines[y].fg:sub(x, x), 16)
|
||||
fp:write(string.char(bit.bor(bg, bit.blshift(fg, 4))))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local fp_out = io.open(table.remove(args, 1), "wb")
|
||||
fp_out:write("CPI" .. string.char(0x80))
|
||||
|
||||
local fp = io.open(table.remove(args, 1), "rb")
|
||||
print(fp, fp.close)
|
||||
local img, err = ccpi.parse({
|
||||
read = function(sz) return fp:read(sz) end,
|
||||
})
|
||||
fp:close()
|
||||
if err ~= nil then
|
||||
printError(err)
|
||||
return
|
||||
end
|
||||
|
||||
write_varint(fp_out, img.w)
|
||||
write_varint(fp_out, img.h)
|
||||
write_varint(fp_out, #args)
|
||||
|
||||
write_palette(fp_out, img.palette)
|
||||
write_pixeldata_v0(img, fp_out)
|
||||
|
||||
for i, arg in ipairs(args) do
|
||||
print(arg)
|
||||
local fp = io.open(arg, "rb")
|
||||
img, err = ccpi.parse({
|
||||
read = function(sz) return fp:read(sz) end,
|
||||
})
|
||||
fp:close()
|
||||
if err ~= nil then
|
||||
printError(err)
|
||||
return
|
||||
end
|
||||
write_palette(fp_out, img.palette)
|
||||
write_pixeldata_v0(img, fp_out)
|
||||
end
|
||||
|
||||
fp_out:close()
|
BIN
mess/cc_font.png
Before Width: | Height: | Size: 2.8 KiB |
|
@ -1,25 +0,0 @@
|
|||
from PIL import Image
|
||||
from collections import Counter
|
||||
|
||||
with Image.open("./cc_font.png") as im:
|
||||
pixels = im.load()
|
||||
weights = [0 for _ in range(6 * 9)]
|
||||
for char in range(256):
|
||||
ctx, cty = (char % 16) * 8, (char // 16) * 11
|
||||
for oy in range(9):
|
||||
for ox in range(6):
|
||||
pix = int(pixels[ctx + ox + 1, cty + oy + 1][0]) # type: ignore
|
||||
weights[ox + 6 * oy] += 1 if pix else 0
|
||||
|
||||
with Image.new("L", (6, 9), 0) as im_out:
|
||||
for y in range(9):
|
||||
for x in range(6):
|
||||
print("%3d" % weights[x + 6 * y], end="\t")
|
||||
im_out.putpixel((x, y), weights[x + 6 * y])
|
||||
print()
|
||||
|
||||
im_out.save("avg.png")
|
||||
|
||||
print(dict(enumerate([
|
||||
iv[0] for iv in sorted(enumerate(weights), key=lambda iv: iv[1])
|
||||
])))
|
|
@ -1,65 +0,0 @@
|
|||
|
||||
from typing import Any, Literal
|
||||
from websockets import connect
|
||||
|
||||
HexDigit = Literal[
|
||||
"0", "1", "2", "3", "4", "5", "6", "7",
|
||||
"8", "9", "a", "b", "c", "d", "e", "f"
|
||||
]
|
||||
|
||||
class Packet:
|
||||
packet_id: int
|
||||
side: Literal["client", "server"]
|
||||
|
||||
def __init_subclass__(cls, packet_id: int, side: Literal["client", "server"]) -> None:
|
||||
cls.packet_id = packet_id
|
||||
cls.side = side
|
||||
|
||||
class PacketServerPing(Packet, packet_id=2, side="server"):
|
||||
pass
|
||||
|
||||
class PacketClientPong(Packet, packet_id=2, side="client"):
|
||||
pass
|
||||
|
||||
class PacketServerCapabilities(Packet, packet_id=0, side="server"):
|
||||
clients: int
|
||||
capabilities: list[str]
|
||||
|
||||
class PacketServerComputer(Packet, packet_id=18, side="server"):
|
||||
id: int
|
||||
label: str
|
||||
|
||||
class PacketServerScreen(Packet, packet_id=16, side="server"):
|
||||
cursorBlink: bool
|
||||
width: int
|
||||
height: int
|
||||
cursorX: int
|
||||
cursorY: int
|
||||
curFore: HexDigit
|
||||
curBack: HexDigit
|
||||
text: list[str]
|
||||
back: list[str]
|
||||
fore: list[str]
|
||||
palette: list[int]
|
||||
|
||||
class CCEvent:
|
||||
name: str
|
||||
args: list[Any]
|
||||
|
||||
class PacketClientEvent(Packet, packet_id=17, side="client"):
|
||||
events: list[CCEvent]
|
||||
|
||||
class CCAction:
|
||||
action: int
|
||||
|
||||
def __init_subclass__(cls, action: int) -> None:
|
||||
cls.action = action
|
||||
|
||||
class CCActionFile(CCAction, action=0):
|
||||
file: str
|
||||
contents: str
|
||||
checksum: int
|
||||
|
||||
class PacketServerFile(Packet, packet_id=34, side="server"):
|
||||
actions: list[CCAction]
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
# x-run: python3 % ~/downloads/moZtoMP7HAA.mp4 /tmp/video.cani
|
||||
from typing import Literal
|
||||
from dataclasses import dataclass
|
||||
from struct import pack
|
||||
from sys import argv
|
||||
from subprocess import Popen, PIPE, run
|
||||
from tqdm import tqdm
|
||||
from tempfile import TemporaryDirectory
|
||||
from glob import glob
|
||||
from PIL import Image
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
PIX_BITS = [[1, 2], [4, 8], [16, 0]]
|
||||
|
||||
|
||||
@dataclass
|
||||
class VideoMetadata:
|
||||
framerate: Literal[20, 10, 5] = 10
|
||||
audio_channels: Literal[1, 2] = 2
|
||||
sample_rate: Literal[12000, 24000, 48000] = 48000
|
||||
screen_width: int = 164
|
||||
screen_height: int = 81
|
||||
|
||||
@property
|
||||
def audio_samples_per_frame(self) -> int:
|
||||
return self.sample_rate // self.framerate
|
||||
|
||||
def serialize(self) -> bytes:
|
||||
return bytes([ self.framerate, self.audio_channels ]) \
|
||||
+ pack("<HHH", self.sample_rate, self.screen_width, self.screen_height)
|
||||
|
||||
@dataclass
|
||||
class VideoFrame:
|
||||
audio: list[bytes]
|
||||
video: list[bytes]
|
||||
palette: list[int]
|
||||
|
||||
def serialize(self) -> bytes:
|
||||
return bytes.join(b"", self.audio) \
|
||||
+ bytes.join(b"", self.video) \
|
||||
+ bytes.join(b"", [ bytes.fromhex("%06x" % color) for color in self.palette ])
|
||||
|
||||
@lru_cache
|
||||
def _brightness(palette: tuple, i: int) -> float:
|
||||
r, g, b = palette[i * 3 : (i + 1) * 3]
|
||||
return (r + g + b) / 768
|
||||
|
||||
@lru_cache
|
||||
def _distance(palette: tuple, a: int, b: int) -> float:
|
||||
r1, g1, b1 = palette[a * 3 : (a + 1) * 3]
|
||||
r2, g2, b2 = palette[b * 3 : (b + 1) * 3]
|
||||
rd, gd, bd = r1 - r2, g1 - g2, b1 - b2
|
||||
return (rd * rd + gd * gd + bd * bd) / 1966608
|
||||
|
||||
@lru_cache
|
||||
def _get_colors(imgdata, palette: tuple, x: int, y: int) -> tuple[int, int]:
|
||||
brightest_i, brightest_l = 0, 0
|
||||
darkest_i, darkest_l = 0, 768
|
||||
for oy, line in enumerate(PIX_BITS):
|
||||
for ox in range(len(line)):
|
||||
pix = imgdata[x + ox, y + oy]
|
||||
assert pix < 16, f"{pix} is too big at {x+ox}:{y+oy}"
|
||||
brightness = _brightness(palette, 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(palette: tuple, bg: int, fg: int, c: int) -> bool:
|
||||
return _distance(palette, bg, c) < _distance(palette, fg, c)
|
||||
|
||||
def _get_block(imgdata, palette: tuple, x: int, y: int) -> tuple[int, int, int]:
|
||||
dark_i, bri_i = _get_colors(imgdata, palette, 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(PIX_BITS):
|
||||
for ox, bit in enumerate(line):
|
||||
if not _is_darker(
|
||||
palette, dark_i, bri_i, imgdata[x + ox, y + oy]
|
||||
):
|
||||
out |= bit
|
||||
# bottom right pixel fix?
|
||||
if not _is_darker(palette, dark_i, bri_i, imgdata[x + 1, y + 2]):
|
||||
out ^= 31
|
||||
dark_i, bri_i = bri_i, dark_i
|
||||
return out, dark_i, bri_i
|
||||
|
||||
metadata = VideoMetadata(
|
||||
framerate=20,
|
||||
audio_channels=2,
|
||||
sample_rate=24000,
|
||||
screen_width=164,
|
||||
screen_height=81
|
||||
)
|
||||
|
||||
input_video = argv[1]
|
||||
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
run([
|
||||
"ffmpeg",
|
||||
"-i", input_video,
|
||||
"-f", "s8",
|
||||
"-ac", str(metadata.audio_channels),
|
||||
"-ar", str(metadata.sample_rate),
|
||||
f"{tmpdir}/audio.s8"
|
||||
])
|
||||
|
||||
run([
|
||||
"ffmpeg",
|
||||
"-i", input_video,
|
||||
"-an",
|
||||
"-r", str(metadata.framerate),
|
||||
"-vf", f"scale={metadata.screen_width * 2}:{metadata.screen_height * 3}",
|
||||
f"{tmpdir}/video%06d.jpg"
|
||||
])
|
||||
|
||||
with open(argv[2], "w") as fp_out, open(f"{tmpdir}/audio.s8", "rb") as fp_audio:
|
||||
print(metadata.serialize().hex(), file=fp_out)
|
||||
for i, frame_path in tqdm(enumerate(glob(f"{tmpdir}/video*.jpg"))):
|
||||
with Image.open(frame_path) as img_in:
|
||||
img_in = img_in.convert("P", palette=Image.Palette.ADAPTIVE, colors=16)
|
||||
img_data = img_in.load()
|
||||
img_palette = tuple(img_in.getpalette()) # type: ignore
|
||||
|
||||
audio_samples = fp_audio.read(metadata.audio_samples_per_frame * metadata.audio_channels)
|
||||
|
||||
frame = VideoFrame(
|
||||
[
|
||||
audio_samples[i::metadata.audio_channels]
|
||||
for i in range(metadata.audio_channels)
|
||||
],
|
||||
[],
|
||||
[
|
||||
(r << 16) | (g << 8) | b
|
||||
for r, g, b
|
||||
in zip(img_palette[0::3], img_palette[1::3], img_palette[2::3]) # type: ignore
|
||||
])
|
||||
|
||||
for y in range(0, img_in.height - 2, 3):
|
||||
line = bytearray()
|
||||
for x in range(0, img_in.width - 1, 2):
|
||||
ch, bg, fg = _get_block(img_data, img_palette, x, y)
|
||||
line.extend([(ch + 0x80) & 0xFF, fg << 4 | bg])
|
||||
frame.video.append(line)
|
||||
|
||||
print(frame.serialize().hex(), file=fp_out)
|
|
@ -1,36 +0,0 @@
|
|||
|
||||
local config = {
|
||||
{ filter = ".*charged.*", from = "ae2:charger", to = "create:basin" },
|
||||
{ filter = "!.*charged.*", from = "create:basin", to = "ae2:charger", limit = 1 }
|
||||
}
|
||||
|
||||
local function is_matching(pattern, value)
|
||||
if pattern:sub(1, 1) == "!" then
|
||||
return not is_matching(pattern:sub(2))
|
||||
end
|
||||
return value:match(pattern) ~= nil
|
||||
end
|
||||
|
||||
|
||||
local items = {}
|
||||
|
||||
parallel.waitForAll(function() -- Inventory listener
|
||||
while true do
|
||||
local new_items = {}
|
||||
peripheral.find("inventory", function(inv)
|
||||
local inv_items = peripheral.call(inv, "list")
|
||||
for slot, item in pairs(inv_items) do
|
||||
new_items[string.format("%s#%d", inv, slot)] = item
|
||||
end
|
||||
end)
|
||||
items = new_items
|
||||
os.sleep(0)
|
||||
end
|
||||
end, function() -- Executor
|
||||
while true do
|
||||
for i, rule in ipairs(config) do
|
||||
|
||||
end
|
||||
os.sleep(0)
|
||||
end
|
||||
end)
|
|
@ -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,21 +0,0 @@
|
|||
|
||||
peripheral.find("modem", function(name)
|
||||
rednet.open(name)
|
||||
print("Opened modem " .. name .. " for rednet connections")
|
||||
end)
|
||||
|
||||
print("Hostname: " .. os.getComputerLabel())
|
||||
print("ID: " .. os.getComputerID())
|
||||
rednet.host("ramfs", os.getComputerLabel())
|
||||
|
||||
parallel.waitForAll(function()
|
||||
while true do
|
||||
local ev = { os.pullEvent() }
|
||||
if ev[1] == "ramfs:shutdown" then
|
||||
break
|
||||
end
|
||||
print(table.unpack(ev))
|
||||
end
|
||||
end, function() -- Shutdown routine
|
||||
os.pullEvent("ramfs:shutdown")
|
||||
end)
|
|
@ -1,84 +0,0 @@
|
|||
{
|
||||
"junk": [
|
||||
"metalbarrels:gold_tile_4"
|
||||
],
|
||||
"storage": [
|
||||
"create:item_vault_2",
|
||||
"create:item_vault_1"
|
||||
],
|
||||
"pull": [
|
||||
"minecraft:barrel_1",
|
||||
"NOT IMPLEMENTED"
|
||||
],
|
||||
"push": [
|
||||
{ "name": "*_nugget", "to": "metalbarrels:gold_tile_8" },
|
||||
{ "name": "*_ingot", "to": "metalbarrels:gold_tile_8" },
|
||||
{ "name": "minecraft:*coal", "to": "metalbarrels:gold_tile_9" },
|
||||
{ "name": "NOT IMPLEMENTED", "to": "NOT IMPLEMENTED" }
|
||||
],
|
||||
"stock": {
|
||||
"metalbarrels:gold_tile_13#1": { "name": "minecraft:spruce_log", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#2": { "name": "minecraft:spruce_log", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#3": { "name": "minecraft:spruce_planks", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#10": { "name": "minecraft:oak_log", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#11": { "name": "minecraft:oak_log", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#12": { "name": "minecraft:oak_planks", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#19": { "name": "minecraft:dark_oak_log", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#20": { "name": "minecraft:dark_oak_log", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#21": { "name": "minecraft:dark_oak_planks", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#4": { "name": "minecraft:cobblestone", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#5": { "name": "minecraft:cobblestone", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#6": { "name": "minecraft:stone", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#13": { "name": "minecraft:cobbled_deepslate", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#14": { "name": "minecraft:cobbled_deepslate", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#15": { "name": "minecraft:deepslate", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#7": { "name": "minecraft:dirt", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#16": { "name": "minecraft:sand", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#25": { "name": "minecraft:gravel", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#8": { "name": "minecraft:netherrack", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#17": { "name": "minecraft:soul_sand", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#22": { "name": "minecraft:andesite", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#23": { "name": "minecraft:diorite", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#24": { "name": "minecraft:granite", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#31": { "name": "minecraft:tuff", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#32": { "name": "expcaves:sediment_stone", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#33": { "name": "expcaves:lavastone", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#34": { "name": "expcaves:dirtstone", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#42": { "name": "minecraft:calcite", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#43": { "name": "minecraft:clay_ball", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#40": { "name": "create:limestone", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#49": { "name": "minecraft:stone_bricks", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#50": { "name": "minecraft:cracked_stone_bricks", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#51": { "name": "minecraft:moss_block", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#41": { "name": "forbidden_arcanus:darkstone", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#73": { "name": "minecraft:apple", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#74": { "name": "minecraft:kelp", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#75": { "name": "minecraft:oak_sapling", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#76": { "name": "minecraft:spruce_sapling", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#77": { "name": "minecraft:dark_oak_sapling", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#81": { "name": "minecraft:stick", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#26": { "name": "minecraft:nether_bricks", "count": 64 },
|
||||
"metalbarrels:gold_tile_13#35": { "name": "minecraft:magma_block", "count": 64 },
|
||||
|
||||
"metalbarrels:gold_tile_6#1": { "name": "kubejs:kinetic_mechanism", "count": 64 },
|
||||
"metalbarrels:gold_tile_6#2": { "name": "kubejs:kinetic_mechanism", "count": 64 },
|
||||
"metalbarrels:gold_tile_6#10": { "name": "create:andesite_alloy", "count": 64 },
|
||||
"metalbarrels:gold_tile_6#11": { "name": "create:andesite_alloy", "count": 64 },
|
||||
"metalbarrels:gold_tile_6#12": { "name": "thermal:cured_rubber", "count": 64 },
|
||||
"metalbarrels:gold_tile_6#19": { "name": "thermal:silver_coin", "count": 64 },
|
||||
"metalbarrels:gold_tile_6#20": { "name": "thermal:silver_coin", "count": 64 },
|
||||
"metalbarrels:gold_tile_6#21": { "name": "thermal:silver_coin", "count": 64 },
|
||||
|
||||
"metalbarrels:gold_tile_7#1": { "name": "minecraft:lapis_lazuli", "count": 64 },
|
||||
"metalbarrels:gold_tile_7#2": { "name": "minecraft:redstone", "count": 64 },
|
||||
"metalbarrels:gold_tile_7#3": { "name": "minecraft:coal", "count": 15 },
|
||||
"metalbarrels:gold_tile_7#4": { "name": "thermal:apatite", "count": 64 },
|
||||
"metalbarrels:gold_tile_7#5": { "name": "thermal:sulfur", "count": 64 },
|
||||
"metalbarrels:gold_tile_7#6": { "name": "thermal:niter", "count": 64 },
|
||||
"metalbarrels:gold_tile_7#7": { "name": "thermal:cinnabar", "count": 64 },
|
||||
"metalbarrels:gold_tile_7#10": { "name": "ae2:certus_quartz_dust", "count": 64 },
|
||||
"metalbarrels:gold_tile_7#11": { "name": "ae2:certus_crystal_seed", "count": 64 },
|
||||
"metalbarrels:gold_tile_7#12": { "name": "forbidden_arcanus:xpetrified_orb", "count": 16 }
|
||||
}
|
||||
}
|
||||
|
434
mess/restock.lua
|
@ -1,434 +0,0 @@
|
|||
|
||||
local function draw_bar(y, c1, c2, p, fmt, ...)
|
||||
local str = string.format(fmt, ...)
|
||||
local tw = term.getSize()
|
||||
local w1 = math.ceil(p * tw)
|
||||
local w2 = tw - w1
|
||||
|
||||
local old_bg = term.getBackgroundColor()
|
||||
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
|
||||
|
||||
term.setBackgroundColor(old_bg)
|
||||
end
|
||||
|
||||
function table.deepcopy(obj)
|
||||
if type(obj) ~= 'table' then return obj end
|
||||
local res = {}
|
||||
for k, v in pairs(obj) do res[table.deepcopy(k)] = table.deepcopy(v) end
|
||||
return res
|
||||
end
|
||||
|
||||
function spairs(t, order)
|
||||
-- collect the tbl_keys
|
||||
local tbl_keys = {}
|
||||
for k in pairs(t) do tbl_keys[#tbl_keys+1] = k end
|
||||
|
||||
-- if order function given, sort by it by passing the table and tbl_keys a, b,
|
||||
-- otherwise just sort the tbl_keys
|
||||
if order then
|
||||
table.sort(tbl_keys, function(a,b) return order(t, a, b) end)
|
||||
else
|
||||
table.sort(tbl_keys)
|
||||
end
|
||||
|
||||
-- return the iterator function
|
||||
local i = 0
|
||||
return function()
|
||||
i = i + 1
|
||||
if tbl_keys[i] then
|
||||
return tbl_keys[i], t[tbl_keys[i]]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function string.split(inputstr, sep)
|
||||
sep = sep or "%s"
|
||||
local t = {}
|
||||
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
|
||||
table.insert(t, str)
|
||||
end
|
||||
return table.unpack(t)
|
||||
end
|
||||
|
||||
function map(tbl, fn)
|
||||
local out = {}
|
||||
for k, v in pairs(tbl) do
|
||||
out[k] = fn(k, v, tbl)
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function filter(tbl, fil)
|
||||
local out = {}
|
||||
for k, v in pairs(tbl) do
|
||||
if fil(k, v, tbl) then
|
||||
out[k] = v
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function pop(tbl)
|
||||
local out = { k = nil, v = nil }
|
||||
for k, v in pairs(tbl) do
|
||||
out.k = k
|
||||
out.v = v
|
||||
table.remove(tbl, k)
|
||||
break
|
||||
end
|
||||
return out.k, out.v
|
||||
end
|
||||
|
||||
function groupby(tbl, fn)
|
||||
local out = {}
|
||||
for k, v in pairs(tbl) do
|
||||
local group = fn(k, v, tbl)
|
||||
out[group] = out[group] or {}
|
||||
table.insert(out[group], { k = k, v = v })
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function reduce(tbl, operator, initial)
|
||||
local accumulator = initial
|
||||
for k, v in pairs(tbl) do
|
||||
accumulator = operator(k, v, accumulator, tbl)
|
||||
end
|
||||
return accumulator
|
||||
end
|
||||
|
||||
function pipe(input)
|
||||
local function chain(fn)
|
||||
return function(self, ...)
|
||||
fn(self, ...)
|
||||
return self
|
||||
end
|
||||
end
|
||||
return {
|
||||
_value = table.deepcopy(input),
|
||||
groupby = chain(function(self, fn) self._value = groupby(self._value, fn) end),
|
||||
filter = chain(function(self, fn) self._value = filter(self._value, fn) end),
|
||||
map = chain(function(self, fn) self._value = map(self._value, fn) end),
|
||||
reduce = chain(function(self, operator, initial) self._value = reduce(self._value, operator, initial) end),
|
||||
sort = chain(function(self, key)
|
||||
local out = {}
|
||||
for k, v in spairs(self._value, key) do
|
||||
table.insert(out, { k = k, v = v })
|
||||
end
|
||||
self._value = out
|
||||
end),
|
||||
get = function(self) return self._value end
|
||||
}
|
||||
end
|
||||
|
||||
local keyboard = {
|
||||
"qwertyuiop",
|
||||
"asdfghjkl\x1b",
|
||||
"zxcvbnm \xd7",
|
||||
}
|
||||
|
||||
local config = { stock = {}, trash = {}, take = {}, storage = {} }
|
||||
local item_state = { stock = {}, storage = {}, junk = {} }
|
||||
local storage_sizes = { stock = {}, storage = {}, junk = {} }
|
||||
local sort_mode, sort_inverse = "count", false
|
||||
local search_open, search_query = false, ""
|
||||
|
||||
local mon = peripheral.find("monitor")
|
||||
mon.setTextScale(0.5)
|
||||
mon.clear()
|
||||
mon.setPaletteColor(colors.blue, 0x131326) -- odd lines
|
||||
mon.setPaletteColor(colors.purple, 0x261326) -- even lines
|
||||
mon.setPaletteColor(colors.magenta, 0xaa55ff) -- search query
|
||||
mon.setPaletteColor(colors.cyan, 0x55aaff) -- keyboard
|
||||
mon.setPaletteColor(colors.brown, 0x262626) -- keyboard bg
|
||||
|
||||
parallel.waitForAll(function()
|
||||
while true do
|
||||
local fp = assert(io.open("/restock.json", "r"))
|
||||
config = textutils.unserializeJSON(fp:read("a"))
|
||||
fp:close()
|
||||
os.sleep(1)
|
||||
end
|
||||
end, function()
|
||||
while true do
|
||||
local new_state = {}
|
||||
local new_sizes = {}
|
||||
map(config.storage, function(i, inv)
|
||||
new_sizes[inv] = peripheral.call(inv, "size")
|
||||
for slot, item in pairs(peripheral.call(inv, "list")) do
|
||||
new_state[string.format("%s#%d", inv, slot)] = item
|
||||
end
|
||||
end)
|
||||
item_state.storage = new_state
|
||||
storage_sizes.storage = new_sizes
|
||||
os.sleep(0)
|
||||
end
|
||||
end, function()
|
||||
while true do
|
||||
local new_state = {}
|
||||
local new_sizes = {}
|
||||
map(
|
||||
groupby(config.stock, function(location)
|
||||
local storage = string.split(location, "#")
|
||||
return storage
|
||||
end
|
||||
), function(inv)
|
||||
new_sizes[inv] = peripheral.call(inv, "size")
|
||||
for slot, item in pairs(peripheral.call(inv, "list")) do
|
||||
new_state[string.format("%s#%d", inv, slot)] = item
|
||||
end
|
||||
end)
|
||||
item_state.stock = new_state
|
||||
storage_sizes.stock = new_sizes
|
||||
os.sleep(0)
|
||||
end
|
||||
end, function() -- IMPORT
|
||||
while true do
|
||||
map(
|
||||
groupby(config.stock, function(location) return ({ string.split(location, "#") })[1] end),
|
||||
function(stock_inv)
|
||||
local size = storage_sizes.stock[stock_inv] or 0
|
||||
for i = 1, size do
|
||||
local slot = string.format("%s#%d", stock_inv, i)
|
||||
local slot_stocked, slot_expected = item_state.stock[slot], config.stock[slot]
|
||||
if (slot_stocked and slot_expected and slot_stocked.name ~= slot_expected.name) or (slot_stocked ~= nil and slot_expected == nil) then
|
||||
for _, output in ipairs(config.storage) do
|
||||
if peripheral.call(stock_inv, "pushItems", output, i) ~= 0 then
|
||||
print("MOVE", slot, slot_stocked.name)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
os.sleep(0)
|
||||
end
|
||||
end, function() -- STOCK
|
||||
while true do
|
||||
local new_junk = {}
|
||||
local item_locations = groupby(item_state.storage, function(location, item)
|
||||
new_junk[item.name] = (new_junk[item.name] or 0) + item.count
|
||||
return item.name
|
||||
end)
|
||||
|
||||
local tw, th = term.getSize()
|
||||
map(config.stock, function(destination, item)
|
||||
local dst_container, dst_slot = string.split(destination, "#")
|
||||
new_junk[item.name] = nil
|
||||
if item_locations[item.name] == nil then return end
|
||||
local dst_item = item_state.stock[destination]
|
||||
local dst_count = dst_item and dst_item.count or 0
|
||||
if dst_count >= item.count then return end
|
||||
local missing = item.count - dst_count
|
||||
|
||||
local _, take_from = pop(filter(item_locations[item.name], function(_, src_item)
|
||||
local src_container, src_slot = string.split(src_item.k, "#")
|
||||
return src_container ~= dst_container
|
||||
end))
|
||||
|
||||
if not take_from then return end
|
||||
local src_container, src_slot = string.split(take_from.k, "#")
|
||||
local t = os.clock()
|
||||
local out = peripheral.call(dst_container, "pullItems", src_container, tonumber(src_slot), missing, tonumber(dst_slot))
|
||||
print(string.format("PUT %s*%d/%d to %s", item.name, out, missing, destination))
|
||||
-- print(string.format("%s -> %s (%s*%d) took %.2f", take_from.k, destination, item.name, item.count, os.clock() - t))
|
||||
end)
|
||||
item_state.junk = new_junk
|
||||
os.sleep(0)
|
||||
end
|
||||
end, function() -- JUNK
|
||||
while true do
|
||||
local y = 2
|
||||
for name, count in pairs(item_state.junk) do
|
||||
map(filter(item_state.storage, function(location, item)
|
||||
return item.name == name
|
||||
end), function(location, item)
|
||||
local junk_container, junk_slot = string.split(location, "#")
|
||||
for _, junk_output in ipairs(config.junk) do
|
||||
if peripheral.call(junk_container, "pushItems", junk_output, tonumber(junk_slot)) ~= 0 then
|
||||
print("JUNK OUT", item.name, "->", junk_container)
|
||||
break
|
||||
end
|
||||
end
|
||||
end)
|
||||
y = y + 1
|
||||
end
|
||||
os.sleep(0)
|
||||
end
|
||||
end, function() -- USER INPUT
|
||||
while true do
|
||||
local ev = { os.pullEvent() }
|
||||
if ev[1] == "char" and search_open then
|
||||
search_query = search_query .. ev[2]
|
||||
mon.setBackgroundColor(colors.gray)
|
||||
mon.setTextColor(colors.magenta)
|
||||
mon.setCursorPos(tw - 14, th - 20)
|
||||
mon.write("> " .. search_query)
|
||||
for i, line in ipairs(keyboard) do
|
||||
mon.setCursorPos(tw - 14, th - 20 + i)
|
||||
mon.setBackgroundColor(colors.brown)
|
||||
mon.setTextColor(colors.cyan)
|
||||
mon.write(line)
|
||||
end
|
||||
elseif ev[1] == "key" then
|
||||
if ev[2] == keys.enter then
|
||||
search_open = not search_open
|
||||
elseif ev[2] == keys.backspace and search_open then
|
||||
search_query = search_query:sub(1, #search_query - 1)
|
||||
mon.setBackgroundColor(colors.gray)
|
||||
mon.setTextColor(colors.magenta)
|
||||
mon.setCursorPos(tw - 14, th - 20)
|
||||
mon.write("> " .. search_query)
|
||||
for i, line in ipairs(keyboard) do
|
||||
mon.setCursorPos(tw - 14, th - 20 + i)
|
||||
mon.setBackgroundColor(colors.brown)
|
||||
mon.setTextColor(colors.cyan)
|
||||
mon.write(line)
|
||||
end
|
||||
end
|
||||
elseif ev[1] == "monitor_touch" then
|
||||
local tw, th = mon.getSize()
|
||||
local _, _, x, y = table.unpack(ev)
|
||||
if y == 3 or y == th then
|
||||
local new_mode
|
||||
if x >= 1 and x <= 8 then new_mode = "count"
|
||||
elseif x >= 10 and x <= (tw - 1) then new_mode = "name"
|
||||
elseif x == tw then
|
||||
search_open = not search_open
|
||||
end
|
||||
|
||||
if new_mode ~= nil and new_mode == sort_mode then
|
||||
sort_inverse = not sort_inverse
|
||||
elseif new_mode ~= nil then
|
||||
sort_mode = new_mode
|
||||
sort_inverse = false
|
||||
end
|
||||
elseif search_open and x >= (tw - 14) and x < (tw - 4) and y > (th - 20) and y <= (th - 17) then
|
||||
mon.setBackgroundColor(colors.gray)
|
||||
mon.setTextColor(colors.magenta)
|
||||
mon.setCursorPos(tw - 14, th - 20)
|
||||
mon.write("> " .. search_query)
|
||||
for i, line in ipairs(keyboard) do
|
||||
mon.setCursorPos(tw - 14, th - 20 + i)
|
||||
mon.setBackgroundColor(colors.brown)
|
||||
mon.setTextColor(colors.cyan)
|
||||
mon.write(line)
|
||||
end
|
||||
|
||||
local char = keyboard[20 - th + y]:sub(15 - tw + x, 15 - tw + x)
|
||||
if char >= "a" and char <= "z" then
|
||||
os.queueEvent("key", keys[char], false)
|
||||
os.queueEvent("char", char)
|
||||
os.queueEvent("key_up", keys[char])
|
||||
else
|
||||
-- 27 backspace (ev=259)
|
||||
-- 215 enter (ev=257)
|
||||
local code = nil
|
||||
|
||||
if char == "\x1b" then code = keys.backspace
|
||||
elseif char == "\xd7" then code = keys.enter
|
||||
end
|
||||
|
||||
if code then
|
||||
os.queueEvent("key", code, false)
|
||||
os.queueEvent("key_up", code)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end, function() -- STATUS
|
||||
|
||||
while true do
|
||||
local back_term = term.redirect(mon)
|
||||
local tw, th = term.getSize()
|
||||
term.setBackgroundColor(colors.black)
|
||||
term.setTextColor(colors.white)
|
||||
term.clear()
|
||||
|
||||
local usage = pipe(config.storage)
|
||||
:map(function(_, storage)
|
||||
local used, total = 0, storage_sizes.storage[storage] or 0
|
||||
for slot = 1, total do
|
||||
if item_state.storage[string.format("%s#%d", storage, slot)] then
|
||||
used = used + 1
|
||||
end
|
||||
end
|
||||
return { used = used, total = total }
|
||||
end)
|
||||
:reduce(function(_, info, acc)
|
||||
return { used = info.used + acc.used, total = info.total + acc.total }
|
||||
end, { used = 0, total = 0 }):get()
|
||||
|
||||
draw_bar(1, colors.lightGray, colors.gray, usage.used / usage.total, "Storage utilization: %7.3f%%", 100 * usage.used / usage.total)
|
||||
|
||||
term.setCursorPos(1, 3)
|
||||
term.setBackgroundColor(colors.gray)
|
||||
term.clearLine()
|
||||
local infoline = string.format("Count %s Name %s",
|
||||
(sort_mode == "count" and (sort_inverse and "\x1e" or "\x1f") or " "),
|
||||
(sort_mode == "name" and (sort_inverse and "\x1e" or "\x1f") or " "))
|
||||
local fg = sort_mode == "count" and "55555551f88888881" or "88888881f55555551"
|
||||
term.blit(infoline, fg, "77777777777777777")
|
||||
term.setCursorPos(tw, 3)
|
||||
term.write("\x0c")
|
||||
term.setCursorPos(1, th)
|
||||
term.clearLine()
|
||||
term.blit(infoline, fg, "77777777777777777")
|
||||
term.setCursorPos(tw, th)
|
||||
term.write("\x0c")
|
||||
|
||||
|
||||
pipe(item_state.storage)
|
||||
:groupby(function(slot, item) return item.name end)
|
||||
:filter(function(name, slots)
|
||||
if not search_open then return true end
|
||||
return string.match(name, search_query)
|
||||
end)
|
||||
:map(function(name, slots) return reduce(slots, function(_, slot, acc) return slot.v.count + acc end, 0) end)
|
||||
:sort(function(t, a, b)
|
||||
local swap = false
|
||||
if sort_mode == "count" then swap = t[a] > t[b] end
|
||||
if sort_mode == "name" then swap = a > b end
|
||||
return swap ~= sort_inverse
|
||||
end)
|
||||
:map(function(i, kv)
|
||||
if i >= (th - 4) then return end
|
||||
term.setCursorPos(1, 3 + i)
|
||||
term.setBackgroundColor((i % 2) == 0 and colors.blue or colors.purple)
|
||||
term.clearLine()
|
||||
term.write(string.format("%8d %s", kv.v, kv.k))
|
||||
end)
|
||||
|
||||
if search_open then
|
||||
mon.setBackgroundColor(colors.gray)
|
||||
mon.setTextColor(colors.magenta)
|
||||
term.setCursorPos(tw - 14, th - 20)
|
||||
term.write("> " .. search_query)
|
||||
for i, line in ipairs(keyboard) do
|
||||
term.setCursorPos(tw - 14, th - 20 + i)
|
||||
term.setBackgroundColor(colors.brown)
|
||||
term.setTextColor(colors.cyan)
|
||||
term.write(line)
|
||||
end
|
||||
end
|
||||
|
||||
term.redirect(back_term)
|
||||
os.sleep(1)
|
||||
end
|
||||
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")
|
|
@ -1,11 +0,0 @@
|
|||
# x-run: sanic stream_video:app
|
||||
from sanic import Request, Sanic, Websocket
|
||||
|
||||
app = Sanic("CCVideoStreamer")
|
||||
|
||||
@app.websocket("/stream")
|
||||
async def ws_stream(req: Request, ws: Websocket):
|
||||
with open("./video.cani", "r") as fp:
|
||||
for line in fp:
|
||||
await ws.send(bytes.fromhex(line.strip()))
|
||||
await ws.close()
|
|
@ -1,83 +0,0 @@
|
|||
|
||||
local url = "wss://kc.is.being.pet/ws"
|
||||
|
||||
local screen = peripheral.wrap("monitor_1")
|
||||
local speakers = {
|
||||
l = peripheral.wrap("speaker_1"),
|
||||
r = peripheral.wrap("speaker_0"),
|
||||
}
|
||||
|
||||
local ws = assert(http.websocket(url))
|
||||
|
||||
local 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
|
||||
|
||||
local metadata_pkt = ws.receive()
|
||||
metadata_pkt = { string.byte(metadata_pkt, 1, #metadata_pkt) }
|
||||
local framerate = table.remove(metadata_pkt, 1)
|
||||
local n_channels = table.remove(metadata_pkt, 1)
|
||||
local sample_rate, data = parse_u16(metadata_pkt)
|
||||
local screen_w, data = parse_u16(data)
|
||||
local screen_h, data = parse_u16(data)
|
||||
local samples_per_frame = math.floor(sample_rate / framerate)
|
||||
|
||||
print(string.format("FPS: %d", framerate))
|
||||
print(string.format("Audio: %d channels, %d Hz", n_channels, sample_rate))
|
||||
print(string.format("Video: %dx%d", screen_w, screen_h))
|
||||
print(string.format("S/F: %d", samples_per_frame))
|
||||
|
||||
local function decode_s8(buf)
|
||||
local buffer = {}
|
||||
for i = 1, #buf do
|
||||
local v = buf[i]
|
||||
if bit32.band(v, 0x80) then
|
||||
v = bit32.bxor(v, 0x7F) - 128
|
||||
end
|
||||
table.insert(buffer, v)
|
||||
table.insert(buffer, v)
|
||||
end
|
||||
return buffer
|
||||
end
|
||||
|
||||
local function mkSpeakerCoro(p, b)
|
||||
return function()
|
||||
while not p.playAudio(b) do
|
||||
os.pullEvent("speaker_audio_empty")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
while true do
|
||||
local video_pkt = ws.receive()
|
||||
local channels = { l = {}, r = {} }
|
||||
local offset = 1
|
||||
channels.l = decode_s8({ string.byte(video_pkt, offset, offset + samples_per_frame - 1) })
|
||||
offset = offset + samples_per_frame
|
||||
channels.r = decode_s8({ string.byte(video_pkt, offset, offset + samples_per_frame - 1) })
|
||||
offset = offset + samples_per_frame
|
||||
for y = 1, screen_h do
|
||||
local tx, bg, fg = {}, {}, {}
|
||||
for x = 1, screen_w do
|
||||
table.insert(tx, string.sub(video_pkt, offset, offset))
|
||||
local color = string.byte(video_pkt, offset + 1, offset + 1)
|
||||
table.insert(bg, string.format("%x", bit.brshift(color, 4)))
|
||||
table.insert(fg, string.format("%x", bit.band(color, 0xF)))
|
||||
offset = offset + 2
|
||||
end
|
||||
screen.setCursorPos(1, y)
|
||||
screen.blit(table.concat(tx), table.concat(bg), table.concat(fg))
|
||||
end
|
||||
|
||||
for i = 1, 16 do
|
||||
local r, g, b = string.byte(video_pkt, offset, offset + 3)
|
||||
screen.setPaletteColor(bit.blshift(1, i - 1), bit.bor(bit.bor(bit.blshift(r, 16), bit.blshift(g, 8)), b))
|
||||
offset = offset + 3
|
||||
end
|
||||
|
||||
parallel.waitForAll(
|
||||
mkSpeakerCoro(speakers.l, channels.l),
|
||||
mkSpeakerCoro(speakers.r, channels.r))
|
||||
end
|
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
|
@ -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()
|
108
obcb-cc.lua
|
@ -1,108 +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
|
||||
|
||||
local shutdown = false
|
||||
|
||||
parallel.waitForAll(function()
|
||||
while not shutdown do
|
||||
local data, is_binary = ws.receive()
|
||||
if data == nil then return end
|
||||
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()
|
||||
term.clear()
|
||||
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 not shutdown do
|
||||
local ev = { os.pullEvent() }
|
||||
if ev[1] == "char" and ev[2] == "q" then
|
||||
shutdown = true
|
||||
ws.close()
|
||||
break
|
||||
elseif ev[1] == "obcb:hello" then
|
||||
term.setCursorPos(1, 1)
|
||||
term.clearLine()
|
||||
print(string.format("Hello: obcb v%d.%d", ev[2], ev[3]))
|
||||
elseif ev[1] == "obcb:clients" then
|
||||
term.setCursorPos(1, 2)
|
||||
term.clearLine()
|
||||
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)
|
||||
|
||||
shutdown = true
|
||||
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
|
32
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
|
||||
local 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
|
@ -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()
|
114
tapeget.lua
|
@ -1,70 +1,9 @@
|
|||
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())
|
||||
if not tape then
|
||||
printError("wrong name")
|
||||
return
|
||||
end
|
||||
else
|
||||
tape = drives[1]
|
||||
end
|
||||
local tape = peripheral.find("tape_drive")
|
||||
if not tape then
|
||||
print("tape where")
|
||||
return
|
||||
end
|
||||
|
||||
if not http then
|
||||
|
@ -72,52 +11,39 @@ if not http then
|
|||
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)
|
||||
term.setCursorPos(1, y)
|
||||
term.write(n_to_kib(written) .. " / " .. n_to_kib(length) .. string.format(" %7.3f%%", 100 * written / length))
|
||||
end
|
||||
end
|
||||
term.setCursorPos(1, y)
|
||||
term.clearLine()
|
||||
print("Written "..n_to_kib(written))
|
||||
|
||||
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
|
@ -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()
|
381
wsvpn.c
|
@ -1,381 +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);
|
||||
static void on_ws_connect(struct mg_connection *connection, struct mg_http_message *message);
|
||||
static void on_ws_message(struct mg_connection *connection, struct mg_ws_message *message);
|
||||
static void on_ws_disconnect(struct mg_connection *connection);
|
||||
|
||||
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) {
|
||||
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_match(http_message->uri, mg_str_s("/open"), 0)) {
|
||||
mg_ws_upgrade(connection, http_message, NULL);
|
||||
} else if (mg_match(http_message->uri, mg_str_s("/metrics"), 0)) {
|
||||
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);
|
||||
} 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);
|
||||
} else if (event_type == MG_EV_CLOSE) {
|
||||
if (connection->is_websocket) {
|
||||
on_ws_disconnect(connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)message;
|
||||
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) {
|
||||
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.buf[1]);
|
||||
|
||||
switch (message->data.buf[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.buf[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.buf[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.buf[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.buf[3]);
|
||||
uint16_t reply_channel = ntohs(*(uint16_t*)&message->data.buf[5]);
|
||||
uint16_t data_length = ntohs(*(uint16_t*)&message->data.buf[7]);
|
||||
modem_transmit(client, request_id, channel, reply_channel, (void*)&message->data.buf[9], data_length);
|
||||
}
|
||||
return;
|
||||
default:
|
||||
ws_send_error(client, request_id, "Unknown opcode: 0x%02x", message->data.buf[0]);
|
||||
connection->is_draining = 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void on_ws_disconnect(struct mg_connection *connection) {
|
||||
|
||||
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
|
@ -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
|