Compare commits

..

7 Commits

Author SHA1 Message Date
Casey 68717eaa04
rectangles have height, did you know that? 2023-10-14 14:03:53 +03:00
Casey 631abc9ab9
proper health issue fix 2023-10-14 14:03:15 +03:00
Casey a37ba81545
ignore self and proper bar size 2023-10-14 14:01:38 +03:00
Casey b3515c737f
some entities don't have health 2023-10-14 13:58:23 +03:00
Casey 3cdeac3988
oh stupid meeeeeeeee 2023-10-14 13:32:05 +03:00
Casey 00405645f7
Repo URLs 2023-10-14 13:31:20 +03:00
Casey 1dbf9b56a5
Reworked wallhack 2023-10-14 13:26:32 +03:00
73 changed files with 145 additions and 8001 deletions

8
.gitignore vendored
View File

@ -1,8 +0,0 @@
img2cpi
img2cpi.o
wsvpn
wsvpn.o
cpi2png
cc-common.o
vim.state
__pycache__

6
.gitmodules vendored
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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": [ "files": [
{ {
"path": "wsvpn.lua", "path": "startup",
"src": "wsvpn.lua"
},
{
"path": "startup.lua",
"src": "startup.lua" "src": "startup.lua"
}, },
{ {

View File

@ -8,8 +8,35 @@ _G.canvas3d_src.clear()
_G.canvas3d = canvas3d_src.create() _G.canvas3d = canvas3d_src.create()
_G.player = nil _G.player = nil
_G.nearbyEntities = {} _G.surroundings = {
_G.nearbyEntitiesByUUID = {} 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) local function run_wrapped(func, filename)
return function() return function()
@ -41,16 +68,16 @@ end
print("Loaded " .. #modules .. " modules") print("Loaded " .. #modules .. " modules")
local function safeset(func, name, old) local function safeget(func, name, fallback, ...)
if func then if func then
local s, res = pcall(func) local s, res = pcall(func, ...)
if not s then if not s then
printError("ERR: " .. name .. " failed: " .. res) printError(name .. " failed: " .. res)
else else
return res return res
end end
end end
return old return fallback
end end
print("Running...") print("Running...")
@ -68,15 +95,28 @@ end,
function() -- Neural Interface coroutine function() -- Neural Interface coroutine
print("NI routine started") print("NI routine started")
while _G._running do while _G._running do
_G.player = safeset(NI.getMetaOwner, "getMetaOwner()", _G.player) _G.player = safeget(NI.getMetaOwner, "getMetaOwner()", _G.player)
_G.nearbyEntities = safeset(NI.sense, "sense()", _G.nearbyEntities or {})
_G.nearbyEntitiesByUUID = {} surroundings.entities = safeget(NI.sense, "sense()", surroundings.entities)
for i = 1, #_G.nearbyEntities do
_G.nearbyEntitiesByUUID[_G.nearbyEntities[i].id] = _G.nearbyEntities[i] 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 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() _G.canvas3d.recenter()
os.sleep(0.05) os.sleep(0.05)
end end
_G.canvas3d_src.clear() _G.canvas3d_src.clear()
_G.canvas2d.clear() _G.canvas2d.clear()
end, table.unpack(modules)) end, table.unpack(modules))

View File

@ -1,7 +1,7 @@
return function() return function()
while _G._running do while _G._running do
local ev = { os.pullEvent() } local ev = { os.pullEvent() }
if ev[1] == "key" and ev[2] == keys.f4 then if ev[1] == "key" and ev[2] == keys.q then
os.queueEvent("exit") os.queueEvent("exit")
break break
elseif ev[1] == "timer" or ev[1] == "plethora_task" then elseif ev[1] == "timer" or ev[1] == "plethora_task" then

View File

@ -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() return function()
local cache = {} local cache = {}
while _G._running do while _G._running do
for id, entry in pairs(cache) do for uuid in table.keys(cache) do
if nearbyEntitiesByUUID[id] == nil then if surroundings.entitiesByUUID[uuid] == nil then
entry.cube.remove() cache[uuid]:destroy()
entry.frame.remove() cache[uuid] = nil
cache[id] = nil
end end
end end
for id, entity in pairs(nearbyEntitiesByUUID) do for uuid, entity in pairs(surroundings.entitiesByUUID) do
if id ~= player.id then if uuid == player.id then
if cache[id] == nil then -- nothing
cache[id] = {} elseif cache[uuid] == nil then
cache[id].cube = canvas3d.addBox(0, 0, 0) cache[uuid] = entityBox(entity)
cache[id].cube.setSize(0.5, 0.5, 0.5) else
cache[id].frame = canvas3d.addFrame({ 0, 0, 0 }) cache[uuid]:update(entity)
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)
end end
end end
os.sleep(0.05) os.sleep(0.05)
end end
for uuid in table.keys(cache) do
cache[uuid]:destroy()
end
end end

