rosu_map/
beatmap.rs

1use std::{io, path::Path, str::FromStr};
2
3use crate::{
4    decode::{DecodeBeatmap, DecodeState},
5    section::{
6        colors::{Color, Colors, ColorsState, CustomColor, ParseColorsError},
7        editor::{Editor, EditorState, ParseEditorError},
8        events::BreakPeriod,
9        general::{CountdownType, GameMode},
10        hit_objects::{
11            hit_samples::SampleBank, HitObject, HitObjects, HitObjectsState, ParseHitObjectsError,
12        },
13        metadata::{Metadata, MetadataState, ParseMetadataError},
14        timing_points::ControlPoints,
15    },
16    LATEST_FORMAT_VERSION,
17};
18
19/// Fully parsed content of a `.osu` file.
20#[derive(Clone, Debug, PartialEq)]
21pub struct Beatmap {
22    pub format_version: i32,
23
24    // General
25    pub audio_file: String,
26    pub audio_lead_in: f64,
27    pub preview_time: i32,
28    pub default_sample_bank: SampleBank,
29    pub default_sample_volume: i32,
30    pub stack_leniency: f32,
31    pub mode: GameMode,
32    pub letterbox_in_breaks: bool,
33    pub special_style: bool,
34    pub widescreen_storyboard: bool,
35    pub epilepsy_warning: bool,
36    pub samples_match_playback_rate: bool,
37    pub countdown: CountdownType,
38    pub countdown_offset: i32,
39
40    // Editor
41    pub bookmarks: Vec<i32>,
42    pub distance_spacing: f64,
43    pub beat_divisor: i32,
44    pub grid_size: i32,
45    pub timeline_zoom: f64,
46
47    // Metadata
48    pub title: String,
49    pub title_unicode: String,
50    pub artist: String,
51    pub artist_unicode: String,
52    pub creator: String,
53    pub version: String,
54    pub source: String,
55    pub tags: String,
56    pub beatmap_id: i32,
57    pub beatmap_set_id: i32,
58
59    // Difficulty
60    pub hp_drain_rate: f32,
61    pub circle_size: f32,
62    pub overall_difficulty: f32,
63    pub approach_rate: f32,
64    pub slider_multiplier: f64,
65    pub slider_tick_rate: f64,
66
67    // Events
68    pub background_file: String,
69    pub breaks: Vec<BreakPeriod>,
70
71    // TimingPoints
72    pub control_points: ControlPoints,
73
74    // Colors
75    pub custom_combo_colors: Vec<Color>,
76    pub custom_colors: Vec<CustomColor>,
77
78    // HitObjects
79    pub hit_objects: Vec<HitObject>,
80}
81
82impl Beatmap {
83    /// Parse a [`Beatmap`] by providing a path to a `.osu` file.
84    ///
85    /// # Example
86    ///
87    /// ```rust,no_run
88    /// # use rosu_map::Beatmap;
89    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
90    /// let path = "/path/to/file.osu";
91    /// let map: Beatmap = Beatmap::from_path(path)?;
92    /// # Ok(()) }
93    /// ```
94    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, io::Error> {
95        crate::from_path(path)
96    }
97
98    /// Parse a [`Beatmap`] by providing the content of a `.osu` file as a
99    /// slice of bytes.
100    ///
101    /// # Example
102    ///
103    /// ```rust
104    /// # use rosu_map::Beatmap;
105    /// use rosu_map::section::general::GameMode;
106    ///
107    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
108    /// let bytes: &[u8] = b"[General]
109    /// Mode: 2
110    ///
111    /// [Metadata]
112    /// Creator: pishifat";
113    ///
114    /// let map: Beatmap = Beatmap::from_bytes(bytes)?;
115    /// assert_eq!(map.mode, GameMode::Catch);
116    /// assert_eq!(map.creator, "pishifat");
117    /// # Ok(()) }
118    /// ```
119    pub fn from_bytes(bytes: &[u8]) -> Result<Self, io::Error> {
120        crate::from_bytes(bytes)
121    }
122}
123
124impl FromStr for Beatmap {
125    type Err = io::Error;
126
127    /// Parse a [`Beatmap`] by providing the content of a `.osu` file as a
128    /// string.
129    ///
130    /// # Example
131    ///
132    /// ```rust
133    /// # use rosu_map::Beatmap;
134    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
135    /// let s: &str = "[Difficulty]
136    /// SliderMultiplier: 3
137    ///
138    /// [Editor]
139    /// BeatDivisor: 4";
140    ///
141    /// let map: Beatmap = s.parse()?; // same as `Beatmap::from_str(s)`
142    /// # let _ = <Beatmap as std::str::FromStr>::from_str(s).unwrap();
143    /// assert_eq!(map.slider_multiplier, 3.0);
144    /// assert_eq!(map.beat_divisor, 4);
145    /// # Ok(()) }
146    /// ```
147    fn from_str(s: &str) -> Result<Self, Self::Err> {
148        crate::from_str(s)
149    }
150}
151
152impl Default for Beatmap {
153    fn default() -> Self {
154        let editor = Editor::default();
155        let metadata = Metadata::default();
156        let colors = Colors::default();
157        let hit_objects = HitObjects::default();
158
159        Self {
160            format_version: LATEST_FORMAT_VERSION,
161            audio_file: hit_objects.audio_file,
162            audio_lead_in: hit_objects.audio_lead_in,
163            preview_time: hit_objects.preview_time,
164            default_sample_bank: hit_objects.default_sample_bank,
165            default_sample_volume: hit_objects.default_sample_volume,
166            stack_leniency: hit_objects.stack_leniency,
167            mode: hit_objects.mode,
168            letterbox_in_breaks: hit_objects.letterbox_in_breaks,
169            special_style: hit_objects.special_style,
170            widescreen_storyboard: hit_objects.widescreen_storyboard,
171            epilepsy_warning: hit_objects.epilepsy_warning,
172            samples_match_playback_rate: hit_objects.samples_match_playback_rate,
173            countdown: hit_objects.countdown,
174            countdown_offset: hit_objects.countdown_offset,
175            bookmarks: editor.bookmarks,
176            distance_spacing: editor.distance_spacing,
177            beat_divisor: editor.beat_divisor,
178            grid_size: editor.grid_size,
179            timeline_zoom: editor.timeline_zoom,
180            title: metadata.title,
181            title_unicode: metadata.title_unicode,
182            artist: metadata.artist,
183            artist_unicode: metadata.artist_unicode,
184            creator: metadata.creator,
185            version: metadata.version,
186            source: metadata.source,
187            tags: metadata.tags,
188            beatmap_id: metadata.beatmap_id,
189            beatmap_set_id: metadata.beatmap_set_id,
190            hp_drain_rate: hit_objects.hp_drain_rate,
191            circle_size: hit_objects.circle_size,
192            overall_difficulty: hit_objects.overall_difficulty,
193            approach_rate: hit_objects.approach_rate,
194            slider_multiplier: hit_objects.slider_multiplier,
195            slider_tick_rate: hit_objects.slider_tick_rate,
196            background_file: hit_objects.background_file,
197            breaks: hit_objects.breaks,
198            control_points: hit_objects.control_points,
199            custom_combo_colors: colors.custom_combo_colors,
200            custom_colors: colors.custom_colors,
201            hit_objects: hit_objects.hit_objects,
202        }
203    }
204}
205
206thiserror! {
207    /// All the ways that parsing a `.osu` file into [`Beatmap`] can fail.
208    #[derive(Debug)]
209    pub enum ParseBeatmapError {
210        #[error("failed to parse colors section")]
211        Colors(#[from] ParseColorsError),
212        #[error("failed to parse editor section")]
213        Editor(#[from] ParseEditorError),
214        #[error("failed to parse hit objects")]
215        HitOjects(#[from] ParseHitObjectsError),
216        #[error("failed to parse metadata section")]
217        Metadata(#[from] ParseMetadataError),
218    }
219}
220
221/// The parsing state for [`Beatmap`] in [`DecodeBeatmap`].
222pub struct BeatmapState {
223    pub version: i32,
224    pub editor: EditorState,
225    pub metadata: MetadataState,
226    pub colors: ColorsState,
227    pub hit_objects: HitObjectsState,
228}
229
230impl DecodeState for BeatmapState {
231    fn create(version: i32) -> Self {
232        Self {
233            version,
234            editor: EditorState::create(version),
235            metadata: MetadataState::create(version),
236            colors: ColorsState::create(version),
237            hit_objects: HitObjectsState::create(version),
238        }
239    }
240}
241
242impl From<BeatmapState> for Beatmap {
243    #[allow(clippy::useless_conversion)]
244    fn from(state: BeatmapState) -> Self {
245        let editor: Editor = state.editor.into();
246        let metadata: Metadata = state.metadata.into();
247        let colors: Colors = state.colors.into();
248        let hit_objects: HitObjects = state.hit_objects.into();
249
250        Beatmap {
251            format_version: state.version,
252            audio_file: hit_objects.audio_file,
253            audio_lead_in: hit_objects.audio_lead_in,
254            preview_time: hit_objects.preview_time,
255            default_sample_bank: hit_objects.default_sample_bank,
256            default_sample_volume: hit_objects.default_sample_volume,
257            stack_leniency: hit_objects.stack_leniency,
258            mode: hit_objects.mode,
259            letterbox_in_breaks: hit_objects.letterbox_in_breaks,
260            special_style: hit_objects.special_style,
261            widescreen_storyboard: hit_objects.widescreen_storyboard,
262            epilepsy_warning: hit_objects.epilepsy_warning,
263            samples_match_playback_rate: hit_objects.samples_match_playback_rate,
264            countdown: hit_objects.countdown,
265            countdown_offset: hit_objects.countdown_offset,
266            bookmarks: editor.bookmarks,
267            distance_spacing: editor.distance_spacing,
268            beat_divisor: editor.beat_divisor,
269            grid_size: editor.grid_size,
270            timeline_zoom: editor.timeline_zoom,
271            title: metadata.title,
272            title_unicode: metadata.title_unicode,
273            artist: metadata.artist,
274            artist_unicode: metadata.artist_unicode,
275            creator: metadata.creator,
276            version: metadata.version,
277            source: metadata.source,
278            tags: metadata.tags,
279            beatmap_id: metadata.beatmap_id,
280            beatmap_set_id: metadata.beatmap_set_id,
281            hp_drain_rate: hit_objects.hp_drain_rate,
282            circle_size: hit_objects.circle_size,
283            overall_difficulty: hit_objects.overall_difficulty,
284            approach_rate: hit_objects.approach_rate,
285            slider_multiplier: hit_objects.slider_multiplier,
286            slider_tick_rate: hit_objects.slider_tick_rate,
287            background_file: hit_objects.background_file,
288            breaks: hit_objects.breaks,
289            control_points: hit_objects.control_points,
290            custom_combo_colors: colors.custom_combo_colors,
291            custom_colors: colors.custom_colors,
292            hit_objects: hit_objects.hit_objects,
293        }
294    }
295}
296
297impl DecodeBeatmap for Beatmap {
298    type Error = ParseBeatmapError;
299    type State = BeatmapState;
300
301    fn parse_general(state: &mut Self::State, line: &str) -> Result<(), Self::Error> {
302        HitObjects::parse_general(&mut state.hit_objects, line)
303            .map_err(ParseBeatmapError::HitOjects)
304    }
305
306    fn parse_editor(state: &mut Self::State, line: &str) -> Result<(), Self::Error> {
307        Editor::parse_editor(&mut state.editor, line).map_err(ParseBeatmapError::Editor)
308    }
309
310    fn parse_metadata(state: &mut Self::State, line: &str) -> Result<(), Self::Error> {
311        Metadata::parse_metadata(&mut state.metadata, line).map_err(ParseBeatmapError::Metadata)
312    }
313
314    fn parse_difficulty(state: &mut Self::State, line: &str) -> Result<(), Self::Error> {
315        HitObjects::parse_difficulty(&mut state.hit_objects, line)
316            .map_err(ParseBeatmapError::HitOjects)
317    }
318
319    fn parse_events(state: &mut Self::State, line: &str) -> Result<(), Self::Error> {
320        HitObjects::parse_events(&mut state.hit_objects, line).map_err(ParseBeatmapError::HitOjects)
321    }
322
323    fn parse_timing_points(state: &mut Self::State, line: &str) -> Result<(), Self::Error> {
324        HitObjects::parse_timing_points(&mut state.hit_objects, line)
325            .map_err(ParseBeatmapError::HitOjects)
326    }
327
328    fn parse_colors(state: &mut Self::State, line: &str) -> Result<(), Self::Error> {
329        Colors::parse_colors(&mut state.colors, line).map_err(ParseBeatmapError::Colors)
330    }
331
332    fn parse_hit_objects(state: &mut Self::State, line: &str) -> Result<(), Self::Error> {
333        HitObjects::parse_hit_objects(&mut state.hit_objects, line)
334            .map_err(ParseBeatmapError::HitOjects)
335    }
336
337    fn parse_variables(_: &mut Self::State, _: &str) -> Result<(), Self::Error> {
338        Ok(())
339    }
340
341    fn parse_catch_the_beat(_: &mut Self::State, _: &str) -> Result<(), Self::Error> {
342        Ok(())
343    }
344
345    fn parse_mania(_: &mut Self::State, _: &str) -> Result<(), Self::Error> {
346        Ok(())
347    }
348}