summaryrefslogtreecommitdiff
path: root/crates/renderer/src/overlay
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/overlay
parent4a22d2573cd4014c3cc0ed784dd2e9d6bed7fb67 (diff)
Update
Diffstat (limited to 'crates/renderer/src/overlay')
-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
6 files changed, 962 insertions, 21 deletions
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))
+}