376 lines
11 KiB
Rust
376 lines
11 KiB
Rust
//! 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<Self, Error> {
|
|
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<Self, Error> {
|
|
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<Self, Error> {
|
|
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<Self, Error> {
|
|
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<Self, Error> {
|
|
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<GestureStep>,
|
|
pub trigger: String,
|
|
pub trigger_key: u8,
|
|
pub trigger_key_mask: u32,
|
|
asset_id: UUID,
|
|
asset_data: Vec<u8>,
|
|
}
|
|
|
|
impl AssetGesture {
|
|
pub fn new_with_constructor() -> Result<Self, Error> {
|
|
Ok(Self::default())
|
|
}
|
|
|
|
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
|
|
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<bool, Error> {
|
|
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::<i32>(&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::<i32>(&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::<i32>(&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::<i32>(&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::<i32>(&mut lines)?;
|
|
sequence.push(GestureStep::Sound(GestureStepSound { id, name }));
|
|
}
|
|
2 => {
|
|
let text = next_line(&mut lines)?.to_owned();
|
|
let _flags = parse_line::<i32>(&mut lines)?;
|
|
sequence.push(GestureStep::Chat(GestureStepChat { text }));
|
|
}
|
|
3 => {
|
|
let wait_time = parse_line::<f32>(&mut lines)?;
|
|
let flags = parse_line::<i32>(&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<Item = &'a str>) -> Result<&'a str, Error> {
|
|
lines.next().ok_or(Error::Argument)
|
|
}
|
|
|
|
fn parse_line<'a, T: std::str::FromStr>(
|
|
lines: &mut impl Iterator<Item = &'a str>,
|
|
) -> Result<T, Error> {
|
|
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());
|
|
}
|
|
}
|