1mod reader;
34mod writer;
35
36pub use reader::{read_ssf, read_ssf_from_bytes};
37pub use writer::{write_ssf, write_ssf_to_vec};
38
39use thiserror::Error;
40
41use rakata_core::StrRef;
42
43use crate::binary::{self, DecodeBinary, EncodeBinary};
44
45const FILE_HEADER_SIZE: usize = 12;
47const SOUND_ENTRY_SIZE: usize = 4;
49const SOUND_SLOT_COUNT: usize = 28;
51const CANONICAL_RESERVED_ENTRY_COUNT: usize = 12;
53const SSF_MAGIC: [u8; 4] = *b"SSF ";
55const SSF_VERSION_V11: [u8; 4] = *b"V1.1";
57const CANONICAL_SOUND_TABLE_OFFSET: u32 = 12;
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub enum SsfSoundSlot {
63 BattleCry1 = 0,
65 BattleCry2 = 1,
67 BattleCry3 = 2,
69 BattleCry4 = 3,
71 BattleCry5 = 4,
73 BattleCry6 = 5,
75 Select1 = 6,
77 Select2 = 7,
79 Select3 = 8,
81 AttackGrunt1 = 9,
83 AttackGrunt2 = 10,
85 AttackGrunt3 = 11,
87 PainGrunt1 = 12,
89 PainGrunt2 = 13,
91 LowHealth = 14,
93 Dead = 15,
95 CriticalHit = 16,
97 TargetImmune = 17,
99 LayMine = 18,
101 DisarmMine = 19,
103 BeginStealth = 20,
105 BeginSearch = 21,
107 BeginUnlock = 22,
109 UnlockFailed = 23,
111 UnlockSuccess = 24,
113 SeparatedFromParty = 25,
115 RejoinedParty = 26,
117 Poisoned = 27,
119}
120
121impl SsfSoundSlot {
122 pub const ALL: &'static [Self] = &[
124 Self::BattleCry1,
125 Self::BattleCry2,
126 Self::BattleCry3,
127 Self::BattleCry4,
128 Self::BattleCry5,
129 Self::BattleCry6,
130 Self::Select1,
131 Self::Select2,
132 Self::Select3,
133 Self::AttackGrunt1,
134 Self::AttackGrunt2,
135 Self::AttackGrunt3,
136 Self::PainGrunt1,
137 Self::PainGrunt2,
138 Self::LowHealth,
139 Self::Dead,
140 Self::CriticalHit,
141 Self::TargetImmune,
142 Self::LayMine,
143 Self::DisarmMine,
144 Self::BeginStealth,
145 Self::BeginSearch,
146 Self::BeginUnlock,
147 Self::UnlockFailed,
148 Self::UnlockSuccess,
149 Self::SeparatedFromParty,
150 Self::RejoinedParty,
151 Self::Poisoned,
152 ];
153
154 #[allow(clippy::as_conversions)] pub const fn index(self) -> usize {
157 self as usize
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct Ssf {
164 pub sound_table_offset: u32,
168 pub sounds: [StrRef; SOUND_SLOT_COUNT],
170 pub reserved_entries: Vec<StrRef>,
174}
175
176impl Default for Ssf {
177 fn default() -> Self {
178 Self {
179 sound_table_offset: CANONICAL_SOUND_TABLE_OFFSET,
180 sounds: [StrRef::invalid(); SOUND_SLOT_COUNT],
181 reserved_entries: vec![StrRef::invalid(); CANONICAL_RESERVED_ENTRY_COUNT],
182 }
183 }
184}
185
186impl Ssf {
187 pub fn new() -> Self {
189 Self::default()
190 }
191
192 pub fn get(&self, slot: SsfSoundSlot) -> StrRef {
194 self.sounds[slot.index()]
195 }
196
197 pub fn set(&mut self, slot: SsfSoundSlot, strref: StrRef) {
199 self.sounds[slot.index()] = strref;
200 }
201
202 pub fn get_raw(&self, slot: SsfSoundSlot) -> i32 {
204 self.get(slot).raw()
205 }
206
207 pub fn set_raw(&mut self, slot: SsfSoundSlot, strref_raw: i32) {
209 self.set(slot, StrRef::from_raw(strref_raw));
210 }
211
212 pub fn reset(&mut self) {
214 self.sounds.fill(StrRef::invalid());
215 }
216}
217
218impl DecodeBinary for Ssf {
219 type Error = SsfBinaryError;
220
221 fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
222 read_ssf_from_bytes(bytes)
223 }
224}
225
226impl EncodeBinary for Ssf {
227 type Error = SsfBinaryError;
228
229 fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
230 write_ssf_to_vec(self)
231 }
232}
233
234#[derive(Debug, Error)]
236pub enum SsfBinaryError {
237 #[error(transparent)]
239 Io(#[from] std::io::Error),
240 #[error("invalid SSF magic: {0:?}")]
242 InvalidMagic([u8; 4]),
243 #[error("invalid SSF version: {0:?}")]
245 InvalidVersion([u8; 4]),
246 #[error("invalid SSF header: {0}")]
248 InvalidHeader(String),
249 #[error("invalid SSF data: {0}")]
251 InvalidData(String),
252 #[error("value overflow while writing field `{0}`")]
254 ValueOverflow(&'static str),
255}
256
257impl From<binary::BinaryLayoutError> for SsfBinaryError {
258 fn from(error: binary::BinaryLayoutError) -> Self {
259 Self::InvalidHeader(error.to_string())
260 }
261}