osu_file_parser/osu_file/timingpoints/
mod.rs

1pub mod error;
2pub mod types;
3
4use crate::osu_file::types::Decimal;
5use either::Either;
6use nom::{
7    branch::alt,
8    combinator::{cut, map_res, success, verify, rest},
9    error::context,
10    sequence::{preceded, tuple},
11    Parser,
12};
13use rust_decimal_macros::dec;
14
15use crate::{helper::parse_zero_one_bool, parsers::*};
16
17use super::{
18    Error, Integer, Version, VersionedDefault, VersionedFrom, VersionedFromStr, VersionedToString,
19};
20
21pub use error::*;
22pub use types::*;
23
24#[derive(Clone, Debug, Default, Hash, PartialEq, Eq)]
25pub struct TimingPoints(pub Vec<TimingPoint>);
26
27impl VersionedFromStr for TimingPoints {
28    type Err = Error<ParseError>;
29
30    fn from_str(s: &str, version: Version) -> std::result::Result<Option<Self>, Self::Err> {
31        let mut timing_points = Vec::new();
32
33        for (line_index, s) in s.lines().enumerate() {
34            if s.trim().is_empty() {
35                continue;
36            }
37
38            timing_points.push(Error::new_from_result_into(
39                TimingPoint::from_str(s, version),
40                line_index,
41            )?);
42        }
43
44        if let Some(s) = timing_points.get(0) {
45            if s.is_some() {
46                Ok(Some(TimingPoints(
47                    timing_points
48                        .into_iter()
49                        .map(|v| v.unwrap())
50                        .collect::<Vec<_>>(),
51                )))
52            } else {
53                Ok(None)
54            }
55        } else {
56            Ok(Some(TimingPoints(Vec::new())))
57        }
58    }
59}
60
61impl VersionedToString for TimingPoints {
62    fn to_string(&self, version: Version) -> Option<String> {
63        Some(
64            self.0
65                .iter()
66                .filter_map(|t| t.to_string(version))
67                .collect::<Vec<_>>()
68                .join("\n"),
69        )
70    }
71}
72
73impl VersionedDefault for TimingPoints {
74    fn default(_: Version) -> Option<Self> {
75        Some(TimingPoints(Vec::new()))
76    }
77}
78
79/// Struct representing a timing point.
80/// Each timing point influences a specified portion of the map, commonly called a `timing section`.
81/// The .osu file format requires these to be sorted in chronological order.
82#[derive(Clone, Hash, PartialEq, Eq, Debug)]
83pub struct TimingPoint {
84    // for some reason decimal is parsed anyway in the beatmap???
85    time: Decimal,
86    beat_length: Decimal,
87    meter: Integer,
88    sample_set: SampleSet,
89    sample_index: SampleIndex,
90    volume: Volume,
91    uninherited: Option<bool>,
92    effects: Option<Effects>,
93}
94
95impl TimingPoint {
96    /// Converts beat duration in milliseconds to BPM.
97    pub fn beat_duration_ms_to_bpm(
98        beat_duration_ms: rust_decimal::Decimal,
99    ) -> rust_decimal::Decimal {
100        rust_decimal::Decimal::ONE / beat_duration_ms * dec!(60000)
101    }
102
103    /// Converts BPM to beat duration in milliseconds.
104    pub fn bpm_to_beat_duration_ms(bpm: rust_decimal::Decimal) -> rust_decimal::Decimal {
105        rust_decimal::Decimal::ONE / (bpm / dec!(60000))
106    }
107
108    /// New instance of `TimingPoint` that is inherited.
109    pub fn new_inherited(
110        time: Integer,
111        slider_velocity_multiplier: rust_decimal::Decimal,
112        meter: Integer,
113        sample_set: SampleSet,
114        sample_index: SampleIndex,
115        volume: Volume,
116        effects: Effects,
117    ) -> Self {
118        let beat_length = (rust_decimal::Decimal::ONE / slider_velocity_multiplier) * dec!(-100);
119
120        Self {
121            time: time.into(),
122            beat_length: beat_length.into(),
123            meter,
124            sample_set,
125            sample_index,
126            volume,
127            uninherited: Some(false),
128            effects: Some(effects),
129        }
130    }
131
132    /// New instance of `TimingPoint` that is uninherited.
133    pub fn new_uninherited(
134        time: Integer,
135        beat_duration_ms: Decimal,
136        meter: Integer,
137        sample_set: SampleSet,
138        sample_index: SampleIndex,
139        volume: Volume,
140        effects: Effects,
141    ) -> Self {
142        Self {
143            time: time.into(),
144            beat_length: beat_duration_ms,
145            meter,
146            sample_set,
147            sample_index,
148            volume,
149            uninherited: Some(true),
150            effects: Some(effects),
151        }
152    }
153
154    /// Calculates BPM using the `beatLength` field when unherited.
155    /// - Returns `None` if the timing point is inherited or `beat_length` isn't a valid decimal.
156    pub fn calc_bpm(&self) -> Option<rust_decimal::Decimal> {
157        match self.uninherited {
158            Some(uninherited) => {
159                if uninherited {
160                    match self.beat_length.get() {
161                        Either::Left(value) => Some(Self::beat_duration_ms_to_bpm(*value)),
162                        Either::Right(_) => None,
163                    }
164                } else {
165                    None
166                }
167            }
168            None => None,
169        }
170    }
171    /// Calculates the slider velocity multiplier when the timing point is inherited.
172    /// - Returns `None` if the timing point is uninherited or `beat_length` isn't a valid decimal.
173    pub fn calc_slider_velocity_multiplier(&self) -> Option<rust_decimal::Decimal> {
174        match self.uninherited {
175            Some(uninherited) => {
176                if uninherited {
177                    None
178                } else {
179                    match self.beat_length.get() {
180                        Either::Left(value) => {
181                            Some(rust_decimal::Decimal::ONE / (value / dec!(-100)))
182                        }
183                        Either::Right(_) => None,
184                    }
185                }
186            }
187            None => None,
188        }
189    }
190
191    /// Start time of the timing section, in milliseconds from the beginning of the beatmap's audio.
192    /// The end of the timing section is the next timing point's time (or never, if this is the last timing point).
193    pub fn time(&self) -> &Decimal {
194        &self.time
195    }
196
197    /// Set the timing point's start time.
198    pub fn set_time(&mut self, time: Integer) {
199        self.time = time.into();
200    }
201
202    /// Amount of beats in a measure. Inherited timing points ignore this property.
203    pub fn meter(&self) -> i32 {
204        self.meter
205    }
206
207    /// Set the timing point's meter field.
208    pub fn set_meter(&mut self, meter: Integer) {
209        self.meter = meter;
210    }
211
212    /// Default sample set for hit objects
213    pub fn sample_set(&self) -> SampleSet {
214        self.sample_set
215    }
216
217    /// Set the timing point's sample set.
218    pub fn set_sample_set(&mut self, sample_set: SampleSet) {
219        self.sample_set = sample_set;
220    }
221
222    /// Custom sample index for hit objects.
223    pub fn sample_index(&self) -> SampleIndex {
224        self.sample_index
225    }
226
227    /// Get a mutable reference to the timing point's sample index.
228    pub fn sample_index_mut(&mut self) -> &mut SampleIndex {
229        &mut self.sample_index
230    }
231
232    /// Volume percentage for hit objects.
233    pub fn volume(&self) -> &Volume {
234        &self.volume
235    }
236
237    /// Set the timing point's volume.
238    pub fn set_volume(&mut self, volume: Volume) {
239        self.volume = volume;
240    }
241
242    /// Get the timing point's uninherited.
243    pub fn uninherited(&self) -> bool {
244        self.uninherited.unwrap_or(true)
245    }
246
247    /// Set the timing point's uninherited.
248    pub fn set_uninherited(&mut self, uninherited: bool) {
249        self.uninherited = Some(uninherited);
250    }
251
252    /// Get the timing point's effects.
253    pub fn effects(&self) -> Option<&Effects> {
254        self.effects.as_ref()
255    }
256
257    /// Get a mutable reference to the timing point's effects.
258    pub fn effects_mut(&mut self) -> &mut Option<Effects> {
259        &mut self.effects
260    }
261}
262
263const OLD_VERSION_TIME_OFFSET: rust_decimal::Decimal = dec!(24);
264
265impl VersionedFromStr for TimingPoint {
266    type Err = ParseTimingPointError;
267
268    fn from_str(s: &str, version: Version) -> std::result::Result<Option<Self>, Self::Err> {
269        let meter_fallback = 4;
270        let sample_set_fallback = SampleSet::Normal;
271        let sample_index_fallback = <SampleIndex as VersionedFrom<u32>>::from(1, version).unwrap();
272        let volume_fallback = <Volume as VersionedFrom<Integer>>::from(100, version).unwrap();
273
274        // for now we limit some fields for certain versions since we aren't sure if they are optional or not
275        // could change in the future to be more flexible
276        let (
277            _,
278            (
279                time,
280                (beat_length, meter, sample_set, (sample_index, (volume, uninherited, effects))),
281            ),
282        ) = tuple((
283            context(
284                ParseTimingPointError::InvalidTime.into(),
285                comma_field_type(),
286            )
287            .map(|mut t: Decimal| {
288                if (3..=4).contains(&version) {
289                    if let Either::Left(value) = t.get_mut() {
290                        *value += OLD_VERSION_TIME_OFFSET;
291                    }
292                }
293
294                t
295            }),
296            preceded(
297                context(ParseTimingPointError::MissingBeatLength.into(), comma()),
298                alt((
299                    preceded(verify(success(0), |_| version <= 3), consume_rest_type()).map(
300                        |beat_length| {
301                            (
302                                beat_length,
303                                meter_fallback,
304                                sample_set_fallback,
305                                (sample_index_fallback, (volume_fallback, None, None)),
306                            )
307                        },
308                    ),
309                    tuple((
310                        comma_field_type(),
311                        preceded(
312                            context(ParseTimingPointError::MissingMeter.into(), comma()),
313                            context(
314                                ParseTimingPointError::InvalidMeter.into(),
315                                comma_field_type(),
316                            ),
317                        ),
318                        preceded(
319                            context(ParseTimingPointError::MissingSampleSet.into(), comma()),
320                            context(
321                                ParseTimingPointError::InvalidSampleSet.into(),
322                                comma_field_versioned_type(version),
323                            ),
324                        ),
325                        preceded(
326                            context(ParseTimingPointError::MissingSampleIndex.into(), comma()),
327                            alt((
328                                preceded(
329                                    verify(success(0), |_| version <= 4),
330                                    context(
331                                        ParseTimingPointError::InvalidSampleIndex.into(),
332                                        consume_rest_versioned_type(version),
333                                    ),
334                                )
335                                .map(|sample_index| (sample_index, (volume_fallback, None, None))),
336                                tuple((
337                                    context(
338                                        ParseTimingPointError::InvalidSampleIndex.into(),
339                                        comma_field_versioned_type(version),
340                                    ),
341                                    preceded(
342                                        context(
343                                            ParseTimingPointError::MissingVolume.into(),
344                                            comma(),
345                                        ),
346                                        alt((
347                                            preceded(
348                                                verify(success(0), |_| version <= 4),
349                                                context(
350                                                    ParseTimingPointError::InvalidVolume.into(),
351                                                    cut(consume_rest_versioned_type(version)),
352                                                ),
353                                            )
354                                            .map(|volume| (volume, None, None)),
355                                            context(
356                                                ParseTimingPointError::InvalidVolume.into(),
357                                                consume_rest_versioned_type(version),
358                                            )
359                                            .map(|volume| (volume, None, None)),
360                                            tuple((
361                                                context(
362                                                    ParseTimingPointError::InvalidVolume.into(),
363                                                    comma_field_versioned_type(version),
364                                                ),
365                                                preceded(context(
366                                                    ParseTimingPointError::MissingUninherited
367                                                        .into(),
368                                                    comma(),
369                                                ), 
370                                                alt((
371                                                    preceded(verify(success(0), |_| version <= 5), 
372                                                    context(
373                                                        ParseTimingPointError::InvalidUninherited.into(),
374                                                        map_res(rest, parse_zero_one_bool),
375                                                    )
376                                                ).map(|uninherited| (Some(uninherited), None)),         
377                                                tuple((
378                                                    context(
379                                                        ParseTimingPointError::InvalidUninherited
380                                                            .into(),
381                                                        map_res(comma_field(), parse_zero_one_bool),
382                                                    ),
383                                                preceded(
384                                                    context(
385                                                        ParseTimingPointError::MissingEffects
386                                                            .into(),
387                                                        comma(),
388                                                    ),
389                                                    context(
390                                                        ParseTimingPointError::InvalidEffects
391                                                            .into(),
392                                                        consume_rest_versioned_type(version),
393                                                    ),
394                                                ),
395                                                )).map(|(uninherited, effects)| (Some(uninherited), Some(effects))),
396                                            ))
397                                            )))
398                                            .map(
399                                                |(volume, (uninherited, effects))| {
400                                                    (volume, uninherited, effects)
401                                                },
402                                            ),
403                                        )),
404                                    ),
405                                ))
406                                .map(
407                                    |(sample_index, (volume, uninherited, effects))| {
408                                        (sample_index, (volume, uninherited, effects))
409                                    },
410                                ),
411                            )),
412                        ),
413                    ))
414                    .map(
415                        |(
416                            beat_length,
417                            meter,
418                            sample_set,
419                            (sample_index, (volume, uninherited, effects)),
420                        )| {
421                            (
422                                beat_length,
423                                meter,
424                                sample_set,
425                                (sample_index, (volume, uninherited, effects)),
426                            )
427                        },
428                    ),
429                )),
430            ),
431        ))(s)?;
432
433        Ok(Some(TimingPoint {
434            time,
435            beat_length,
436            meter,
437            sample_set,
438            sample_index,
439            volume,
440            uninherited,
441            effects,
442        }))
443    }
444}
445
446impl VersionedToString for TimingPoint {
447    fn to_string(&self, version: Version) -> Option<String> {
448        let mut fields = vec![
449            if (3..=4).contains(&version) {
450                match self.time.get() {
451                    Either::Left(value) => (value - OLD_VERSION_TIME_OFFSET).to_string(),
452                    Either::Right(value) => value.to_string(),
453                }
454            } else {
455                self.time.to_string()
456            },
457            self.beat_length.to_string(),
458        ];
459
460        if version > 3 {
461            fields.push(self.meter.to_string());
462            fields.push(self.sample_set.to_string(version).unwrap());
463            fields.push(self.sample_index.to_string(version).unwrap());
464        }
465        if version > 4 {
466            fields.push(self.volume.to_string(version).unwrap());
467            match self.uninherited {
468                Some(value) => fields.push((value as u8).to_string()),
469                None => {
470                    if self.effects.is_some() {
471                        fields.push(String::new())
472                    }
473                }
474            }
475            if let Some(value) = self.effects {
476                fields.push(value.to_string(version).unwrap())
477            }
478        }
479
480        Some(fields.join(","))
481    }
482}