diff options
| author | LLLL Colonq <llll@colonq> | 2026-04-26 22:56:52 -0400 |
|---|---|---|
| committer | LLLL Colonq <llll@colonq> | 2026-04-26 22:59:25 -0400 |
| commit | d943ba194b3cfab18354e96f7be2c1e434d6c073 (patch) | |
| tree | 786b7c92f3d9abc6a147a59e440982306fc77f55 /crates/renderer/src/overlay/clippy.rs | |
| parent | 4a22d2573cd4014c3cc0ed784dd2e9d6bed7fb67 (diff) | |
Update
Diffstat (limited to 'crates/renderer/src/overlay/clippy.rs')
| -rw-r--r-- | crates/renderer/src/overlay/clippy.rs | 605 |
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(()) + } +} |
