Skip to main content

nbs/
header.rs

1use crate::{NbsError, NbsFormat};
2use byteorder::LittleEndian;
3use std::time::Duration;
4
5/// The header contains information about the file
6#[derive(Debug)]
7pub struct Header {
8    /// The first 2 bytes are always zero in the new fromat.
9    /// In the old NBS format, this used to be song length, which can never be zero.
10    pub(crate) old_song_length: i16,
11    /// The version of the new NBS format.
12    /// Only avabile in the new format.
13    pub(crate) version_number: Option<i8>,
14    /// Amount of default instruments when the song was saved.
15    /// This is needed to determine at what index custom instruments start.
16    /// Only avabile in the new format
17    pub vannila_instrument_count: Option<i8>,
18    /// The length of the song, measured in ticks.
19    /// Divide this by the tempo to get the length of the song in seconds.
20    /// Only avabile in the new format starting from version 3.
21    pub(crate) song_length: Option<i16>,
22    /// The last layer with at least one note block in it, or the last layer that has had its name, volume or stereo changed.
23    pub layer_count: i16,
24    /// The name of the song.
25    pub song_name: String,
26    /// The author of the song.
27    pub song_author: String,
28    /// The original author of the song.
29    pub original_song_author: String,
30    /// The description of the song.
31    pub song_description: String,
32    /// The tempo of the song multiplied by 100.
33    pub song_tempo: i16,
34    /// Whether auto-saving has been enabled.
35    /// As of NBS version 4 this value is still saved to the file, but no longer used in the program.
36    pub auto_saving: bool,
37    /// The amount of minutes between each auto-save (if it has been enabled) (1-60).
38    /// As of NBS version 4 this value is still saved to the file, but no longer used in the program.
39    pub auto_saving_duration: i8,
40    /// The time signature of the song.
41    /// If this is 3, then the signature is 3/4. Default is 4. This value ranges from 2-8.
42    pub time_signature: i8,
43    /// Amount of minutes spent on the project.
44    pub minutes_spent: i32,
45    /// Amount of times the user has left-clicked.
46    pub left_clicks: i32,
47    /// Amount of times the user has right-clicked.
48    pub right_clicks: i32,
49    /// Amount of times the user has added a note block.
50    pub noteblocks_added: i32,
51    /// The amount of times the user have removed a note block.
52    pub noteblocks_removed: i32,
53    /// If the song has been imported from a .mid or .schematic file, that file name is stored here (only the name of the file, not the path).
54    pub imported_file_name: String,
55    /// Whether looping is on or off.
56    /// Only avabile in the new format.
57    pub is_loop: Option<bool>,
58    /// 0 = infinite. Other values mean the amount of times the song loops.
59    /// Only avabile in the new format.
60    pub max_loop_count: Option<i8>,
61    /// Determines which part of the song (in ticks) it loops back to.
62    /// Only avabile in the new format.
63    pub loop_start_tick: Option<i16>,
64    /// Not part of the Header.
65    pub format: NbsFormat,
66}
67
68impl Header {
69    pub fn new(format: NbsFormat) -> Self {
70        Header {
71            old_song_length: 0,
72            version_number: Some(format.version()),
73            vannila_instrument_count: Some(16),
74            song_length: Some(0),
75            layer_count: 0,
76            song_name: String::new(),
77            song_author: String::new(),
78            original_song_author: String::new(),
79            song_description: String::new(),
80            song_tempo: 1000,
81            auto_saving: false,
82            auto_saving_duration: 0,
83            time_signature: 4,
84            minutes_spent: 0,
85            left_clicks: 0,
86            right_clicks: 0,
87            noteblocks_added: 0,
88            noteblocks_removed: 0,
89            imported_file_name: String::new(),
90            is_loop: Some(false),
91            max_loop_count: Some(0),
92            loop_start_tick: Some(0),
93            format: format,
94        }
95    }
96
97    pub fn decode<R>(reader: &mut R) -> Result<Self, NbsError>
98    where
99        R: crate::ReadStringExt,
100    {
101        let old_song_length = reader.read_i16::<LittleEndian>()?;
102        let version = if old_song_length != 0 {
103            NbsFormat::NoteBlockStudio
104        } else {
105            NbsFormat::OpenNoteBlockStudio(reader.read_i8()?)
106        };
107        let version_number = match version {
108            NbsFormat::NoteBlockStudio => None,
109            NbsFormat::OpenNoteBlockStudio(v) => Some(v),
110        };
111        let vannila_instrument_count = if version.is_new() {
112            Some(reader.read_i8()?)
113        } else {
114            None
115        };
116        let song_length = if version.is_new() {
117            Some(reader.read_i16::<LittleEndian>()?)
118        } else {
119            None
120        };
121        let layer_count = reader.read_i16::<LittleEndian>()?;
122        let song_name = reader.read_string()?;
123        let song_author = reader.read_string()?;
124        let original_song_author = reader.read_string()?;
125        let song_description = reader.read_string()?;
126        let song_tempo = reader.read_i16::<LittleEndian>()?;
127        let auto_saving = if reader.read_i8()? == 1 { true } else { false };
128        let auto_saving_duration = reader.read_i8()?;
129        let time_signature = reader.read_i8()?;
130        let minutes_spent = reader.read_i32::<LittleEndian>()?;
131        let left_clicks = reader.read_i32::<LittleEndian>()?;
132        let right_clicks = reader.read_i32::<LittleEndian>()?;
133        let noteblocks_added = reader.read_i32::<LittleEndian>()?;
134        let noteblocks_removed = reader.read_i32::<LittleEndian>()?;
135        let imported_file_name = reader.read_string()?;
136        let is_loop = if version.is_new() {
137            Some(if reader.read_i8()? == 1 { true } else { false })
138        } else {
139            None
140        };
141        let max_loop_count = if version.is_new() {
142            Some(reader.read_i8()?)
143        } else {
144            None
145        };
146        let loop_start_tick = if version.is_new() {
147            Some(reader.read_i16::<LittleEndian>()?)
148        } else {
149            None
150        };
151        Ok(Header {
152            old_song_length,
153            version_number,
154            vannila_instrument_count,
155            song_length,
156            layer_count,
157            song_name,
158            song_author,
159            original_song_author,
160            song_description,
161            song_tempo,
162            auto_saving,
163            auto_saving_duration,
164            time_signature,
165            minutes_spent,
166            left_clicks,
167            right_clicks,
168            noteblocks_added,
169            noteblocks_removed,
170            imported_file_name,
171            is_loop,
172            max_loop_count,
173            loop_start_tick,
174            format: version,
175        })
176    }
177
178    pub fn encode<W>(&self, format: NbsFormat, writer: &mut W) -> Result<(), NbsError>
179    where
180        W: crate::WriteStringExt,
181    {
182        writer.write_i16::<LittleEndian>(self.old_song_length)?;
183        if format.version() > 0 {
184            writer.write_i8(self.version_number.ok_or(NbsError::InvalidFormat)?)?;
185            writer.write_i8(
186                self.vannila_instrument_count
187                    .ok_or(NbsError::InvalidFormat)?,
188            )?;
189        }
190        if format.version() >= 3 {
191            writer.write_i16::<LittleEndian>(self.song_length.ok_or(NbsError::InvalidFormat)?)?;
192        }
193        writer.write_i16::<LittleEndian>(self.layer_count)?;
194        writer.write_string(&self.song_name)?;
195        writer.write_string(&self.song_author)?;
196        writer.write_string(&self.original_song_author)?;
197        writer.write_string(&self.song_description)?;
198        writer.write_i16::<LittleEndian>(self.song_tempo)?;
199        writer.write_i8(if self.auto_saving { 1 } else { 0 })?;
200        writer.write_i8(self.auto_saving_duration)?;
201        writer.write_i8(self.time_signature)?;
202        writer.write_i32::<LittleEndian>(self.minutes_spent)?;
203        writer.write_i32::<LittleEndian>(self.left_clicks)?;
204        writer.write_i32::<LittleEndian>(self.right_clicks)?;
205        writer.write_i32::<LittleEndian>(self.noteblocks_added)?;
206        writer.write_i32::<LittleEndian>(self.noteblocks_removed)?;
207        writer.write_string(&self.imported_file_name)?;
208        if format.version() > 0 {
209            writer.write_i8(if self.is_loop.ok_or(NbsError::InvalidFormat)? {
210                1
211            } else {
212                0
213            })?;
214            writer.write_i8(self.max_loop_count.ok_or(NbsError::InvalidFormat)?)?;
215            writer
216                .write_i16::<LittleEndian>(self.loop_start_tick.ok_or(NbsError::InvalidFormat)?)?;
217        }
218
219        Ok(())
220    }
221
222    pub fn vannila_instrument_count(&self) -> Result<i8, NbsError> {
223        Ok(match self.format {
224            NbsFormat::NoteBlockStudio => 10,
225            NbsFormat::OpenNoteBlockStudio(_) => self
226                .vannila_instrument_count
227                .ok_or(NbsError::InvalidFormat)?,
228        })
229    }
230
231    /// Returns the song ticks.
232    /// This method will only return valid results for old versions and version 3 and 4 of the new version.
233    pub fn song_ticks(&self) -> Result<Option<i16>, NbsError> {
234        Ok(match self.format {
235            NbsFormat::NoteBlockStudio => Some(self.old_song_length),
236            NbsFormat::OpenNoteBlockStudio(v) => {
237                if v >= 3 {
238                    Some(self.song_length.ok_or(NbsError::InvalidFormat)?)
239                } else {
240                    None
241                }
242            }
243        })
244    }
245
246    /// Returns the song Duration.
247    /// This method will only return valid results for old versions and version 3 and 4 of the new version.
248    pub fn song_length(&self) -> Result<Option<Duration>, NbsError> {
249        let song_ticks = self.song_ticks()?;
250        if song_ticks.is_none() {
251            return Ok(None);
252        }
253        Ok(Some(Duration::from_secs_f32(
254            song_ticks.unwrap() as f32 / (self.song_tempo as f32 / 100.0),
255        )))
256    }
257}