From 3cadd81a6be4d995d672a8ec4d1665600c583d0d Mon Sep 17 00:00:00 2001 From: hkc Date: Thu, 3 Oct 2024 19:03:22 +0300 Subject: [PATCH] Added random missing stuff so I won't go crazy --- .gitignore | 4 + ccpi.lua | 22 +- mess/cc-concat.lua | 82 ++++++++ mess/libcloudcatcher.py | 65 ++++++ mess/package_video.py | 150 ++++++++++++++ mess/pipez.lua | 36 ++++ mess/ramfsd.lua | 21 ++ mess/restock.json | 84 ++++++++ mess/restock.lua | 434 ++++++++++++++++++++++++++++++++++++++++ mess/stream_video.py | 11 + mess/streamplay.lua | 83 ++++++++ obcb-cc.lua | 13 +- 12 files changed, 1002 insertions(+), 3 deletions(-) create mode 100644 mess/cc-concat.lua create mode 100644 mess/libcloudcatcher.py create mode 100644 mess/package_video.py create mode 100644 mess/pipez.lua create mode 100644 mess/ramfsd.lua create mode 100644 mess/restock.json create mode 100644 mess/restock.lua create mode 100644 mess/stream_video.py create mode 100644 mess/streamplay.lua diff --git a/.gitignore b/.gitignore index b8fb39c..78d9050 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ img2cpi img2cpi.o wsvpn wsvpn.o +cpi2png +cc-common.o +vim.state +__pycache__ diff --git a/ccpi.lua b/ccpi.lua index 849182c..a941271 100644 --- a/ccpi.lua +++ b/ccpi.lua @@ -64,9 +64,29 @@ decoders[1] = function(image, fp) 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 = {} } + local image = { w = 0, h = 0, scale = 1.0, palette = {}, lines = {}, extras = {} } local magic = fp.read(4) if magic == "CCPI" then diff --git a/mess/cc-concat.lua b/mess/cc-concat.lua new file mode 100644 index 0000000..2340719 --- /dev/null +++ b/mess/cc-concat.lua @@ -0,0 +1,82 @@ +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() diff --git a/mess/libcloudcatcher.py b/mess/libcloudcatcher.py new file mode 100644 index 0000000..c460cba --- /dev/null +++ b/mess/libcloudcatcher.py @@ -0,0 +1,65 @@ + +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] + diff --git a/mess/package_video.py b/mess/package_video.py new file mode 100644 index 0000000..6053237 --- /dev/null +++ b/mess/package_video.py @@ -0,0 +1,150 @@ +# 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(" 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) diff --git a/mess/pipez.lua b/mess/pipez.lua new file mode 100644 index 0000000..a36320c --- /dev/null +++ b/mess/pipez.lua @@ -0,0 +1,36 @@ + +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) diff --git a/mess/ramfsd.lua b/mess/ramfsd.lua new file mode 100644 index 0000000..50ff6fb --- /dev/null +++ b/mess/ramfsd.lua @@ -0,0 +1,21 @@ + +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) diff --git a/mess/restock.json b/mess/restock.json new file mode 100644 index 0000000..69dfba1 --- /dev/null +++ b/mess/restock.json @@ -0,0 +1,84 @@ +{ + "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 } + } +} + diff --git a/mess/restock.lua b/mess/restock.lua new file mode 100644 index 0000000..a7f8d2f --- /dev/null +++ b/mess/restock.lua @@ -0,0 +1,434 @@ + +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) diff --git a/mess/stream_video.py b/mess/stream_video.py new file mode 100644 index 0000000..8cbbcfd --- /dev/null +++ b/mess/stream_video.py @@ -0,0 +1,11 @@ +# 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() diff --git a/mess/streamplay.lua b/mess/streamplay.lua new file mode 100644 index 0000000..ebbef06 --- /dev/null +++ b/mess/streamplay.lua @@ -0,0 +1,83 @@ + +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 diff --git a/obcb-cc.lua b/obcb-cc.lua index f0e6167..2fe4054 100644 --- a/obcb-cc.lua +++ b/obcb-cc.lua @@ -26,9 +26,12 @@ 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 true do + 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 @@ -63,20 +66,25 @@ function() 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 true do + 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 @@ -96,4 +104,5 @@ function() end end) +shutdown = true ws.close()