summaryrefslogtreecommitdiff
path: root/crates/renderer/src
diff options
context:
space:
mode:
authorLLLL Colonq <llll@colonq>2026-04-26 22:56:52 -0400
committerLLLL Colonq <llll@colonq>2026-04-26 22:59:25 -0400
commitd943ba194b3cfab18354e96f7be2c1e434d6c073 (patch)
tree786b7c92f3d9abc6a147a59e440982306fc77f55 /crates/renderer/src
parent4a22d2573cd4014c3cc0ed784dd2e9d6bed7fb67 (diff)
Update
Diffstat (limited to 'crates/renderer/src')
-rw-r--r--crates/renderer/src/assets.rs16
-rw-r--r--crates/renderer/src/assets/acs/bonzi.acsbin0 -> 5249795 bytes
-rw-r--r--crates/renderer/src/assets/acs/clippy.acsbin0 -> 2904417 bytes
-rw-r--r--crates/renderer/src/assets/acs/pikachu.acsbin0 -> 890717 bytes
-rw-r--r--crates/renderer/src/assets/shaders/acs/frag.glsl8
-rw-r--r--crates/renderer/src/assets/shaders/acs/vert.glsl4
-rw-r--r--crates/renderer/src/assets/shaders/flat_noflip/frag.glsl7
-rw-r--r--crates/renderer/src/assets/shaders/flat_noflip/vert.glsl4
-rw-r--r--crates/renderer/src/assets/textures/clippyborder.pngbin0 -> 1063 bytes
-rw-r--r--crates/renderer/src/assets/textures/combo/a.webpbin0 -> 1538 bytes
-rw-r--r--crates/renderer/src/assets/textures/combo/b.webpbin0 -> 1100 bytes
-rw-r--r--crates/renderer/src/assets/textures/combo/c.webpbin0 -> 1126 bytes
-rw-r--r--crates/renderer/src/assets/textures/combo/d.webpbin0 -> 1470 bytes
-rw-r--r--crates/renderer/src/assets/textures/combo/s.webpbin0 -> 1304 bytes
-rw-r--r--crates/renderer/src/assets/textures/combo/ss.webpbin0 -> 2068 bytes
-rw-r--r--crates/renderer/src/assets/textures/combo/sss.webpbin0 -> 2806 bytes
-rw-r--r--crates/renderer/src/ffi/egl.rs41
-rw-r--r--crates/renderer/src/ffi/glfw.rs7
-rw-r--r--crates/renderer/src/ffi/mod.rs2
-rw-r--r--crates/renderer/src/main.rs43
-rw-r--r--crates/renderer/src/overlay.rs21
-rw-r--r--crates/renderer/src/overlay/automata.rs6
-rw-r--r--crates/renderer/src/overlay/clippy.rs605
-rw-r--r--crates/renderer/src/overlay/combo.rs216
-rw-r--r--crates/renderer/src/overlay/model.rs2
-rw-r--r--crates/renderer/src/overlay/shader.rs32
-rw-r--r--crates/renderer/src/overlay/tcg.rs122
-rw-r--r--crates/renderer/src/texture_server.rs80
-rw-r--r--crates/renderer/src/toggle.rs4
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
new file mode 100644
index 0000000..b463dc8
--- /dev/null
+++ b/crates/renderer/src/assets/acs/bonzi.acs
Binary files differ
diff --git a/crates/renderer/src/assets/acs/clippy.acs b/crates/renderer/src/assets/acs/clippy.acs
new file mode 100644
index 0000000..0dbc388
--- /dev/null
+++ b/crates/renderer/src/assets/acs/clippy.acs
Binary files differ
diff --git a/crates/renderer/src/assets/acs/pikachu.acs b/crates/renderer/src/assets/acs/pikachu.acs
new file mode 100644
index 0000000..4e7615d
--- /dev/null
+++ b/crates/renderer/src/assets/acs/pikachu.acs
Binary files differ
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
new file mode 100644
index 0000000..3bb3089
--- /dev/null
+++ b/crates/renderer/src/assets/textures/clippyborder.png
Binary files differ
diff --git a/crates/renderer/src/assets/textures/combo/a.webp b/crates/renderer/src/assets/textures/combo/a.webp
new file mode 100644
index 0000000..15d4042
--- /dev/null
+++ b/crates/renderer/src/assets/textures/combo/a.webp
Binary files differ
diff --git a/crates/renderer/src/assets/textures/combo/b.webp b/crates/renderer/src/assets/textures/combo/b.webp
new file mode 100644
index 0000000..3eec6b1
--- /dev/null
+++ b/crates/renderer/src/assets/textures/combo/b.webp
Binary files differ
diff --git a/crates/renderer/src/assets/textures/combo/c.webp b/crates/renderer/src/assets/textures/combo/c.webp
new file mode 100644
index 0000000..1babbdf
--- /dev/null
+++ b/crates/renderer/src/assets/textures/combo/c.webp
Binary files differ
diff --git a/crates/renderer/src/assets/textures/combo/d.webp b/crates/renderer/src/assets/textures/combo/d.webp
new file mode 100644
index 0000000..1d08e9c
--- /dev/null
+++ b/crates/renderer/src/assets/textures/combo/d.webp
Binary files differ
diff --git a/crates/renderer/src/assets/textures/combo/s.webp b/crates/renderer/src/assets/textures/combo/s.webp
new file mode 100644
index 0000000..faec75a
--- /dev/null
+++ b/crates/renderer/src/assets/textures/combo/s.webp
Binary files differ
diff --git a/crates/renderer/src/assets/textures/combo/ss.webp b/crates/renderer/src/assets/textures/combo/ss.webp
new file mode 100644
index 0000000..62fec55
--- /dev/null
+++ b/crates/renderer/src/assets/textures/combo/ss.webp
Binary files differ
diff --git a/crates/renderer/src/assets/textures/combo/sss.webp b/crates/renderer/src/assets/textures/combo/sss.webp
new file mode 100644
index 0000000..f1ecb80
--- /dev/null
+++ b/crates/renderer/src/assets/textures/combo/sss.webp
Binary files differ
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");