Skip to main content

rosu_pp/model/beatmap/attributes/
builder.rs

1use rosu_map::section::general::GameMode;
2
3use crate::{
4    Beatmap, Difficulty, GameMods,
5    model::beatmap::{
6        BeatmapAttributes,
7        attributes::{ModStatus, attribute::BeatmapAttribute, difficulty::BeatmapDifficulty},
8    },
9};
10
11/// A builder for [`BeatmapAttributes`].
12#[derive(Clone, Debug, PartialEq)]
13#[must_use]
14pub struct BeatmapAttributesBuilder {
15    mode: GameMode,
16    is_convert: bool,
17    difficulty: BeatmapDifficulty,
18    mods: GameMods,
19    clock_rate: Option<f64>,
20}
21
22#[rustfmt::skip]
23macro_rules! set_attr {
24    ( $short:ident = $long:literal ) => {
25        #[doc = concat!(
26            "Specify the ", $long,
27            ". \n\n`fixed` determines if the given value should be used before \
28            or after accounting for mods, e.g. on `true` the value will be used \
29            as-is and on `false` it will be modified based on the mods."
30        )]
31        pub const fn $short(&mut self, $short: f32, fixed: bool) -> &mut Self {
32            self.difficulty.$short = if fixed {
33                BeatmapAttribute::Fixed($short)
34            } else {
35                BeatmapAttribute::Given($short)
36            };
37
38            self
39        }
40    };
41}
42
43impl BeatmapAttributesBuilder {
44    /// Create a new [`BeatmapAttributesBuilder`].
45    ///
46    /// The mode will be `GameMode::Osu` and attributes are set to `5.0`.
47    pub const fn new() -> Self {
48        Self {
49            mode: GameMode::Osu,
50            is_convert: false,
51            difficulty: BeatmapDifficulty::DEFAULT,
52            mods: GameMods::DEFAULT,
53            clock_rate: None,
54        }
55    }
56
57    /// Use the given [`Beatmap`]'s attributes, mode, and convert status.
58    pub const fn map(&mut self, map: &Beatmap) -> &mut Self {
59        self.mode = map.mode;
60        self.is_convert = map.is_convert;
61
62        self.difficulty = BeatmapDifficulty {
63            // Clamping necessary to match lazer on maps like /b/4243836.
64            ar: BeatmapAttribute::Value(map.ar.clamp(0.0, 10.0)),
65            od: BeatmapAttribute::Value(map.od.clamp(0.0, 10.0)),
66            cs: BeatmapAttribute::Value(map.cs),
67            hp: BeatmapAttribute::Value(map.hp),
68        };
69
70        self
71    }
72
73    set_attr!(ar = "approach rate");
74
75    set_attr!(od = "overall difficulty");
76
77    set_attr!(cs = "circle size");
78
79    set_attr!(hp = "drain rate");
80
81    /// Specify mods.
82    ///
83    /// Accepted types are
84    /// - `u32`
85    /// - [`rosu_mods::GameModsLegacy`]
86    /// - [`rosu_mods::GameMods`]
87    /// - [`rosu_mods::GameModsIntermode`]
88    /// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode)
89    ///
90    /// See <https://github.com/ppy/osu-api/wiki#mods>
91    pub fn mods(&mut self, mods: impl Into<GameMods>) -> &mut Self {
92        self.mods = mods.into();
93
94        self
95    }
96
97    /// Specify a custom clock rate.
98    pub const fn clock_rate(&mut self, clock_rate: f64) -> &mut Self {
99        self.clock_rate = Some(clock_rate);
100
101        self
102    }
103
104    /// Specify a [`GameMode`] and whether it's a converted map.
105    pub const fn mode(&mut self, mode: GameMode, is_convert: bool) -> &mut Self {
106        self.mode = mode;
107        self.is_convert = is_convert;
108
109        self
110    }
111
112    /// Specify all settings through [`Difficulty`].
113    pub fn difficulty(&mut self, difficulty: &Difficulty) -> &mut Self {
114        let map_diff = difficulty.get_map_difficulty();
115
116        self.difficulty = BeatmapDifficulty {
117            ar: self.difficulty.ar.overwrite(map_diff.ar),
118            cs: self.difficulty.cs.overwrite(map_diff.cs),
119            hp: self.difficulty.hp.overwrite(map_diff.hp),
120            od: self.difficulty.od.overwrite(map_diff.od),
121        };
122        self.mods = difficulty.get_mods().clone();
123        self.clock_rate = Some(difficulty.get_clock_rate());
124
125        self
126    }
127
128    /// Calculate the [`BeatmapAttributes`].
129    pub fn build(&self) -> BeatmapAttributes {
130        let mods = &self.mods;
131
132        let mut difficulty = self.difficulty.clone();
133        difficulty.apply_mods(mods, self.mode);
134
135        BeatmapAttributes {
136            difficulty,
137            clock_rate: self.clock_rate.unwrap_or_else(|| mods.clock_rate()),
138            mod_status: ModStatus::new(mods),
139            mode: self.mode,
140            is_convert: self.is_convert,
141            classic_and_not_v2: mods.cl() && !mods.sv2(),
142        }
143    }
144}
145
146impl Default for BeatmapAttributesBuilder {
147    fn default() -> Self {
148        Self::new()
149    }
150}
151
152impl From<&Beatmap> for BeatmapAttributesBuilder {
153    fn from(map: &Beatmap) -> Self {
154        let mut this = Self::new();
155        this.map(map);
156
157        this
158    }
159}
160
161impl From<&Difficulty> for BeatmapAttributesBuilder {
162    fn from(difficulty: &Difficulty) -> Self {
163        let mut this = Self::new();
164        this.difficulty(difficulty);
165
166        this
167    }
168}