diff options
| author | LLLL Colonq <llll@colonq> | 2026-04-26 22:56:52 -0400 |
|---|---|---|
| committer | LLLL Colonq <llll@colonq> | 2026-04-26 22:59:25 -0400 |
| commit | d943ba194b3cfab18354e96f7be2c1e434d6c073 (patch) | |
| tree | 786b7c92f3d9abc6a147a59e440982306fc77f55 /crates/renderer/src | |
| parent | 4a22d2573cd4014c3cc0ed784dd2e9d6bed7fb67 (diff) | |
Update
Diffstat (limited to 'crates/renderer/src')
29 files changed, 1193 insertions, 27 deletions
diff --git a/crates/renderer/src/assets.rs b/crates/renderer/src/assets.rs index bfcaa0d..4eb681a 100644 --- a/crates/renderer/src/assets.rs +++ b/crates/renderer/src/assets.rs @@ -3,6 +3,8 @@ use teleia::*; pub struct Assets { pub font: font::Bitmap, pub shader_flat: shader::Shader, + pub shader_flat_noflip: shader::Shader, + pub shader_acs: shader::Shader, pub shader_scene: shader::Shader, pub shader_color: shader::Shader, pub shader_tcg: shader::Shader, @@ -13,17 +15,28 @@ pub struct Assets { pub texture_mod: texture::Texture, pub texture_operatop: texture::Texture, pub texture_operabottom: texture::Texture, + pub texture_clippyborder: texture::Texture, } impl Assets { pub fn new(ctx: &context::Context) -> Self { Self { - font: font::Bitmap::new(ctx), + font: font::Bitmap::default(ctx), shader_flat: shader::Shader::new( ctx, include_str!("assets/shaders/flat/vert.glsl"), include_str!("assets/shaders/flat/frag.glsl"), ), + shader_flat_noflip: shader::Shader::new( + ctx, + include_str!("assets/shaders/flat_noflip/vert.glsl"), + include_str!("assets/shaders/flat_noflip/frag.glsl"), + ), + shader_acs: shader::Shader::new( + ctx, + include_str!("assets/shaders/acs/vert.glsl"), + include_str!("assets/shaders/acs/frag.glsl"), + ), shader_scene: shader::Shader::new( ctx, include_str!("assets/shaders/scene/vert.glsl"), @@ -58,6 +71,7 @@ impl Assets { texture_mod: texture::Texture::new(ctx, include_bytes!("assets/textures/mod.png")), texture_operatop: texture::Texture::new(ctx, include_bytes!("assets/textures/operatop.png")), texture_operabottom: texture::Texture::new(ctx, include_bytes!("assets/textures/operabottom.png")), + texture_clippyborder: texture::Texture::new(ctx, include_bytes!("assets/textures/clippyborder.png")), } } } diff --git a/crates/renderer/src/assets/acs/bonzi.acs b/crates/renderer/src/assets/acs/bonzi.acs Binary files differnew file mode 100644 index 0000000..b463dc8 --- /dev/null +++ b/crates/renderer/src/assets/acs/bonzi.acs diff --git a/crates/renderer/src/assets/acs/clippy.acs b/crates/renderer/src/assets/acs/clippy.acs Binary files differnew file mode 100644 index 0000000..0dbc388 --- /dev/null +++ b/crates/renderer/src/assets/acs/clippy.acs diff --git a/crates/renderer/src/assets/acs/pikachu.acs b/crates/renderer/src/assets/acs/pikachu.acs Binary files differnew file mode 100644 index 0000000..4e7615d --- /dev/null +++ b/crates/renderer/src/assets/acs/pikachu.acs diff --git a/crates/renderer/src/assets/shaders/acs/frag.glsl b/crates/renderer/src/assets/shaders/acs/frag.glsl new file mode 100644 index 0000000..8fba8bf --- /dev/null +++ b/crates/renderer/src/assets/shaders/acs/frag.glsl @@ -0,0 +1,8 @@ +uniform sampler2D texture_data; + +void main() +{ + vec2 tcfull = vec2(vertex_texcoord.x, vertex_texcoord.y); + vec4 texel = texture(texture_data, tcfull); + frag_color = texel; +} diff --git a/crates/renderer/src/assets/shaders/acs/vert.glsl b/crates/renderer/src/assets/shaders/acs/vert.glsl new file mode 100644 index 0000000..e324f7e --- /dev/null +++ b/crates/renderer/src/assets/shaders/acs/vert.glsl @@ -0,0 +1,4 @@ +void main() +{ + default_main(); +}
\ No newline at end of file diff --git a/crates/renderer/src/assets/shaders/flat_noflip/frag.glsl b/crates/renderer/src/assets/shaders/flat_noflip/frag.glsl new file mode 100644 index 0000000..e20995c --- /dev/null +++ b/crates/renderer/src/assets/shaders/flat_noflip/frag.glsl @@ -0,0 +1,7 @@ +uniform sampler2D texture_data; + +void main() +{ + vec4 texel = texture(texture_data, vertex_texcoord); + frag_color = texel; +} diff --git a/crates/renderer/src/assets/shaders/flat_noflip/vert.glsl b/crates/renderer/src/assets/shaders/flat_noflip/vert.glsl new file mode 100644 index 0000000..e324f7e --- /dev/null +++ b/crates/renderer/src/assets/shaders/flat_noflip/vert.glsl @@ -0,0 +1,4 @@ +void main() +{ + default_main(); +}
\ No newline at end of file diff --git a/crates/renderer/src/assets/textures/clippyborder.png b/crates/renderer/src/assets/textures/clippyborder.png Binary files differnew file mode 100644 index 0000000..3bb3089 --- /dev/null +++ b/crates/renderer/src/assets/textures/clippyborder.png diff --git a/crates/renderer/src/assets/textures/combo/a.webp b/crates/renderer/src/assets/textures/combo/a.webp Binary files differnew file mode 100644 index 0000000..15d4042 --- /dev/null +++ b/crates/renderer/src/assets/textures/combo/a.webp diff --git a/crates/renderer/src/assets/textures/combo/b.webp b/crates/renderer/src/assets/textures/combo/b.webp Binary files differnew file mode 100644 index 0000000..3eec6b1 --- /dev/null +++ b/crates/renderer/src/assets/textures/combo/b.webp diff --git a/crates/renderer/src/assets/textures/combo/c.webp b/crates/renderer/src/assets/textures/combo/c.webp Binary files differnew file mode 100644 index 0000000..1babbdf --- /dev/null +++ b/crates/renderer/src/assets/textures/combo/c.webp diff --git a/crates/renderer/src/assets/textures/combo/d.webp b/crates/renderer/src/assets/textures/combo/d.webp Binary files differnew file mode 100644 index 0000000..1d08e9c --- /dev/null +++ b/crates/renderer/src/assets/textures/combo/d.webp diff --git a/crates/renderer/src/assets/textures/combo/s.webp b/crates/renderer/src/assets/textures/combo/s.webp Binary files differnew file mode 100644 index 0000000..faec75a --- /dev/null +++ b/crates/renderer/src/assets/textures/combo/s.webp diff --git a/crates/renderer/src/assets/textures/combo/ss.webp b/crates/renderer/src/assets/textures/combo/ss.webp Binary files differnew file mode 100644 index 0000000..62fec55 --- /dev/null +++ b/crates/renderer/src/assets/textures/combo/ss.webp diff --git a/crates/renderer/src/assets/textures/combo/sss.webp b/crates/renderer/src/assets/textures/combo/sss.webp Binary files differnew file mode 100644 index 0000000..f1ecb80 --- /dev/null +++ b/crates/renderer/src/assets/textures/combo/sss.webp diff --git a/crates/renderer/src/ffi/egl.rs b/crates/renderer/src/ffi/egl.rs new file mode 100644 index 0000000..33a39d4 --- /dev/null +++ b/crates/renderer/src/ffi/egl.rs @@ -0,0 +1,41 @@ +// from <EGL/egl.h> +pub const EGL_NONE: EGLenum = 0x3038; +pub const EGL_WIDTH: EGLenum = 0x3057; +pub const EGL_HEIGHT: EGLenum = 0x3056; + +// from <EGL/eglext.h> +pub const EGL_LINUX_DMA_BUF_EXT: EGLenum = 0x3270; +pub const EGL_LINUX_DRM_FOURCC_EXT: EGLenum = 0x3271; +pub const EGL_DMA_BUF_PLANE0_FD_EXT: EGLenum = 0x3272; +pub const EGL_DMA_BUF_PLANE0_OFFSET_EXT: EGLenum = 0x3273; +pub const EGL_DMA_BUF_PLANE0_PITCH_EXT: EGLenum = 0x3274; + +#[repr(C)] +pub struct EGLDisplayTarget(()); +pub type EGLDisplay = *mut EGLDisplayTarget; + +#[repr(C)] +pub struct EGLContextTarget(()); +pub type EGLContext = *mut EGLContextTarget; + +#[repr(C)] +pub struct EGLClientBufferTarget(()); +pub type EGLClientBuffer = *mut EGLClientBufferTarget; + +#[repr(C)] +pub struct EGLImageTarget(()); +pub type EGLImage = *mut EGLImageTarget; + +pub type EGLenum = core::ffi::c_uint; + +pub type EGLAttrib = libc::intptr_t; + +#[link(name = "EGL")] +extern "C" { + pub fn eglCreateImage(d: EGLDisplay, c: EGLContext, t: EGLenum, buf: EGLClientBuffer, attribs: *const EGLAttrib) -> EGLImage; +} + +#[link(name = "GL")] +extern "C" { + pub fn glEGLImageTargetTexture2DOES(t: u32, img: EGLImage); +} diff --git a/crates/renderer/src/ffi/glfw.rs b/crates/renderer/src/ffi/glfw.rs new file mode 100644 index 0000000..1b79089 --- /dev/null +++ b/crates/renderer/src/ffi/glfw.rs @@ -0,0 +1,7 @@ +use super::egl::*; + +#[link(name = "glfw")] +extern "C" { + pub fn glfwGetEGLDisplay() -> EGLDisplay; + pub fn glfwGetEGLContext(w: *mut glfw::ffi::GLFWwindow) -> EGLContext; +} diff --git a/crates/renderer/src/ffi/mod.rs b/crates/renderer/src/ffi/mod.rs new file mode 100644 index 0000000..a98fd71 --- /dev/null +++ b/crates/renderer/src/ffi/mod.rs @@ -0,0 +1,2 @@ +pub mod egl; +pub mod glfw; diff --git a/crates/renderer/src/main.rs b/crates/renderer/src/main.rs index ae8cc03..b71a836 100644 --- a/crates/renderer/src/main.rs +++ b/crates/renderer/src/main.rs @@ -1,3 +1,6 @@ +#![feature(unix_socket_ancillary_data)] +#![feature(str_from_utf16_endian)] +#![feature(try_blocks)] #![allow(dead_code, unused_variables)] mod assets; mod terminal; @@ -5,9 +8,12 @@ mod background; mod toggle; mod overlay; mod input; +mod ffi; +mod texture_server; use teleia::*; -use clap::{command, Command}; +use clap::{command, Command, Arg}; +use std::io::Write as _; pub fn main() -> Erm<()> { let matches = command!() @@ -22,6 +28,16 @@ pub fn main() -> Erm<()> { Command::new("model-terminal") .about("Run the LCOLONQ model renderer in a terminal") ) + .subcommand( + Command::new("validate-card") + .arg(Arg::new("path")) + .about("Validate the TCG card at the given path.") + ) + .subcommand( + Command::new("repair-card") + .arg(Arg::new("path")) + .about("Repair the TCG card at the given path.") + ) .get_matches(); match matches.subcommand() { Some(("overlay", _cm)) => { @@ -29,8 +45,10 @@ pub fn main() -> Erm<()> { overlay::Overlays::new(ctx, vec![ Box::new(overlay::automata::Overlay::new(ctx)), Box::new(overlay::shader::Overlay::new(ctx)), - Box::new(overlay::drawing::Overlay::new(ctx)), Box::new(overlay::tcg::Overlay::new(ctx)), + Box::new(overlay::clippy::Overlay::new(ctx)), + Box::new(overlay::combo::Overlay::new(ctx)), + Box::new(overlay::drawing::Overlay::new(ctx)), // Box::new(overlay::model::Overlay::new(ctx)), // Box::new(overlay::loopback::Overlay::new(ctx)), ]) @@ -39,6 +57,27 @@ pub fn main() -> Erm<()> { Some(("model-terminal", _cm)) => { teleia::run("LCOLONQ", 1920, 1080, teleia::Options::HIDDEN, overlay::model::Terminal::new)?; }, + Some(("validate-card", cm)) => { + if let Some(path) = cm.get_one::<String>("path") { + if overlay::tcg::validate_card(&std::fs::read(path)?)? { + println!("signatures match!"); + std::process::exit(0); + } else { + println!("signatures do not match"); + std::process::exit(1); + } + } else { + eprintln!("no path specified"); + } + }, + Some(("repair-card", cm)) => { + if let Some(path) = cm.get_one::<String>("path") { + let res = overlay::tcg::repair_card(&std::fs::read(path)?)?; + std::io::stdout().write_all(&res)?; + } else { + eprintln!("no path specified"); + } + }, _ => unreachable!("no subcommand"), } Ok(()) diff --git a/crates/renderer/src/overlay.rs b/crates/renderer/src/overlay.rs index 049d788..5abeb04 100644 --- a/crates/renderer/src/overlay.rs +++ b/crates/renderer/src/overlay.rs @@ -4,13 +4,17 @@ pub mod drawing; pub mod automata; pub mod tcg; pub mod loopback; +pub mod clippy; +pub mod combo; +use redis::Commands; use teleia::*; use std::f32::consts::PI; use byteorder::{LE, ReadBytesExt}; +use sha2::Digest; -use crate::{assets, fig, toggle, input, background}; +use crate::{assets, background, input, net::fig, texture_server, toggle}; pub struct Chat { author: String, @@ -49,6 +53,7 @@ pub struct State { model: scene::Scene, model_neck_base: glam::Mat4, fig_binary: fig::BinaryClient, + texture_server: texture_server::Server, tracking: Tracking, info: Info, chat: Chat, @@ -87,7 +92,12 @@ impl State { b"overlay shader chat", b"overlay automata spawn", b"overlay tcg generate", + b"overlay clippy animate", + b"overlay clippy border on", + b"overlay clippy border off", + b"overlay combo message", ]).expect("failed to connect to bus"), + texture_server: texture_server::Server::new(ctx), tracking: Tracking { eyes: (1.0, 1.0), mouth: 0.0, @@ -121,6 +131,7 @@ impl State { if let Some(n) = self.model.nodes_by_name.get("J_Bip_C_Neck").and_then(|i| self.model.nodes.get_mut(*i)) { n.transform = self.model_neck_base * glam::Mat4::from_quat(self.tracking.neck); } + self.texture_server.update(ctx); Ok(()) } } @@ -256,6 +267,14 @@ impl teleia::state::Game for Overlays { for ov in self.overlays.iter_mut() { ov.render(ctx, st, &mut self.state)?; } + // st.bind_2d(ctx, &self.state.assets.shader_flat); + // self.state.assets.shader_flat.set_f32(ctx, "transparency", 0.0); + // self.state.assets.shader_flat.set_position_2d(ctx, st, + // &glam::Vec2::new(0.0, 0.0), + // &glam::Vec2::new(200.0, 200.0), + // ); + // self.state.texture_server.tex.bind(ctx); + // st.mesh_square.render(ctx); Ok(()) } } diff --git a/crates/renderer/src/overlay/automata.rs b/crates/renderer/src/overlay/automata.rs index 224a783..f48ed9d 100644 --- a/crates/renderer/src/overlay/automata.rs +++ b/crates/renderer/src/overlay/automata.rs @@ -249,13 +249,13 @@ impl overlay::Overlay for Overlay { } fn handle_binary( &mut self, ctx: &context::Context, st: &mut state::State, _ost: &mut overlay::State, - msg: &fig::BinaryMessage, + msg: &net::fig::BinaryMessage, ) -> Erm<()> { if msg.event == b"overlay automata spawn" { let res: Erm<()> = (|| { let mut reader = std::io::Cursor::new(&msg.data); - let rle = fig::read_length_prefixed_utf8(&mut reader)?; - let _user = fig::read_length_prefixed_utf8(&mut reader)?; + let rle = net::fig::read_length_prefixed_utf8(&mut reader)?; + let _user = net::fig::read_length_prefixed_utf8(&mut reader)?; let col = reader.read_u32::<LE>()?; log::info!("col: {}", col); let r = (col >> 16 & 0xff) as u8; diff --git a/crates/renderer/src/overlay/clippy.rs b/crates/renderer/src/overlay/clippy.rs new file mode 100644 index 0000000..e1ddb0e --- /dev/null +++ b/crates/renderer/src/overlay/clippy.rs @@ -0,0 +1,605 @@ +use teleia::*; + +use crate::overlay; + +use rand::Rng; +use std::{collections::HashMap, io::{Seek, SeekFrom}}; +use byteorder::{LE, ReadBytesExt}; + +struct Bitstream { + bytes: Vec<u8>, + idx: usize, + bidx: usize, +} +impl Bitstream { + pub fn new(bytes: Vec<u8>) -> Self { Self { bytes, idx: 0, bidx: 0 } } + fn pop_bit(&mut self) -> Option<bool> { + let w = self.bytes.get(self.idx)?; + let ret = (w >> self.bidx) & 0b1; + self.bidx += 1; + if self.bidx > 7 { self.bidx = 0; self.idx += 1; } + Some(ret == 1) + } + fn pop_bits(&mut self, count: usize) -> Option<u32> { + let mut ret = 0; + for shift in 0..count { + ret |= (self.pop_bit()? as u32) << shift; + } + Some(ret) + } + fn pop_byte(&mut self) -> Option<u8> { Some(self.pop_bits(8)? as u8) } +} + +fn decompress(bytes: Vec<u8>) -> Option<Vec<u8>> { + let mut ret = Vec::new(); + let mut bits = Bitstream::new(bytes); + if bits.pop_byte()? != 0 { + panic!("no leading 0 byte"); + } + while let Some(bty) = bits.pop_bit() { match bty { + false => ret.push(bits.pop_byte()?), + true => { + let mut bytes_to_read = 2; + let mut off_sequential_ones = 0; + for _ in 0..3 { + if bits.pop_bit()? { off_sequential_ones += 1; } + else { break; } + } + let (bitcount, addend) = match off_sequential_ones { + 0 => (6, 1), + 1 => (9, 65), + 2 => (12, 577), + 3 => (20, 4673), + _ => unreachable!("bad sequential bits"), + }; + let mut num = bits.pop_bits(bitcount)?; + if bitcount == 20 { + if num == 0x000FFFFF { break; } + else { bytes_to_read += 1; } + } + num += addend; + let idx = ret.len() - num as usize; + let mut sequential_ones = 0; + for i in 0..12 { + if i == 11 { + if bits.pop_bit()? { panic!("malformed data! expected 0-bit terminator!"); } + break; + } else { + if bits.pop_bit()? { sequential_ones += 1; } else { break; } + } + } + bytes_to_read += (1 << sequential_ones) - 1; + bytes_to_read += bits.pop_bits(sequential_ones)? as usize; + for i in 0..bytes_to_read { + ret.push(ret[idx + i]); + } + }, + }} + Some(ret) +} + +fn parse_string<R>(r: &mut R) -> Erm<String> where R: ReadBytesExt + Seek { + let len = r.read_u32::<LE>()?; // account for null terminator + if len == 0 { return Ok(String::new()) } + let mut bs = vec![0; (len * 2 + 2) as usize]; + r.read_exact(&mut bs)?; + Ok(String::from_utf16le(&bs[0..bs.len() - 2])?) +} + +type RGB = [u8; 3]; +fn parse_rgb<R>(r: &mut R) -> Erm<RGB> where R: ReadBytesExt + Seek { + let cb = r.read_u8()?; + let cg = r.read_u8()?; + let cr = r.read_u8()?; + let _ = r.read_u8()?; + Ok([cr, cg, cb]) +} + +#[derive(Debug)] +struct Locator { + offset: u32, + size: u32, +} +impl Locator { + fn parse<R>(r: &mut R) -> Erm<Self> where R: ReadBytesExt + Seek { + Ok(Self { + offset: r.read_u32::<LE>()?, + size: r.read_u32::<LE>()?, + }) + } + fn seek<R>(&self, r: &mut R) -> Erm<()> where R: Seek { + r.seek(SeekFrom::Start(self.offset as _))?; + Ok(()) + } +} + +#[derive(Debug)] +struct GUID(u32, u16, u16, [u8; 8]); +impl GUID { + fn parse<R>(r: &mut R) -> Erm<Self> where R: ReadBytesExt + Seek { + Ok(Self( + r.read_u32::<LE>()?, + r.read_u16::<LE>()?, + r.read_u16::<LE>()?, + [ r.read_u8()?, r.read_u8()?, r.read_u8()?, r.read_u8()?, + r.read_u8()?, r.read_u8()?, r.read_u8()?, r.read_u8()? ] + )) + } +} + +#[derive(Debug)] +struct VoiceInfoExtra { + lang_id: u16, + dialect: String, + gender: u16, + age: u16, + style: String, +} +impl VoiceInfoExtra { + fn parse<R>(r: &mut R) -> Erm<Self> where R: ReadBytesExt + Seek { + Ok(Self { + lang_id: r.read_u16::<LE>()?, + dialect: parse_string(r)?, + gender: r.read_u16::<LE>()?, + age: r.read_u16::<LE>()?, + style: parse_string(r)?, + }) + } +} +#[derive(Debug)] +struct VoiceInfo { + engine_id: GUID, + mode_id: GUID, + speed: u32, + pitch: u16, + extra: Option<VoiceInfoExtra>, +} +impl VoiceInfo { + fn parse<R>(r: &mut R) -> Erm<Self> where R: ReadBytesExt + Seek { + let engine_id = GUID::parse(r)?; + let mode_id = GUID::parse(r)?; + let speed = r.read_u32::<LE>()?; + let pitch = r.read_u16::<LE>()?; + let is_extra = r.read_u8()? == 1; + let extra = if is_extra { Some(VoiceInfoExtra::parse(r)?) } else { None }; + Ok(Self { + engine_id, mode_id, + speed, pitch, + extra + }) + } +} + +#[derive(Debug)] +struct BalloonInfo { + lines: u8, + chars_per_line: u8, + foreground: RGB, + background: RGB, + border: RGB, + font_name: String, + font_height: i32, + font_weight: i32, + font_italic: bool, + font_underline: bool, +} +impl BalloonInfo { + fn parse<R>(r: &mut R) -> Erm<Self> where R: ReadBytesExt + Seek { + Ok(Self { + lines: r.read_u8()?, + chars_per_line: r.read_u8()?, + foreground: parse_rgb(r)?, + background: parse_rgb(r)?, + border: parse_rgb(r)?, + font_name: parse_string(r)?, + font_height: r.read_i32::<LE>()?, + font_weight: r.read_i32::<LE>()?, + font_italic: r.read_u8()? == 1, + font_underline: r.read_u8()? == 1, + }) + } +} + +#[derive(Debug)] +struct CharacterInfo { + minor_version: u16, + major_version: u16, + localized_info: Locator, + id: GUID, + width: u16, + height: u16, + transparent_index: u8, + flags: u32, + animation_major_version: u16, + animation_minor_version: u16, + voice_info: Option<VoiceInfo>, + balloon_info: Option<BalloonInfo>, + palette: Vec<RGB>, +} +impl CharacterInfo { + fn parse<R>(r: &mut R) -> Erm<Self> where R: ReadBytesExt + Seek { + let minor_version = r.read_u16::<LE>()?; + let major_version = r.read_u16::<LE>()?; + let localized_info = Locator::parse(r)?; + let id = GUID::parse(r)?; + let width = r.read_u16::<LE>()?; + let height = r.read_u16::<LE>()?; + let transparent_index = r.read_u8()?; + let flags = r.read_u32::<LE>()?; + let animation_major_version = r.read_u16::<LE>()?; + let animation_minor_version = r.read_u16::<LE>()?; + let voice_info = if flags >> 4 & 1 == 1 { None } else { Some(VoiceInfo::parse(r)?) }; + let balloon_info = if flags >> 8 & 1 == 1 { None } else { Some(BalloonInfo::parse(r)?) }; + let palette = { + let len = r.read_u32::<LE>()?; + (0..len).map(|_| parse_rgb(r)).collect::<Erm<Vec<RGB>>>()? + }; + Ok(Self { + minor_version, major_version, + localized_info, id, + width, height, + transparent_index, + flags, + animation_major_version, animation_minor_version, + voice_info, + balloon_info, + palette, + }) + } +} + +#[derive(Debug)] +struct FrameImage { + index: u32, + x_off: i16, + y_off: i16, +} +impl FrameImage { + fn parse<R>(r: &mut R) -> Erm<Self> where R: ReadBytesExt + Seek { + Ok(Self { + index: r.read_u32::<LE>()?, + x_off: r.read_i16::<LE>()?, + y_off: r.read_i16::<LE>()?, + }) + } + fn parse_list<R>(r: &mut R) -> Erm<Vec<Self>> where R: ReadBytesExt + Seek { + let len = r.read_u16::<LE>()?; + (0..len).map(|_| Self::parse(r)).collect() + } +} + +#[derive(Debug)] +struct BranchInfo { + index: u16, + probability: u16, +} +impl BranchInfo { + fn parse<R>(r: &mut R) -> Erm<Self> where R: ReadBytesExt + Seek { + Ok(Self { + index: r.read_u16::<LE>()?, + probability: r.read_u16::<LE>()?, + }) + } + fn parse_list<R>(r: &mut R) -> Erm<Vec<Self>> where R: ReadBytesExt + Seek { + let len = r.read_u8()?; + (0..len).map(|_| Self::parse(r)).collect() + } +} + +#[derive(Debug)] +enum OverlayType { + MouthClosed, + MouthWideOpen1, + MouthWideOpen2, + MouthWideOpen3, + MouthWideOpen4, + MouthMedium, + MouthNarrow, +} +#[derive(Debug)] +struct OverlayInfo { + ty: OverlayType, + replace_top_image: bool, + index: u16, + x_off: i16, + y_off: i16, + width: u16, + height: u16, + data: Option<Vec<u8>>, +} +impl OverlayInfo { + fn parse<R>(r: &mut R) -> Erm<Self> where R: ReadBytesExt + Seek { + let ty = match r.read_u8()? { + 0 => OverlayType::MouthClosed, + 1 => OverlayType::MouthWideOpen1, + 2 => OverlayType::MouthWideOpen2, + 3 => OverlayType::MouthWideOpen3, + 4 => OverlayType::MouthWideOpen4, + 5 => OverlayType::MouthMedium, + 6 => OverlayType::MouthNarrow, + _ => { return utils::erm_msg("invalid overlay type"); } + }; + let replace_top_image = r.read_u8()? == 1; + let index = r.read_u16::<LE>()?; + let _ = r.read_u8()?; + let has_data = r.read_u8()? == 1; + let x_off = r.read_i16::<LE>()?; + let y_off = r.read_i16::<LE>()?; + let width = r.read_u16::<LE>()?; + let height = r.read_u16::<LE>()?; + let data = if has_data { + let len = r.read_u32::<LE>()?; + let mut buf = vec![0; len as usize]; + r.read_exact(&mut buf)?; + Some(buf) + } else { None }; + Ok(Self { + ty, + replace_top_image, + index, + x_off, y_off, + width, height, + data, + }) + } + fn parse_list<R>(r: &mut R) -> Erm<Vec<Self>> where R: ReadBytesExt + Seek { + let len = r.read_u8()?; + (0..len).map(|_| Self::parse(r)).collect() + } +} + +#[derive(Debug)] +struct FrameInfo { + images: Vec<FrameImage>, + audio_index: u16, + duration: u16, + exit_frame: i16, + branches: Vec<BranchInfo>, + mouth_overlays: Vec<OverlayInfo>, +} +impl FrameInfo { + fn parse<R>(r: &mut R) -> Erm<Self> where R: ReadBytesExt + Seek { + Ok(Self { + images: FrameImage::parse_list(r)?, + audio_index: r.read_u16::<LE>()?, + duration: r.read_u16::<LE>()?, + exit_frame: r.read_i16::<LE>()?, + branches: BranchInfo::parse_list(r)?, + mouth_overlays: OverlayInfo::parse_list(r)?, + }) + } + fn parse_list<R>(r: &mut R) -> Erm<Vec<Self>> where R: ReadBytesExt + Seek { + let len = r.read_u16::<LE>()?; + (0..len).map(|_| Self::parse(r)).collect() + } +} + +#[derive(Debug)] +enum AnimationTransition {Return, ExitBranch, None} +#[derive(Debug)] +struct AnimationInfo { + name: String, + transition: AnimationTransition, + return_name: String, + frames: Vec<FrameInfo> +} +impl AnimationInfo { + fn parse<R>(r: &mut R) -> Erm<Self> where R: ReadBytesExt + Seek { + let _name = parse_string(r)?; + let loc = Locator::parse(r)?; + let pos = r.stream_position()?; + loc.seek(r)?; + let name = parse_string(r)?; + let transition = match r.read_u8()? { + 0 => AnimationTransition::Return, + 1 => AnimationTransition::ExitBranch, + 2 => AnimationTransition::None, + _ => { return utils::erm_msg("invalid animation transition type"); } + }; + let return_name = parse_string(r)?; + let frames = FrameInfo::parse_list(r)?; + r.seek(SeekFrom::Start(pos))?; + Ok(Self { + name, transition, return_name, frames + }) + } + fn parse_map<R>(r: &mut R) -> Erm<HashMap<String, Self>> where R: ReadBytesExt + Seek { + let mut ret = HashMap::new(); + let len = r.read_u32::<LE>()?; + for _ in 0..len { + let v = Self::parse(r)?; + ret.insert(v.name.clone(), v); + } + Ok(ret) + } +} + +struct ImageInfo { + width: u16, + height: u16, + image: Vec<u8>, +} +impl std::fmt::Debug for ImageInfo { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + fmt.write_fmt(format_args!("<image w: {}, h: {}>", self.width, self.height))?; + Ok(()) + } +} +impl ImageInfo { + fn size() -> u32 { 3 } + fn parse<R>(r: &mut R) -> Erm<Self> where R: ReadBytesExt + Seek { + let loc = Locator::parse(r)?; + let _ = r.read_u32::<LE>()?; + let pos = r.stream_position()?; + loc.seek(r)?; + let _ = r.read_u8()?; + let width = r.read_u16::<LE>()?; + let height = r.read_u16::<LE>()?; + let compressed = r.read_u8()? == 1; + let img_len = r.read_u32::<LE>()?; + let mut img_bytes = vec![0; img_len as usize]; + r.read_exact(&mut img_bytes)?; + r.seek(SeekFrom::Start(pos))?; + Ok(Self { + width, height, + image: if compressed { + decompress(img_bytes).ok_or_else( + || utils::Error { msg: "failed to decompress".to_owned()} + )? + } else { img_bytes } + }) + } + fn parse_list<R>(r: &mut R) -> Erm<Vec<Self>> where R: ReadBytesExt + Seek { + let len = r.read_u32::<LE>()?; + (0..len).map(|_| Self::parse(r)).collect() + } +} + +#[derive(Debug)] +struct ACS { + character_info: CharacterInfo, + animation_info: HashMap<String, AnimationInfo>, + image_info: Vec<ImageInfo>, + audio_info: Locator, +} +impl ACS { + fn parse<R>(r: &mut R) -> Erm<Self> where R: ReadBytesExt + Seek { + if r.read_u32::<LE>()? != 0xabcdabc3 { + return utils::erm_msg("invalid magic bytes"); + } + let character_info_loc = Locator::parse(r)?; + let animation_info_loc = Locator::parse(r)?; + let image_info_loc = Locator::parse(r)?; + let audio_info = Locator::parse(r)?; + Ok(Self { + character_info: { character_info_loc.seek(r)?; CharacterInfo::parse(r)? }, + animation_info: { animation_info_loc.seek(r)?; AnimationInfo::parse_map(r)? }, + image_info: { image_info_loc.seek(r)?; ImageInfo::parse_list(r)? }, + audio_info, + }) + } +} + +pub struct Overlay { + tex: texture::Texture, + acs: ACS, + anim: Option<String>, + anim_idx: usize, + border: bool, +} +impl Overlay { + pub fn new(ctx: &context::Context) -> Self { + let tex = texture::Texture::new_empty(ctx); + let bytes = include_bytes!("../assets/acs/clippy.acs"); + let mut r = std::io::Cursor::new(bytes); + let acs = ACS::parse(&mut r).unwrap(); + log::info!("{:#?}", acs.animation_info.keys().collect::<Vec<&String>>()); + Self { + tex, + acs, + anim: Some("EMPTYTRASH".to_owned()), + anim_idx: 0, + border: false, + } + } + pub fn reset_animation(&mut self) { + self.anim = None; + self.anim_idx = 0; + } + pub fn play_animation(&mut self, anim: &str) { + self.anim = Some(anim.to_owned()); + self.anim_idx = 0; + } + pub fn play_idle_animation(&mut self) { + let idle = [ + "IDLEEYEBROWRAISE", + "IDLE1_1", + "IDLESIDETOSIDE", + "IDLEFINGERTAP", + "IDLEHEADSCRATCH", + ]; + let idx = rand::thread_rng().gen_range(0..idle.len()); + self.play_animation(idle[idx]); + } +} +impl overlay::Overlay for Overlay { + fn handle_binary(&mut self, ctx: &context::Context, st: &mut state::State, ost: &mut overlay::State, msg: &net::fig::BinaryMessage) -> Erm<()> { + match &*msg.event { + b"overlay clippy animate" => { + let res: Erm<()> = try { + let mut reader = std::io::Cursor::new(&msg.data); + let anim = net::fig::read_length_prefixed_utf8(&mut reader)?; + self.play_animation(&anim); + () + }; + if let Err(e) = res { log::warn!("malformed clippy update: {}", e); } + }, + b"overlay clippy border on" => self.border = true, + b"overlay clippy border off" => self.border = false, + _ => {}, + } + Ok(()) + } + fn update(&mut self, ctx: &context::Context, st: &mut state::State, ost: &mut overlay::State) -> Erm<()> { + let anim = self.anim.as_deref().unwrap_or("RESTPOSE"); + if let Some(ainfo) = &self.acs.animation_info.get(anim) { + let frames = &ainfo.frames; + let f = &frames[self.anim_idx]; + if f.images.len() > 0 { + let idx = f.images[0].index as usize; + let colors: Vec<u8> = self.acs.image_info[idx].image.iter().flat_map(|idx| { + if *idx == self.acs.character_info.transparent_index { + [0, 0, 0, 0] + } else { + let c = self.acs.character_info.palette[*idx as usize]; + [c[0], c[1], c[2], 255] + } + }).collect(); + self.tex.upload_rgba8(ctx, + self.acs.image_info[idx].width as _, + self.acs.image_info[idx].height as _, + &colors + ); + } + if st.tick % 6 == 0 { + if self.anim_idx < frames.len() - 1 { + self.anim_idx += 1; + } else { + self.reset_animation(); + } + } + } else { + self.reset_animation(); + } + if self.anim.is_none() { + if rand::thread_rng().gen_ratio(1, 10 * 60) { + self.play_idle_animation(); + } + } + Ok(()) + } + fn render(&mut self, ctx: &context::Context, st: &mut state::State, ost: &mut overlay::State) -> Erm<()> { + st.bind_2d(ctx, &ost.assets.shader_acs); + let w = 3.0 * self.acs.character_info.width as f32; + let h = 3.0 * self.acs.character_info.height as f32; + ost.assets.shader_acs.set_position_2d_helper(ctx, st, + &(st.render_dims - glam::Vec2::new(w, h)), + &glam::Vec2::new(w, h), + // &glam::Quat::from_rotation_z(st.tick as f32 / 100.0), + &glam::Quat::IDENTITY, + ); + self.tex.bind(ctx); + st.mesh_square.render(ctx); + if self.border { + st.bind_2d(ctx, &ost.assets.shader_flat); + ost.assets.shader_flat.set_position_2d_helper(ctx, st, + &glam::Vec2::new(1590.0, 541.0), + &glam::Vec2::new(295.0, 238.0), + &glam::Quat::IDENTITY, + ); + ost.assets.texture_clippyborder.bind(ctx); + st.mesh_square.render(ctx); + } + Ok(()) + } +} diff --git a/crates/renderer/src/overlay/combo.rs b/crates/renderer/src/overlay/combo.rs new file mode 100644 index 0000000..f200e22 --- /dev/null +++ b/crates/renderer/src/overlay/combo.rs @@ -0,0 +1,216 @@ +use std::collections::HashMap; + +use rand::Rng; +use teleia::*; + +use crate::overlay; + +use enum_map::{enum_map, Enum, EnumMap}; +use strum::{EnumIter, IntoEnumIterator}; + +const MAX_SCORE: f32 = 12800.0; + +const WIDTH: i64 = 250; +const HEIGHT: i64 = 400; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Enum, EnumIter)] +enum Rank {SSS, SS, S, A, B, C, D, None} +impl Rank { + fn threshold(self) -> f32 { + match self { + Self::SSS => 2800.0, + Self::SS => 2100.0, + Self::S => 1500.0, + Self::A => 1000.0, + Self::B => 600.0, + Self::C => 300.0, + Self::D => 100.0, + Self::None => 0.0, + } + } + fn multiplier(self) -> f32 { + match self { + Self::SSS => 16.0, + Self::SS => 6.0, + Self::S => 5.0, + Self::A => 4.0, + Self::B => 3.0, + Self::C => 2.0, + Self::D => 1.5, + Self::None => 1.0, + } + } +} + +pub struct Overlay { + font: font::Bitmap, + fb: framebuffer::Framebuffer, + rank_textures: EnumMap<Rank, texture::Texture>, + shake: u64, + score: f32, + tags: HashMap<String, u64>, +} +impl Overlay { + pub fn new(ctx: &context::Context) -> Self { + let fb = framebuffer::Framebuffer::new(ctx, &glam::Vec2::new(WIDTH as f32, HEIGHT as f32), &glam::Vec2::new(0.0, 0.0)); + Self { + fb, + font: font::Bitmap::from_image(ctx, 6, 12, 96, 72, include_bytes!("../assets/fonts/terminus.png")), + rank_textures: enum_map! { + Rank::SSS => texture::Texture::new(ctx, include_bytes!("../assets/textures/combo/sss.webp")), + Rank::SS => texture::Texture::new(ctx, include_bytes!("../assets/textures/combo/ss.webp")), + Rank::S => texture::Texture::new(ctx, include_bytes!("../assets/textures/combo/s.webp")), + Rank::A => texture::Texture::new(ctx, include_bytes!("../assets/textures/combo/a.webp")), + Rank::B => texture::Texture::new(ctx, include_bytes!("../assets/textures/combo/b.webp")), + Rank::C => texture::Texture::new(ctx, include_bytes!("../assets/textures/combo/c.webp")), + Rank::D => texture::Texture::new(ctx, include_bytes!("../assets/textures/combo/d.webp")), + Rank::None => texture::Texture::new(ctx, include_bytes!("../assets/textures/test.png")), + }, + shake: 0, + score: 0.0, + tags: HashMap::new(), + } + } + fn rank(&self) -> (Rank, f32) { + let mut prev = MAX_SCORE; + for r in Rank::iter() { + let thresh = r.threshold(); + if self.score > thresh { + return (r, (self.score - thresh) / (prev - thresh)); + } + prev = thresh; + } + return (Rank::None, 0.0); + } + fn tag_multiplier(&self) -> f32 { + return 1.0 + self.tags.len() as f32; + } + fn shake_offset(&self, r: Rank) -> f32 { + rand::thread_rng().gen_range(0..10) as f32 / 100.0 * r.multiplier() + } +} +impl overlay::Overlay for Overlay { + fn handle_binary(&mut self, ctx: &context::Context, st: &mut state::State, ost: &mut overlay::State, msg: &net::fig::BinaryMessage) -> Erm<()> { + match &*msg.event { + b"overlay combo message" => { + let (oldrank, _) = self.rank(); + self.score += 267.0 * self.tag_multiplier(); + let (newrank, _) = self.rank(); + if oldrank != newrank { + self.shake = 20; + } + if let Ok(ts) = String::from_utf8(msg.data.clone()) { + for t in ts.split(",") { + if t.len() > 0 { + self.tags.entry(t.to_owned()).and_modify(|x| *x += 1).or_insert(1); + } + } + } + }, + _ => {}, + } + Ok(()) + } + fn update(&mut self, ctx: &context::Context, st: &mut state::State, ost: &mut overlay::State) -> Erm<()> { + let (rank, _) = self.rank(); + self.score -= 2.0 * rank.multiplier(); + if self.score < 0.0 { + self.score = 0.0; + self.tags.clear(); + } + if self.score > MAX_SCORE { self.score = MAX_SCORE; } + if self.shake > 0 { self.shake -= 1; } + Ok(()) + } + fn render(&mut self, ctx: &context::Context, st: &mut state::State, ost: &mut overlay::State) -> Erm<()> { + let (rank, meter) = self.rank(); + if rank == Rank::None { return Ok(()); } + + st.bind_framebuffer(ctx, &self.fb); + ctx.clear_color(glam::Vec4::new(0.0, 0.0, 0.0, 0.8)); + ctx.clear(); + + // rank name + st.bind_2d(ctx, &ost.assets.shader_flat); + ost.assets.shader_flat.set_position_2d( + ctx, st, + &glam::Vec2::new(-4.0, 4.0), + &glam::Vec2::new(WIDTH as f32, WIDTH as f32 / 3.0), + ); + self.rank_textures[rank].bind(ctx); + st.mesh_square.render(ctx); + + // meter + st.bind_2d(ctx, &ost.assets.shader_color); + ost.assets.shader_color.set_position_2d( + ctx, st, + &glam::Vec2::new(0.0, 100.0), + &glam::Vec2::new(WIDTH as f32, 16.0), + ); + ost.assets.shader_color.set_vec4(ctx, "color", &glam::Vec4::new(0.0, 0.0, 0.0, 1.0)); + st.mesh_square.render(ctx); + ost.assets.shader_color.set_position_2d( + ctx, st, + &glam::Vec2::new(0.0, 100.0), + &glam::Vec2::new(WIDTH as f32 * meter, 16.0), + ); + ost.assets.shader_color.set_vec4(ctx, "color", &glam::Vec4::new(1.0, 1.0, 1.0, 1.0)); + st.mesh_square.render(ctx); + + // tags + let mut tagy = 124.0; + for (t, count) in self.tags.iter() { + self.font.render_text_parameterized( + ctx, st, + &glam::Vec2::new(4.0, tagy), + &format!("+ {t} (x{count})"), + font::BitmapParams { + color: &[glam::Vec3::new(1.0, 1.0, 1.0)], + scale: glam::Vec2::new(2.0, 2.0), + }, + ); + tagy += 24.0; + } + + // multiplier + self.font.render_text_parameterized( + ctx, st, + &glam::Vec2::new(4.0, HEIGHT as f32 - 50.0), + &format!("MULT:"), + font::BitmapParams { + color: &[glam::Vec3::new(1.0, 1.0, 1.0)], + scale: glam::Vec2::new(4.0, 4.0), + }, + ); + self.font.render_text_parameterized( + ctx, st, + &glam::Vec2::new(128.0, HEIGHT as f32 - 50.0), + &format!("{}x", self.tag_multiplier()), + font::BitmapParams { + color: &[glam::Vec3::new(1.0, 0.7, 0.0)], + scale: glam::Vec2::new(4.0, 4.0), + }, + ); + + // actually draw box to screen + st.bind_render_framebuffer(ctx); + ctx.clear_depth(); + st.bind_3d(ctx, &ost.assets.shader_flat_noflip); + let shake_offset = if self.shake > 0 { + glam::Vec3::new(self.shake_offset(rank), 0.0, self.shake_offset(rank)) + } else { + glam::Vec3::ZERO + }; + ost.assets.shader_flat_noflip.set_position_3d( + ctx, st, + &glam::Mat4::from_scale_rotation_translation( + glam::Vec3::new(WIDTH as f32 / HEIGHT as f32, 1.0, 1.0), + glam::Quat::from_rotation_y(std::f32::consts::PI + 0.7), + glam::Vec3::new(-5.5, 1.0, -8.0) + shake_offset, + ), + ); + self.fb.bind_texture(ctx); + st.mesh_square.render(ctx); + Ok(()) + } +} diff --git a/crates/renderer/src/overlay/model.rs b/crates/renderer/src/overlay/model.rs index d74870e..9c4b51b 100644 --- a/crates/renderer/src/overlay/model.rs +++ b/crates/renderer/src/overlay/model.rs @@ -81,7 +81,7 @@ impl Overlay { } impl overlay::Overlay for Overlay { - fn handle_binary(&mut self, ctx: &context::Context, st: &mut state::State, ost: &mut overlay::State, msg: &fig::BinaryMessage) -> Erm<()> { + fn handle_binary(&mut self, ctx: &context::Context, st: &mut state::State, ost: &mut overlay::State, msg: &net::fig::BinaryMessage) -> Erm<()> { if msg.event == b"overlay avatar text" { match str::from_utf8(&msg.data) { Ok(s) => self.terminal.fill_string(s), diff --git a/crates/renderer/src/overlay/shader.rs b/crates/renderer/src/overlay/shader.rs index 2bed8d6..b30242f 100644 --- a/crates/renderer/src/overlay/shader.rs +++ b/crates/renderer/src/overlay/shader.rs @@ -2,8 +2,11 @@ use teleia::*; use crate::{overlay, toggle}; +use glow::HasContext; + pub struct Overlay { visualizer: newton_shader::Visualizer, + fb: framebuffer::Framebuffer, } impl Overlay { @@ -11,6 +14,7 @@ impl Overlay { let visualizer = newton_shader::Visualizer::new(); Self { visualizer, + fb: framebuffer::Framebuffer::new(ctx, &glam::Vec2::new(640.0, 360.0), &glam::Vec2::ZERO), } } } @@ -21,12 +25,12 @@ impl overlay::Overlay for Overlay { self.visualizer.shader = None; Ok(()) } - fn handle_binary(&mut self, ctx: &context::Context, st: &mut state::State, ost: &mut overlay::State, msg: &fig::BinaryMessage) -> Erm<()> { + fn handle_binary(&mut self, ctx: &context::Context, st: &mut state::State, ost: &mut overlay::State, msg: &net::fig::BinaryMessage) -> Erm<()> { if msg.event == b"overlay shader" { let res: Erm<()> = (|| { let mut reader = std::io::Cursor::new(&msg.data); - let author = fig::read_length_prefixed_utf8(&mut reader)?; - let shader = fig::read_length_prefixed_utf8(&mut reader)?; + let author = net::fig::read_length_prefixed_utf8(&mut reader)?; + let shader = net::fig::read_length_prefixed_utf8(&mut reader)?; self.visualizer.author = author.to_string(); if let Err(e) = self.visualizer.set(ctx, st, &shader) { log::warn!("error compiling shader: {}", e); @@ -40,6 +44,11 @@ impl overlay::Overlay for Overlay { } fn render(&mut self, ctx: &context::Context, st: &mut state::State, ost: &mut overlay::State) -> Erm<()> { if let Some(s) = &self.visualizer.shader { + self.fb.bind(ctx); + unsafe { + ctx.gl.disable(glow::BLEND); + } + ctx.clear(); s.bind(ctx); s.set_f32( ctx, "opacity", @@ -51,17 +60,30 @@ impl overlay::Overlay for Overlay { 0.5 } ); - s.set_vec2(ctx, "resolution", &glam::Vec2::new(ctx.render_width, ctx.render_height)); + s.set_vec2(ctx, "resolution", &st.render_dims); let elapsed = (st.tick - self.visualizer.tickset) as f32 / 60.0; s.set_f32(ctx, "time", elapsed); s.set_f32(ctx, "chat_time", ost.chat.time - self.visualizer.timeset as f32); - ctx.render_no_geometry(); s.set_f32(ctx, "tracking_mouth", ost.tracking.mouth); s.set_vec2(ctx, "tracking_eyes", &glam::Vec2::new(ost.tracking.eyes.0, ost.tracking.eyes.1)); s.set_mat4(ctx, "tracking_neck", &glam::Mat4::from_quat(ost.tracking.neck)); s.set_vec2(ctx, "emacs_cursor", &glam::Vec2::new(ost.info.emacs_cursor.0, ost.info.emacs_cursor.1)); s.set_vec2(ctx, "mouse_cursor", &glam::Vec2::new(ost.info.mouse_cursor.0, ost.info.mouse_cursor.1)); s.set_i32(ctx, "heartrate", ost.info.emacs_heartrate); + ctx.render_no_geometry(); + unsafe { + ctx.gl.enable(glow::BLEND); + } + + st.render_framebuffer.bind(ctx); + st.bind_2d(ctx, &ost.assets.shader_flat_noflip); + self.fb.bind_texture(ctx); + ost.assets.shader_flat_noflip.set_position_2d( + ctx, st, + &glam::Vec2::new(0.0, 0.0), + &glam::Vec2::new(1920.0, 1080.0) + ); + st.mesh_square.render(ctx); } if let Some(t@toggle::Toggle { val: true, .. }) = ost.toggles.get(ctx, st, "adblock") { st.bind_2d(ctx, &ost.assets.shader_flat); diff --git a/crates/renderer/src/overlay/tcg.rs b/crates/renderer/src/overlay/tcg.rs index 7361e86..ece01e8 100644 --- a/crates/renderer/src/overlay/tcg.rs +++ b/crates/renderer/src/overlay/tcg.rs @@ -1,6 +1,8 @@ +use sha2::Digest; use teleia::*; -use std::{cell::RefCell, io::Write, rc::Rc}; +use std::{cell::RefCell, rc::Rc}; +use std::fmt::Write as _; use redis::Commands; use image::EncodableLayout; @@ -129,7 +131,7 @@ impl RenderedCardSlot { struct ImageWrite { buf: Rc<RefCell<Vec<u8>>>, } -impl Write for ImageWrite { +impl std::io::Write for ImageWrite { fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { self.buf.borrow_mut().write(buf) } @@ -138,6 +140,7 @@ impl Write for ImageWrite { struct ImageEncoder { frames: u32, frames_left: u32, + frame_hashes: Vec<u8>, buf: Rc<RefCell<Vec<u8>>>, writer: png::Writer<ImageWrite>, } @@ -164,12 +167,14 @@ impl ImageEncoder { Some(Self { frames, frames_left: frames, + frame_hashes: Vec::new(), buf, writer, }) } fn write_frame(&mut self, pixels: &[u8]) { if self.frames_left > 0 { + self.frame_hashes.extend_from_slice(sha2::Sha256::digest(pixels).as_slice()); let _ = self.writer.write_image_data(&pixels); self.frames_left -= 1; } @@ -177,10 +182,10 @@ impl ImageEncoder { fn is_finished(&self) -> bool { self.frames_left == 0 } - fn finish(self) -> Option<Vec<u8>> { + fn finish(self) -> Option<(Vec<u8>, Vec<u8>)> { if self.is_finished() { self.writer.finish().expect("failed to finish"); - Some(self.buf.replace(Vec::new())) + Some((self.buf.replace(Vec::new()), self.frame_hashes)) } else { None } } } @@ -283,15 +288,24 @@ impl Marquee { } } fn upload_card(ctx: &context::Context, st: &mut state::State, ost: &mut overlay::State, - c: &Card, buf: &[u8] + c: &Card, buf: &[u8], hashes: &[u8], ) -> Erm<()> { - let with_meta = web_image_meta::png::add_text_chunk( - buf, "lcolonqtcg", &c.encoded, + let mut signed = Vec::from(c.encoded.as_bytes()); + signed.extend(hashes); + let signature = sign(&mut ost.redis_conn, &signed)?; + let mut res = "+SIGNED+".to_owned(); + for s in signature { + write!(res, "{:02x}", s)?; + } + res.push_str("+"); + res.push_str(&c.encoded); + let with_signature = web_image_meta::png::add_text_chunk( + buf, "lcolonqtcg", &res, )?; let uuid = uuid::Uuid::new_v4(); - let _: () = ost.redis_conn.hset("tcg:cards", uuid.to_string(), &with_meta)?; + let () = ost.redis_conn.hset("tcg:cards", uuid.to_string(), &with_signature)?; let inventory_key = format!("tcg-inventory:{}", c.owner_id); - let _: () = ost.redis_conn.lpush(inventory_key, uuid.to_string())?; + let () = ost.redis_conn.lpush(inventory_key, uuid.to_string())?; Ok(()) } pub fn render(&mut self, @@ -299,9 +313,9 @@ impl Marquee { renderer: &CardRenderer, ) { for s in self.slots.iter_mut() { - if let Some(b) = s.encoder.take_if(|e| e.is_finished()).and_then(|enc| enc.finish()) { + if let Some((b, h)) = s.encoder.take_if(|e| e.is_finished()).and_then(|enc| enc.finish()) { if let Some(c) = &s.card.card { - let _ = Self::upload_card(ctx, st, ost, &c, &b).expect("failed to upload"); + let _ = Self::upload_card(ctx, st, ost, &c, &b, &h).expect("failed to upload"); } } } @@ -554,7 +568,7 @@ impl overlay::Overlay for Overlay { fn reset(&mut self, ctx: &context::Context, st: &mut state::State, ost: &mut overlay::State) -> Erm<()> { Ok(()) } - fn handle_binary(&mut self, ctx: &context::Context, st: &mut state::State, ost: &mut overlay::State, msg: &fig::BinaryMessage) -> Erm<()> { + fn handle_binary(&mut self, ctx: &context::Context, st: &mut state::State, ost: &mut overlay::State, msg: &net::fig::BinaryMessage) -> Erm<()> { match &*msg.event { b"overlay tcg generate" => { let res: Erm<()> = (|| { @@ -621,3 +635,87 @@ impl overlay::Overlay for Overlay { Ok(()) } } + +pub fn sign(conn: &mut redis::Connection, data: &[u8]) -> Erm<Vec<u8>> { + let mut secret: Vec<u8> = conn.get("tcg:secret")?; + secret.extend_from_slice(data); + Ok(Vec::from(sha2::Sha256::digest(&secret).as_slice())) +} + +pub fn validate_card(png: &[u8]) -> Erm<bool> { + let decoder = png::Decoder::new(png); + let mut reader = decoder.read_info()?; + let mut buf = vec![0; reader.output_buffer_size()]; + let mut frame_hashes = Vec::new(); + while let Ok(f) = reader.next_frame(&mut buf) { + frame_hashes.extend_from_slice(sha2::Sha256::digest(&buf).as_slice()); + } + reader.finish()?; + for tc in reader.info().uncompressed_latin1_text.iter() { + if tc.keyword == "lcolonqtcg" { + eprintln!("{}", tc.text); + let (sig, meta) = if let Some(suffix) = tc.text.strip_prefix("+SIGNED+") { + suffix.split_once("+").unwrap_or(("", suffix)) + } else { + ("", &*tc.text) + }; + let mut to_sign = Vec::from(meta); + to_sign.extend_from_slice(&frame_hashes); + eprintln!("to_sign: {:?}", to_sign); + let signed = { + let redis = redis::Client::open("redis://shiro").unwrap(); + let mut redis_conn = redis.get_connection().unwrap(); + sign(&mut redis_conn, &to_sign)? + }; + let mut computed_sig = String::new(); + for s in signed { + write!(computed_sig, "{:02x}", s)?; + } + eprintln!("attached signature: {}", sig); + eprintln!("computed signature: {}", computed_sig); + return Ok(sig == computed_sig); + } + } + Ok(false) +} + +pub fn repair_card(png: &[u8]) -> Erm<Vec<u8>> { + let decoder = png::Decoder::new(png); + let mut reader = decoder.read_info()?; + let mut buf = vec![0; reader.output_buffer_size()]; + let mut frame_hashes = Vec::new(); + while let Ok(f) = reader.next_frame(&mut buf) { + frame_hashes.extend_from_slice(sha2::Sha256::digest(&buf).as_slice()); + } + reader.finish()?; + for tc in reader.info().uncompressed_latin1_text.iter() { + if tc.keyword == "lcolonqtcg" { + let (sig, meta) = if let Some(suffix) = tc.text.strip_prefix("+SIGNED+") { + suffix.split_once("+").unwrap_or(("", suffix)) + } else { + ("", &*tc.text) + }; + let mut to_sign = Vec::from(meta); + to_sign.extend_from_slice(&frame_hashes); + eprintln!("to_sign: {:?}", to_sign); + let signed = { + let redis = redis::Client::open("redis://shiro").unwrap(); + let mut redis_conn = redis.get_connection().unwrap(); + sign(&mut redis_conn, &to_sign)? + }; + let mut computed_sig = String::new(); + for s in signed { + write!(computed_sig, "{:02x}", s)?; + } + if sig != computed_sig { + eprintln!("repairing, new signature: {}", computed_sig); + eprintln!("old signature: {}", sig); + let clean = web_image_meta::png::clean_chunks(png)?; + return Ok(web_image_meta::png::add_text_chunk( + &clean, "lcolonqtcg", &format!("+SIGNED+{}+{}", computed_sig, meta), + )?); + } + } + } + Ok(Vec::from(png)) +} diff --git a/crates/renderer/src/texture_server.rs b/crates/renderer/src/texture_server.rs new file mode 100644 index 0000000..5afad70 --- /dev/null +++ b/crates/renderer/src/texture_server.rs @@ -0,0 +1,80 @@ +use teleia::*; + +use std::io::Write; +use std::os::fd::FromRawFd; +use std::os::unix::net::{AncillaryData, SocketAncillary}; +use std::sync::mpsc::{Receiver, channel}; +use std::{os::unix::net::UnixDatagram, thread::{spawn, JoinHandle}}; + +use glow::HasContext; +use crate::ffi::egl; +use crate::ffi::glfw; + +const KEY: usize = 42; +const SOCK_PATH: &'static str = "/tmp/newton.sock"; + + +pub struct Server { + pub tex: texture::Texture, + thread: JoinHandle<()>, + receiver: Receiver<i32>, +} +impl Server { + pub fn new(ctx: &context::Context) -> Self { + std::fs::remove_file(SOCK_PATH).expect("failed to remove socket"); + let sock = UnixDatagram::bind(SOCK_PATH).expect("failed to bind texture server socket"); + + let (sender, receiver) = channel::<i32>(); + let thread = spawn(move || { + let mut ancillary_buf = [0; 128]; + let mut ancillary = SocketAncillary::new(&mut ancillary_buf); + loop { + match sock.recv_vectored_with_ancillary(&mut [], &mut ancillary) { + Ok(_) => { + for ar in ancillary.messages() { + match ar { + Ok(AncillaryData::ScmRights(mut sr)) => { + if let Some(fd) = sr.next() { + if let Err(e) = sender.send(fd) { + log::warn!("error on channel: {:?}", e); + } + } + }, + Ok(_) => log::info!("message is not SCM_RIGHTS"), + Err(e) => log::warn!("failed to read ancillary message: {:?}", e), + } + } + }, + Err(e) => log::warn!("texture server failed to receive: {}", e), + } + } + }); + Self { + tex: texture::Texture::new_empty(ctx), + thread, + receiver, + } + } + pub fn update(&self, ctx: &context::Context) { + if let Ok(fd) = self.receiver.try_recv() { + log::info!("received file descriptor: {}", fd); + unsafe { + let window = ctx.window.borrow_mut().0.ptr; + let d = glfw::glfwGetEGLDisplay(); + let c = glfw::glfwGetEGLContext(window); + let img = egl::eglCreateImage(d, c, egl::EGL_LINUX_DMA_BUF_EXT, std::ptr::null_mut(), [ + egl::EGL_WIDTH as _, 16, + egl::EGL_HEIGHT as _, 16, + egl::EGL_LINUX_DRM_FOURCC_EXT as _, 875708993, + egl::EGL_DMA_BUF_PLANE0_FD_EXT as _, fd as _, + egl::EGL_DMA_BUF_PLANE0_OFFSET_EXT as _, 0, + egl::EGL_DMA_BUF_PLANE0_PITCH_EXT as _, 128, + egl::EGL_NONE as _, + ].as_ptr()); + self.tex.bind(ctx); + egl::glEGLImageTargetTexture2DOES(glow::TEXTURE_2D, img); + log::info!("set up texture"); + } + } + } +} diff --git a/crates/renderer/src/toggle.rs b/crates/renderer/src/toggle.rs index 8468fc4..7add3ca 100644 --- a/crates/renderer/src/toggle.rs +++ b/crates/renderer/src/toggle.rs @@ -27,7 +27,7 @@ impl Toggles { } pub fn handle(&mut self, ctx: &context::Context, st: &state::State, - msg: fig::BinaryMessage + msg: net::fig::BinaryMessage ) { let nm = if let Ok(s) = str::from_utf8(&msg.data) { s } else { log::warn!("failed to decode toggle name"); @@ -38,7 +38,7 @@ impl Toggles { } pub fn handle_set(&mut self, ctx: &context::Context, st: &state::State, - msg: fig::BinaryMessage, val: bool + msg: net::fig::BinaryMessage, val: bool ) { let nm = if let Ok(s) = str::from_utf8(&msg.data) { s } else { log::warn!("failed to decode toggle name"); |
