1use std::io::{Read, Seek};
7
8use crate::bank;
9use crate::cbin::{self, Cbin, Header};
10use crate::error::Error;
11use crate::formats::ne5::program;
12use crate::types::RangedU16Pair;
13
14pub const FORMAT: &str = "ne5t";
15pub const KNOWN_VERSIONS: &[u32] = &[0, 1];
18pub const BODY_LEN: usize = 18;
20pub const FILE_LEN: usize = 0x2c + BODY_LEN;
22pub const PROGRAM_COUNT: usize = 4;
23pub const BANK_COUNT: u16 = 4;
24pub const SLOT_COUNT: u16 = 50;
25pub const DEFAULT_VERSION: u32 = 1;
28
29pub type Location = RangedU16Pair<BANK_COUNT, SLOT_COUNT>;
30pub type Bank = bank::Bank<Cbin<Song>, Location>;
31
32#[nord_bits_derive::bitbody(18)]
42pub struct Song {
43 #[bits(0..=15)]
44 pub version: u16,
45 #[bits(16..=24)]
46 pub a: program::Location,
47 #[bits(25..=33)]
48 pub b: program::Location,
49 #[bits(34..=42)]
50 pub c: program::Location,
51 #[bits(43..=51)]
52 pub d: program::Location,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum Slot {
61 A,
62 B,
63 C,
64 D,
65}
66
67impl Slot {
68 pub const ALL: [Slot; PROGRAM_COUNT] = [Slot::A, Slot::B, Slot::C, Slot::D];
70
71 pub fn at(index: usize) -> Option<Slot> {
73 Self::ALL.get(index).copied()
74 }
75}
76
77impl Song {
78 pub fn programs(&self) -> [program::Location; PROGRAM_COUNT] {
80 [self.a, self.b, self.c, self.d]
81 }
82
83 pub fn get(&self, slot: Slot) -> program::Location {
84 match slot {
85 Slot::A => self.a,
86 Slot::B => self.b,
87 Slot::C => self.c,
88 Slot::D => self.d,
89 }
90 }
91
92 pub fn set(&mut self, slot: Slot, location: program::Location) {
93 *match slot {
94 Slot::A => &mut self.a,
95 Slot::B => &mut self.b,
96 Slot::C => &mut self.c,
97 Slot::D => &mut self.d,
98 } = location;
99 }
100}
101
102pub fn location(file: &Cbin<Song>) -> Result<Location, Error> {
104 program::slot(&file.header)
105}
106
107pub fn new(
114 location: Location,
115 version: u32,
116 programs: [program::Location; PROGRAM_COUNT],
117) -> Result<Cbin<Song>, Error> {
118 program::known_version(FORMAT, version, KNOWN_VERSIONS)?;
119 let echo = u16::try_from(version).map_err(|_| crate::error::ParseError::OutOfBounds {
120 value: format!("version {version}"),
121 bound: "a version the body's 16-bit echo can hold".into(),
122 })?;
123 let [a, b, c, d] = programs;
124 Ok(Cbin {
125 header: Header::new(FORMAT, location.inner(), version),
126 body: Song {
127 raw: [0; BODY_LEN],
128 version: echo,
129 a,
130 b,
131 c,
132 d,
133 },
134 })
135}
136
137pub fn read_from(reader: &mut (impl Read + Seek)) -> Result<Cbin<Song>, Error> {
138 let file: Cbin<Song> = cbin::read(reader, FORMAT)?;
139 program::known_version(FORMAT, file.header.version, KNOWN_VERSIONS)?;
140 program::unset_aux(FORMAT, &file.header)?;
141 location(&file)?;
142 Ok(file)
143}
144
145impl bank::Item<Location> for Cbin<Song> {
146 fn location(&self) -> Location {
147 location(self).expect("a song's location is validated at construction")
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::bank::Item;
156 use crate::error::Error;
157 use std::io::Cursor;
158
159 fn song_of(programs: [(u16, u16); PROGRAM_COUNT]) -> Result<Cbin<Song>, Error> {
160 let mut at = [program::Location::default(); PROGRAM_COUNT];
161 for (slot, pair) in at.iter_mut().zip(programs) {
162 *slot = pair.try_into()?;
163 }
164 new((0, 1).try_into()?, DEFAULT_VERSION, at)
165 }
166
167 #[test]
168 fn a_songs_four_programs_survive_a_round_trip() -> Result<(), Error> {
169 let song = song_of([(1, 2), (2, 3), (3, 4), (4, 5)])?;
170
171 assert_eq!(song.location(), (0, 1));
172 for (slot, want) in Slot::ALL.into_iter().zip([(1, 2), (2, 3), (3, 4), (4, 5)]) {
173 assert_eq!(song.get(slot), want, "{slot:?}");
174 }
175
176 let mut bytes = Vec::new();
177 song.write_to(&mut Cursor::new(&mut bytes)).unwrap();
178 let back = read_from(&mut Cursor::new(&mut bytes)).unwrap();
179
180 assert_eq!(song.location(), back.location());
181 for slot in Slot::ALL {
182 assert_eq!(song.get(slot), back.get(slot), "{slot:?}");
183 }
184
185 Ok(())
186 }
187
188 #[test]
191 fn a_version_no_read_accepts_is_not_written() -> Result<(), Error> {
192 let at = [program::Location::default(); PROGRAM_COUNT];
193 for version in KNOWN_VERSIONS {
194 assert!(new((0, 0).try_into()?, *version, at).is_ok(), "v{version}");
195 }
196 let err = new((0, 0).try_into()?, 2, at).expect_err("version 2 must not be written");
197 assert!(
198 matches!(
199 err,
200 Error::Parse(crate::error::ParseError::UnsupportedVersion { version: 2, .. })
201 ),
202 "refused for the wrong reason: {err}",
203 );
204 assert!(new((0, 0).try_into()?, 0x1_0000, at).is_err());
206 Ok(())
207 }
208
209 #[test]
216 fn version_survives_a_round_trip() -> Result<(), Error> {
217 for version in [0u32, 1] {
218 let song = new(
219 (0, 5).try_into()?,
220 version,
221 [
222 (1, 2).try_into()?,
223 (2, 3).try_into()?,
224 (3, 4).try_into()?,
225 (4, 5).try_into()?,
226 ],
227 )?;
228
229 let mut bytes = Vec::new();
230 song.write_to(&mut Cursor::new(&mut bytes)).unwrap();
231
232 assert_eq!(
234 u32::from_le_bytes(bytes[0x14..0x18].try_into().unwrap()),
235 version,
236 "header version for v{version}",
237 );
238 assert_eq!(
241 u16::from_be_bytes(bytes[0x2c..0x2e].try_into().unwrap()) as u32,
242 version,
243 "body version echo for v{version}",
244 );
245
246 let back = read_from(&mut Cursor::new(&mut bytes)).unwrap();
247 assert_eq!(back.header.version, version);
248 assert_eq!(back.get(Slot::A), song.get(Slot::A));
249 }
250 Ok(())
251 }
252
253 #[test]
255 fn setting_one_entry_leaves_the_others_alone() -> Result<(), Error> {
256 let mut song = song_of([(1, 2), (2, 3), (3, 4), (4, 5)])?;
257
258 song.set(Slot::B, (5, 20).try_into()?);
259
260 assert_eq!(song.location(), (0, 1));
261 for (slot, want) in Slot::ALL.into_iter().zip([(1, 2), (5, 20), (3, 4), (4, 5)]) {
262 assert_eq!(song.get(slot), want, "{slot:?}");
263 }
264
265 let mut bytes = Vec::new();
266 song.write_to(&mut Cursor::new(&mut bytes)).unwrap();
267 let back = read_from(&mut Cursor::new(&mut bytes)).unwrap();
268
269 assert_eq!(song.location(), back.location());
270 for slot in Slot::ALL {
271 assert_eq!(song.get(slot), back.get(slot), "{slot:?}");
272 }
273
274 Ok(())
275 }
276
277 #[test]
279 fn a_song_holds_four_entries_and_no_fifth() {
280 assert_eq!(Slot::ALL.len(), PROGRAM_COUNT);
281 assert_eq!(Slot::at(0), Some(Slot::A));
282 assert_eq!(Slot::at(PROGRAM_COUNT - 1), Some(Slot::D));
283 assert_eq!(Slot::at(PROGRAM_COUNT), None);
284 }
285}