rakata_formats/lip/
mod.rs

1//! LIP binary reader and writer.
2//!
3//! LIP files store lip-sync animation keyframes for voiced dialogue. Each
4//! keyframe maps a timestamp to a viseme (mouth shape) index.
5//!
6//! ## Format Layout
7//! ```text
8//! +------------------------------+ 0x0000
9//! | Header (16 bytes)            |
10//! +------------------------------+ 0x0010
11//! | Keyframe table               |
12//! | 5 bytes * entry_count        |
13//! +------------------------------+
14//! ```
15//!
16//! ## Header (16 bytes)
17//! ```text
18//! 0x00..0x04  magic        "LIP "
19//! 0x04..0x08  version      "V1.0"
20//! 0x08..0x0C  length       (f32)
21//! 0x0C..0x10  entry_count  (u32)
22//! ```
23//!
24//! ## Keyframe Entry (5 bytes)
25//! ```text
26//! 0x00..0x04  time         (f32)
27//! 0x04..0x05  shape        (u8)
28//! ```
29
30mod reader;
31mod writer;
32
33pub use reader::{read_lip, read_lip_from_bytes};
34pub use writer::{write_lip, write_lip_to_vec};
35
36use num_enum::{IntoPrimitive, TryFromPrimitive};
37use thiserror::Error;
38
39use crate::binary::{self, DecodeBinary, EncodeBinary};
40
41/// LIP binary header size.
42const FILE_HEADER_SIZE: usize = 16;
43/// LIP keyframe entry size.
44const KEYFRAME_ENTRY_SIZE: usize = 5;
45/// LIP file signature.
46const LIP_MAGIC: [u8; 4] = *b"LIP ";
47/// LIP version used by KotOR.
48const LIP_VERSION_V10: [u8; 4] = *b"V1.0";
49
50/// Known LIP viseme (mouth shape) IDs.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
52#[repr(u8)]
53pub enum LipShape {
54    /// Neutral/rest position.
55    Neutral = 0,
56    /// Wide "ee" mouth.
57    Ee = 1,
58    /// Relaxed "eh" mouth.
59    Eh = 2,
60    /// Open "ah" mouth.
61    Ah = 3,
62    /// Rounded "oh" mouth.
63    Oh = 4,
64    /// Pursed "oo" mouth.
65    Ooh = 5,
66    /// Slight smile "y" mouth.
67    Y = 6,
68    /// Teeth-together "s/ts" mouth.
69    Sts = 7,
70    /// Lower-lip/teeth "f/v" mouth.
71    Fv = 8,
72    /// Tongue-raised "n/ng" mouth.
73    Ng = 9,
74    /// Tongue-between-teeth "th" mouth.
75    Th = 10,
76    /// Closed-lips "m/p/b" mouth.
77    Mpb = 11,
78    /// Tongue-up "t/d" mouth.
79    Td = 12,
80    /// Rounded-relaxed "sh/ch/j" mouth.
81    Sh = 13,
82    /// Tongue-forward "l/r" mouth.
83    L = 14,
84    /// Back-tongue "k/g/h" mouth.
85    Kg = 15,
86}
87
88impl LipShape {
89    /// Returns the known shape for a raw ID, if defined.
90    pub fn from_raw_id(raw_id: u8) -> Option<Self> {
91        Self::try_from(raw_id).ok()
92    }
93
94    /// Returns the raw viseme ID stored in binary files.
95    pub fn raw_id(self) -> u8 {
96        u8::from(self)
97    }
98}
99
100/// Lossless LIP shape code wrapper.
101///
102/// This preserves unknown IDs during parse/write roundtrips while still
103/// exposing known viseme values where possible.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
105pub struct LipShapeCode(u8);
106
107impl LipShapeCode {
108    /// Creates a shape code from raw on-disk value.
109    pub const fn from_raw_id(raw_id: u8) -> Self {
110        Self(raw_id)
111    }
112
113    /// Returns the raw on-disk value.
114    pub const fn raw_id(self) -> u8 {
115        self.0
116    }
117
118    /// Returns the known shape when this code maps to one.
119    pub fn known_shape(self) -> Option<LipShape> {
120        LipShape::from_raw_id(self.0)
121    }
122
123    /// Returns `true` when this code maps to a known shape.
124    pub fn is_known(self) -> bool {
125        self.known_shape().is_some()
126    }
127}
128
129impl From<LipShape> for LipShapeCode {
130    fn from(value: LipShape) -> Self {
131        Self(u8::from(value))
132    }
133}
134
135impl From<u8> for LipShapeCode {
136    fn from(value: u8) -> Self {
137        Self::from_raw_id(value)
138    }
139}
140
141/// One LIP keyframe entry.
142#[derive(Debug, Clone, PartialEq)]
143pub struct LipKeyframe {
144    /// Timestamp in seconds from animation start.
145    pub time: f32,
146    /// Viseme shape code.
147    pub shape: LipShapeCode,
148}
149
150/// In-memory LIP container.
151#[derive(Debug, Clone, PartialEq, Default)]
152pub struct Lip {
153    /// Total lip-sync duration in seconds.
154    pub length: f32,
155    /// Ordered keyframe entries.
156    pub keyframes: Vec<LipKeyframe>,
157}
158
159impl Lip {
160    /// Creates an empty LIP file.
161    pub fn new() -> Self {
162        Self::default()
163    }
164
165    /// Appends a keyframe with raw shape ID.
166    pub fn push_keyframe(&mut self, time: f32, shape_id: u8) {
167        self.push_keyframe_with_shape(time, LipShapeCode::from_raw_id(shape_id));
168    }
169
170    /// Appends a keyframe with explicit shape wrapper.
171    pub fn push_keyframe_with_shape(&mut self, time: f32, shape: LipShapeCode) {
172        self.keyframes.push(LipKeyframe { time, shape });
173    }
174}
175
176impl DecodeBinary for Lip {
177    type Error = LipBinaryError;
178
179    fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
180        read_lip_from_bytes(bytes)
181    }
182}
183
184impl EncodeBinary for Lip {
185    type Error = LipBinaryError;
186
187    fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
188        write_lip_to_vec(self)
189    }
190}
191
192/// Errors produced while parsing or serializing LIP binary data.
193#[derive(Debug, Error)]
194pub enum LipBinaryError {
195    /// I/O read/write failure.
196    #[error(transparent)]
197    Io(#[from] std::io::Error),
198    /// Header signature is not `LIP `.
199    #[error("invalid LIP magic: {0:?}")]
200    InvalidMagic([u8; 4]),
201    /// Header version is unsupported.
202    #[error("invalid LIP version: {0:?}")]
203    InvalidVersion([u8; 4]),
204    /// Header/body layout is invalid or truncated.
205    #[error("invalid LIP header: {0}")]
206    InvalidHeader(String),
207    /// LIP content is structurally invalid.
208    #[error("invalid LIP data: {0}")]
209    InvalidData(String),
210    /// Value cannot fit on-disk integer width.
211    #[error("value overflow while writing field `{0}`")]
212    ValueOverflow(&'static str),
213}
214
215impl From<binary::BinaryLayoutError> for LipBinaryError {
216    fn from(error: binary::BinaryLayoutError) -> Self {
217        Self::InvalidHeader(error.to_string())
218    }
219}