View File

@ -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 files = textutils.unserializeJSON(http.get(repository).readAll())
local function getFile(url, path) local function getFile(url, path)

View File

@ -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

View File

@ -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}")

Binary file not shown.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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, },
};

View File

@ -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
View File

@ -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
View File

@ -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
}

View File

@ -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)

View File

@ -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"

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

View File

@ -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;
}

View File

@ -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
dependencies/stb vendored

@ -1 +0,0 @@
Subproject commit f75e8d1cad7d90d72ef7a4661f1b994ef78b4e31

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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
View File

@ -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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

View File

@ -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()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -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])
])))

View File

@ -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]

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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 }
}
}

View File

@ -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)

View File

@ -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")

View File

@ -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()

View File

@ -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

View File

@ -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!")

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -1,6 +1,6 @@
local args = { ... } local args = { ... }
local dfpwm = require("cc.audio.dfpwm") local buffer_size = 8192
if not http then if not http then
print("no http, check config") print("no http, check config")
@ -21,35 +21,21 @@ if not req then
return return
end end
local headers = req.getResponseHeaders() local buffer = { }
local length = tonumber(headers["Content-Length"]) or 0 for i = 1, buffer_size do
buffer[i] = 0
local function decode_s8(data)
local buffer = {}
for i = 1, #data do
local v = string.byte(data, i)
if bit32.band(v, 0x80) then
v = bit32.bxor(v, 0x7F) - 128
end
buffer[i] = v
end
return buffer
end end
local use_dfpwm = ({ args[1]:find("%.dfpwm") })[2] == #args[1]
local decode = use_dfpwm and dfpwm.make_decoder() or decode_s8
local read_bytes = 0
while true do while true do
local chunk = req.read(16384) local chunk = req.read(buffer_size)
if not chunk then if not chunk then
break break
end end
buffer = {}
local buffer = decode(chunk) for i = 1, #chunk do
buffer[i] = string.byte(chunk, i) - 128
end
while not speaker.playAudio(buffer) do while not speaker.playAudio(buffer) do
os.pullEvent("speaker_audio_empty") os.pullEvent("speaker_audio_empty")
end end
end end
print()

View File

@ -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)

View File

@ -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)")

View File

@ -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()

View File

@ -1,70 +1,9 @@
local args = { ... } local args = { ... }
local seekTo = 0 local tape = peripheral.find("tape_drive")
local driveName = nil if not tape then
if args[1] == "-S" then print("tape where")
table.remove(args, 1) return
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
end end
if not http then if not http then
@ -72,52 +11,39 @@ if not http then
return return
end 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 if not req then
print("oopsie: "..err) print("oopsie")
return return
end end
local headers = req.getResponseHeaders() local headers = req.getResponseHeaders()
local length = tonumber(headers["content-length"]) or 1 local length = headers["content-length"]
if length > tape.getSize() then local function n_to_kib(value)
printError("Tape is smaller than the file you're trying to write") return string.format("%6.1f kiB", value / 1024)
printError("Are you sure?")
io.write("Write anyways? [y/N]: ")
local r = read()
if r ~= "y" and r ~= "Y" then
return
end
end end
tape.stop() local written = 1
tape.seek(-tape.getSize())
tape.seek(seekTo)
tape.stop()
local _, y = term.getCursorPos() local _, y = term.getCursorPos()
local written = 0
local i = 1
while true do while true do
local chunk = req.read(256) local chunk = req.read()
if not chunk then if not chunk then
print("EOF")
break break
end end
written = written + #chunk
tape.write(chunk) tape.write(chunk)
if i % 10 == 0 then written = written + 1
term.setCursorPos(1, y) if (written % 8192) == 0 then
term.clearLine()
textProgress(written / length, colors.green, colors.gray, "%s / %s", n_to_kib(written), n_to_kib(length))
os.sleep(0.01) os.sleep(0.01)
term.setCursorPos(1, y)
term.write(n_to_kib(written) .. " / " .. n_to_kib(length) .. string.format(" %7.3f%%", 100 * written / length))
end end
end end
term.setCursorPos(1, y)
term.clearLine()
print("Written "..n_to_kib(written))
tape.stop() tape.stop()
tape.seek(-tape.getSize()) tape.seek(-tape.getSize())

View File

@ -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
)

View File

@ -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)

View File

@ -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")

View File

@ -1,7 +0,0 @@
{
"ver": "0.0.3",
"files": [
{ "src": "/startup.lua", "dst": "/startup.lua" },
{ "src": "/main.lua", "dst": "/main.lua" }
]
}

275
video.lua
View File

@ -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))

View File

@ -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
View File

@ -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
View File

@ -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