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#[derive(Clone, Debug, PartialEq)]
21pub struct Beatmap {
22 pub format_version: i32,
23
24 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 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 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 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 pub background_file: String,
69 pub breaks: Vec<BreakPeriod>,
70
71 pub control_points: ControlPoints,
73
74 pub custom_combo_colors: Vec<Color>,
76 pub custom_colors: Vec<CustomColor>,
77
78 pub hit_objects: Vec<HitObject>,
80}
81
82impl Beatmap {
83 pub fn from_path(path: impl AsRef<Path>) -> Result<Self, io::Error> {
95 crate::from_path(path)
96 }
97
98 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 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 #[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
221pub 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}