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#[derive(Clone, Debug, PartialEq)]
43pub struct Beatmap {
44 pub version: i32,
45 pub is_convert: bool,
46
47 pub stack_leniency: f32,
49 pub mode: GameMode,
50
51 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 pub breaks: Vec<BreakPeriod>,
61
62 pub timing_points: Vec<TimingPoint>,
64 pub difficulty_points: Vec<DifficultyPoint>,
65 pub effect_points: Vec<EffectPoint>,
66
67 pub hit_objects: Vec<HitObject>,
69 pub hit_sounds: Vec<HitSoundType>,
70}
71
72impl Beatmap {
73 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, io::Error> {
75 rosu_map::from_path(path)
76 }
77
78 pub fn from_bytes(bytes: &[u8]) -> Result<Self, io::Error> {
81 rosu_map::from_bytes(bytes)
82 }
83
84 pub fn attributes(&self) -> BeatmapAttributesBuilder {
87 BeatmapAttributesBuilder::from(self)
88 }
89
90 pub fn bpm(&self) -> f64 {
92 bpm::bpm(self.hit_objects.last(), &self.timing_points)
93 }
94
95 pub fn performance(&self) -> Performance<'_> {
97 Performance::new(self)
98 }
99
100 pub fn gradual_difficulty(&self, difficulty: Difficulty) -> GradualDifficulty {
102 GradualDifficulty::new(difficulty, self)
103 }
104
105 pub fn gradual_performance(&self, difficulty: Difficulty) -> GradualPerformance {
107 GradualPerformance::new(difficulty, self)
108 }
109
110 pub(crate) fn timing_point_at(&self, time: f64) -> Option<&TimingPoint> {
112 timing_point_at(&self.timing_points, time)
113 }
114
115 pub(crate) fn difficulty_point_at(&self, time: f64) -> Option<&DifficultyPoint> {
117 difficulty_point_at(&self.difficulty_points, time)
118 }
119
120 pub(crate) fn effect_point_at(&self, time: f64) -> Option<&EffectPoint> {
122 effect_point_at(&self.effect_points, time)
123 }
124
125 pub fn total_break_time(&self) -> f64 {
127 self.breaks.iter().map(BreakPeriod::duration).sum()
128 }
129
130 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 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 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 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 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 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}