Skip to main content

rosu_pp/model/beatmap/
mod.rs

1use std::{borrow::Cow, io, path::Path, str::FromStr};
2
3use rosu_map::{
4    LATEST_FORMAT_VERSION,
5    section::{general::GameMode, hit_objects::hit_samples::HitSoundType},
6};
7
8pub use rosu_map::section::events::BreakPeriod;
9
10use crate::{
11    Difficulty, GameMods, GradualDifficulty, GradualPerformance, Performance, catch::Catch,
12    mania::Mania, taiko::Taiko, util::sort,
13};
14
15pub use self::{
16    attributes::{
17        AdjustedBeatmapAttributes, BeatmapAttribute, BeatmapAttributes, BeatmapAttributesBuilder,
18        HitWindows,
19    },
20    decode::{BeatmapState, ParseBeatmapError},
21    suspicious::TooSuspicious,
22};
23
24pub(crate) use self::attributes::BeatmapAttributesExt;
25
26use super::{
27    control_point::{
28        DifficultyPoint, EffectPoint, TimingPoint, difficulty_point_at, effect_point_at,
29        timing_point_at,
30    },
31    hit_object::HitObject,
32    mode::ConvertError,
33};
34
35pub(crate) mod attributes;
36mod bpm;
37mod decode;
38mod suspicious;
39
40/// All beatmap data that is relevant for difficulty and performance
41/// calculation.
42#[derive(Clone, Debug, PartialEq)]
43pub struct Beatmap {
44    pub version: i32,
45    pub is_convert: bool,
46
47    // General
48    pub stack_leniency: f32,
49    pub mode: GameMode,
50
51    // Difficulty
52    pub ar: f32,
53    pub cs: f32,
54    pub hp: f32,
55    pub od: f32,
56    pub slider_multiplier: f64,
57    pub slider_tick_rate: f64,
58
59    // Events
60    pub breaks: Vec<BreakPeriod>,
61
62    // TimingPoints
63    pub timing_points: Vec<TimingPoint>,
64    pub difficulty_points: Vec<DifficultyPoint>,
65    pub effect_points: Vec<EffectPoint>,
66
67    // HitObjects
68    pub hit_objects: Vec<HitObject>,
69    pub hit_sounds: Vec<HitSoundType>,
70}
71
72impl Beatmap {
73    /// Parse a [`Beatmap`] by providing a path to a `.osu` file.
74    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, io::Error> {
75        rosu_map::from_path(path)
76    }
77
78    /// Parse a [`Beatmap`] by providing the content of a `.osu` file as a
79    /// slice of bytes.
80    pub fn from_bytes(bytes: &[u8]) -> Result<Self, io::Error> {
81        rosu_map::from_bytes(bytes)
82    }
83
84    /// Returns a [`BeatmapAttributesBuilder`] to calculate modified beatmap
85    /// attributes.
86    pub fn attributes(&self) -> BeatmapAttributesBuilder {
87        BeatmapAttributesBuilder::from(self)
88    }
89
90    /// The beats per minute of the map.
91    pub fn bpm(&self) -> f64 {
92        bpm::bpm(self.hit_objects.last(), &self.timing_points)
93    }
94
95    /// Create a performance calculator for this [`Beatmap`].
96    pub fn performance(&self) -> Performance<'_> {
97        Performance::new(self)
98    }
99
100    /// Create a gradual difficulty calculator for this [`Beatmap`].
101    pub fn gradual_difficulty(&self, difficulty: Difficulty) -> GradualDifficulty {
102        GradualDifficulty::new(difficulty, self)
103    }
104
105    /// Create a gradual performance calculator for this [`Beatmap`].
106    pub fn gradual_performance(&self, difficulty: Difficulty) -> GradualPerformance {
107        GradualPerformance::new(difficulty, self)
108    }
109
110    /// Finds the [`TimingPoint`] that is active at the given time.
111    pub(crate) fn timing_point_at(&self, time: f64) -> Option<&TimingPoint> {
112        timing_point_at(&self.timing_points, time)
113    }
114
115    /// Finds the [`DifficultyPoint`] that is active at the given time.
116    pub(crate) fn difficulty_point_at(&self, time: f64) -> Option<&DifficultyPoint> {
117        difficulty_point_at(&self.difficulty_points, time)
118    }
119
120    /// Finds the [`EffectPoint`] that is active at the given time.
121    pub(crate) fn effect_point_at(&self, time: f64) -> Option<&EffectPoint> {
122        effect_point_at(&self.effect_points, time)
123    }
124
125    /// Sum up the duration of all breaks (in milliseconds).
126    pub fn total_break_time(&self) -> f64 {
127        self.breaks.iter().map(BreakPeriod::duration).sum()
128    }
129
130    /// Attempt to convert a [`Beatmap`] to the specified mode.
131    pub fn convert(mut self, mode: GameMode, mods: &GameMods) -> Result<Self, ConvertError> {
132        self.convert_mut(mode, mods)?;
133
134        Ok(self)
135    }
136
137    /// Attempt to convert a [`&Beatmap`] to the specified mode.
138    ///
139    /// [`&Beatmap`]: Beatmap
140    pub fn convert_ref(
141        &self,
142        mode: GameMode,
143        mods: &GameMods,
144    ) -> Result<Cow<'_, Self>, ConvertError> {
145        if self.mode == mode {
146            return Ok(Cow::Borrowed(self));
147        } else if self.is_convert {
148            return Err(ConvertError::AlreadyConverted);
149        } else if self.mode != GameMode::Osu {
150            return Err(ConvertError::Convert {
151                from: self.mode,
152                to: mode,
153            });
154        }
155
156        let mut map = self.to_owned();
157
158        match mode {
159            GameMode::Taiko => Taiko::convert(&mut map),
160            GameMode::Catch => Catch::convert(&mut map),
161            GameMode::Mania => Mania::convert(&mut map, mods),
162            GameMode::Osu => unreachable!(),
163        }
164
165        Ok(Cow::Owned(map))
166    }
167
168    /// Attempt to convert a [`&mut Beatmap`] to the specified mode.
169    ///
170    /// [`&mut Beatmap`]: Beatmap
171    pub fn convert_mut(&mut self, mode: GameMode, mods: &GameMods) -> Result<(), ConvertError> {
172        if self.mode == mode {
173            return Ok(());
174        } else if self.is_convert {
175            return Err(ConvertError::AlreadyConverted);
176        } else if self.mode != GameMode::Osu {
177            return Err(ConvertError::Convert {
178                from: self.mode,
179                to: mode,
180            });
181        }
182
183        match mode {
184            GameMode::Taiko => Taiko::convert(self),
185            GameMode::Catch => Catch::convert(self),
186            GameMode::Mania => Mania::convert(self, mods),
187            GameMode::Osu => unreachable!(),
188        }
189
190        Ok(())
191    }
192
193    /// Sort hitobjects via [`sort::osu_legacy`] with a specific comparator.
194    ///
195    /// Mania's difficulty calculation applies this sorting *after* all mod
196    /// conversions have been applied to the hitobjects.
197    ///
198    /// <https://github.com/ppy/osu/blob/28c846b4d9366484792e27f4729cd1afa2cdeb66/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs#L70>
199    pub(crate) fn mania_hitobjects_legacy_sort(&mut self) {
200        sort::osu_legacy(&mut self.hit_objects, |a, b| {
201            (f64::round_ties_even(a.start_time) as i32)
202                .cmp(&(f64::round_ties_even(b.start_time) as i32))
203        });
204    }
205
206    /// Check whether hitobjects appear too suspicious for further calculation.
207    ///
208    /// Sometimes a [`Beatmap`] isn't created for gameplay but rather to test
209    /// the limits of osu! itself. Difficulty- and/or performance calculation
210    /// should likely be avoided on these maps due to potential performance
211    /// issues.
212    pub fn check_suspicion(&self) -> Result<(), TooSuspicious> {
213        match TooSuspicious::new(self) {
214            None => Ok(()),
215            Some(err) => Err(err),
216        }
217    }
218}
219
220impl FromStr for Beatmap {
221    type Err = io::Error;
222
223    /// Parse a [`Beatmap`] by providing the content of a `.osu` file as a
224    /// string.
225    fn from_str(s: &str) -> Result<Self, Self::Err> {
226        rosu_map::from_str(s)
227    }
228}
229
230const DEFAULT_SLIDER_LENIENCY: f32 = 0.7;
231
232impl Default for Beatmap {
233    fn default() -> Self {
234        Self {
235            version: LATEST_FORMAT_VERSION,
236            is_convert: false,
237            stack_leniency: DEFAULT_SLIDER_LENIENCY,
238            mode: GameMode::default(),
239            ar: 5.0,
240            cs: 5.0,
241            hp: 5.0,
242            od: 5.0,
243            slider_multiplier: 1.4,
244            slider_tick_rate: 1.0,
245            breaks: Vec::default(),
246            timing_points: Vec::default(),
247            difficulty_points: Vec::default(),
248            effect_points: Vec::default(),
249            hit_objects: Vec::default(),
250            hit_sounds: Vec::default(),
251        }
252    }
253}