forked from hkc/cc-stuff
Added random missing stuff so I won't go crazy
This commit is contained in:
parent
e2e5d57e1f
commit
3cadd81a6b
|
@ -2,3 +2,7 @@ img2cpi
|
|||
img2cpi.o
|
||||
wsvpn
|
||||
wsvpn.o
|
||||
cpi2png
|
||||
cc-common.o
|
||||
vim.state
|
||||
__pycache__
|
||||
|
|
22
ccpi.lua
22
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
|
||||
|
|
|
@ -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()
|
|
@ -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]
|
||||
|
|
@ -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("<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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
|
@ -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()
|
|
@ -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
|
13
obcb-cc.lua
13
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()
|
||||
|
|
Loading…
Reference in New Issue