summaryrefslogtreecommitdiff
path: root/crates/renderer/src/overlay/clippy.rs
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/clippy.rs
parent4a22d2573cd4014c3cc0ed784dd2e9d6bed7fb67 (diff)
Update
Diffstat (limited to 'crates/renderer/src/overlay/clippy.rs')
-rw-r--r--crates/renderer/src/overlay/clippy.rs605
1 files changed, 605 insertions, 0 deletions
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(())
+ }
+}