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