//! Native gesture asset parsing and step types. #![allow(clippy::inherent_to_string)] // Public names mirror C# ToString mappings. #![allow(clippy::missing_errors_doc)] // Result shapes are fixed by the compatibility API. use crate::{Error, assets::GestureStepType}; use libremetaverse_types::{AssetType, UUID}; use std::fmt::Write as _; #[derive(Clone, Debug, PartialEq)] pub enum GestureStep { Animation(GestureStepAnimation), Sound(GestureStepSound), Chat(GestureStepChat), Wait(GestureStepWait), EOF(GestureStepEOF), } impl GestureStep { #[must_use] pub const fn gesture_step_type(&self) -> GestureStepType { match self { Self::Animation(_) => GestureStepType::Animation, Self::Sound(_) => GestureStepType::Sound, Self::Chat(_) => GestureStepType::Chat, Self::Wait(_) => GestureStepType::Wait, Self::EOF(_) => GestureStepType::EOF, } } } #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct GestureStepAnimation { pub animation_start: bool, pub id: UUID, pub name: String, } impl GestureStepAnimation { pub fn new() -> Result { Ok(Self { animation_start: true, ..Self::default() }) } #[must_use] pub fn to_string(&self) -> String { format!( "{} animation: {}", if self.animation_start { "Start" } else { "Stop" }, self.name ) } #[must_use] pub const fn gesture_step_type(&self) -> GestureStepType { GestureStepType::Animation } } #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct GestureStepSound { pub id: UUID, pub name: String, } impl GestureStepSound { pub fn new() -> Result { Ok(Self::default()) } #[must_use] pub fn to_string(&self) -> String { format!("Sound: {}", self.name) } #[must_use] pub const fn gesture_step_type(&self) -> GestureStepType { GestureStepType::Sound } } #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct GestureStepChat { pub text: String, } impl GestureStepChat { pub fn new() -> Result { Ok(Self::default()) } #[must_use] pub fn to_string(&self) -> String { format!("Chat: {}", self.text) } #[must_use] pub const fn gesture_step_type(&self) -> GestureStepType { GestureStepType::Chat } } #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct GestureStepWait { pub wait_for_animation: bool, pub wait_for_time: bool, pub wait_time: f32, } impl GestureStepWait { pub const fn new() -> Result { Ok(Self { wait_for_animation: false, wait_for_time: false, wait_time: 0.0, }) } #[must_use] pub fn to_string(&self) -> String { let mut result = String::from("-- Wait for: "); if self.wait_for_animation { result.push_str("(animations to finish) "); } if self.wait_for_time { let _ = write!(result, "(time {:.1}s)", self.wait_time); } result } #[must_use] pub const fn gesture_step_type(&self) -> GestureStepType { GestureStepType::Wait } } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct GestureStepEOF; impl GestureStepEOF { pub const fn new() -> Result { Ok(Self) } #[must_use] pub fn to_string(&self) -> String { "End of gesture sequence".into() } #[must_use] pub const fn gesture_step_type(&self) -> GestureStepType { GestureStepType::EOF } } #[derive(Clone, Debug, Default, PartialEq)] pub struct AssetGesture { pub replace_with: String, pub sequence: Vec, pub trigger: String, pub trigger_key: u8, pub trigger_key_mask: u32, asset_id: UUID, asset_data: Vec, } impl AssetGesture { pub fn new_with_constructor() -> Result { Ok(Self::default()) } pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec) -> Result { Ok(Self { asset_id, asset_data, ..Self::default() }) } #[must_use] pub const fn asset_type(&self) -> AssetType { AssetType::Gesture } #[must_use] pub const fn asset_id(&self) -> UUID { self.asset_id } #[must_use] pub fn asset_data(&self) -> &[u8] { &self.asset_data } pub fn decode(&mut self) -> Result { Ok(self.decode_inner().is_ok()) } fn decode_inner(&mut self) -> Result<(), Error> { let text = String::from_utf8(self.asset_data.clone()).map_err(|_| Error::Argument)?; let mut lines = text.split('\n'); if parse_line::(&mut lines)? != 2 { return Err(Error::Argument); } self.trigger_key = parse_line(&mut lines)?; self.trigger_key_mask = parse_line(&mut lines)?; next_line(&mut lines)?.clone_into(&mut self.trigger); next_line(&mut lines)?.clone_into(&mut self.replace_with); let count = parse_line::(&mut lines)?; if count < 0 { return Err(Error::Argument); } let mut sequence = Vec::with_capacity(usize::try_from(count).map_err(|_| Error::Argument)?); for _ in 0..count { match parse_line::(&mut lines)? { 0 => { let name = next_line(&mut lines)?.to_owned(); let id = UUID::new_with_string( next_line(&mut lines)?.trim_end_matches('\r').into(), )?; let flags = parse_line::(&mut lines)?; sequence.push(GestureStep::Animation(GestureStepAnimation { animation_start: flags == 0, id, name, })); } 1 => { let name = next_line(&mut lines)?.replace('\r', ""); let id = UUID::new_with_string( next_line(&mut lines)?.trim_end_matches('\r').into(), )?; let _flags = parse_line::(&mut lines)?; sequence.push(GestureStep::Sound(GestureStepSound { id, name })); } 2 => { let text = next_line(&mut lines)?.to_owned(); let _flags = parse_line::(&mut lines)?; sequence.push(GestureStep::Chat(GestureStepChat { text })); } 3 => { let wait_time = parse_line::(&mut lines)?; let flags = parse_line::(&mut lines)?; sequence.push(GestureStep::Wait(GestureStepWait { wait_for_animation: flags & 0x02 != 0, wait_for_time: flags & 0x01 != 0, wait_time, })); } 4 => { sequence.push(GestureStep::EOF(GestureStepEOF)); break; } // The reference ignores unknown step discriminants. _ => {} } } self.sequence = sequence; Ok(()) } pub fn encode(&mut self) -> Result<(), Error> { let mut text = format!( "2\n{}\n{}\n{}\n{}\n{}\n", self.trigger_key, self.trigger_key_mask, self.trigger, self.replace_with, self.sequence.len() ); for step in &self.sequence { match step { GestureStep::Animation(step) => { let _ = write!( text, "0\n{}\n{}\n{}\n", step.name, step.id, i32::from(!step.animation_start) ); } GestureStep::Sound(step) => { let _ = write!(text, "1\n{}\n{}\n0\n", step.name, step.id); } GestureStep::Chat(step) => { let _ = write!(text, "2\n{}\n0\n", step.text); } GestureStep::Wait(step) => { let flags = i32::from(step.wait_for_time) | (i32::from(step.wait_for_animation) << 1); let _ = write!(text, "3\n{:.6}\n{}\n", step.wait_time, flags); } GestureStep::EOF(_) => { text.push_str("4\n"); break; } } } self.asset_data = text.into_bytes(); Ok(()) } } fn next_line<'a>(lines: &mut impl Iterator) -> Result<&'a str, Error> { lines.next().ok_or(Error::Argument) } fn parse_line<'a, T: std::str::FromStr>( lines: &mut impl Iterator, ) -> Result { next_line(lines)? .trim_end_matches('\r') .parse() .map_err(|_| Error::Argument) } #[cfg(test)] mod tests { use super::*; #[test] fn gesture_v2_roundtrip_preserves_all_step_payloads() { let animation = UUID::random().unwrap(); let sound = UUID::random().unwrap(); let mut gesture = AssetGesture::new_with_constructor().unwrap(); gesture.trigger_key = 7; gesture.trigger_key_mask = 3; gesture.trigger = "/wave".into(); gesture.replace_with = "waves".into(); gesture.sequence = vec![ GestureStep::Chat(GestureStepChat { text: "/5 hello".into(), }), GestureStep::Animation(GestureStepAnimation { animation_start: true, id: animation, name: "Wave".into(), }), GestureStep::Sound(GestureStepSound { id: sound, name: "Bell".into(), }), GestureStep::Wait(GestureStepWait { wait_for_animation: true, wait_for_time: true, wait_time: 1.25, }), GestureStep::EOF(GestureStepEOF), ]; gesture.encode().unwrap(); let mut decoded = AssetGesture::new_with_uuid_bytes(UUID::random().unwrap(), gesture.asset_data.clone()) .unwrap(); assert!(decoded.decode().unwrap()); assert_eq!(decoded.trigger_key, gesture.trigger_key); assert_eq!(decoded.trigger_key_mask, gesture.trigger_key_mask); assert_eq!(decoded.trigger, gesture.trigger); assert_eq!(decoded.replace_with, gesture.replace_with); assert_eq!(decoded.sequence, gesture.sequence); } #[test] fn malformed_gesture_returns_false_without_partial_sequence() { let mut gesture = AssetGesture::new_with_uuid_bytes(UUID::zero(), b"1\n0\n".to_vec()).unwrap(); assert!(!gesture.decode().unwrap()); assert!(gesture.sequence.is_empty()); } }