1use std::fmt::{Debug, Formatter, Result as FmtResult};
2
3use rosu_mods::{
4 generated_mods::{
5 DifficultyAdjustCatch, DifficultyAdjustMania, DifficultyAdjustOsu, DifficultyAdjustTaiko,
6 },
7 GameMod, GameModIntermode, GameMods as GameModsLazer, GameModsIntermode, GameModsLegacy,
8};
9
10pub mod rosu_mods {
12 pub use rosu_mods::*;
13}
14
15#[derive(Clone, PartialEq)]
37pub enum GameMods {
38 Lazer(GameModsLazer),
39 Intermode(GameModsIntermode),
40 Legacy(GameModsLegacy),
41}
42
43impl Debug for GameMods {
44 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
45 match self {
46 Self::Lazer(ref mods) => Debug::fmt(mods, f),
47 Self::Intermode(ref mods) => Debug::fmt(mods, f),
48 Self::Legacy(ref mods) => Debug::fmt(mods, f),
49 }
50 }
51}
52
53impl GameMods {
54 pub(crate) const DEFAULT: Self = Self::Legacy(GameModsLegacy::NoMod);
55
56 pub(crate) fn clock_rate(&self) -> f64 {
61 match self {
62 Self::Lazer(ref mods) => mods
63 .iter()
64 .find_map(|m| {
65 let default = match m.intermode() {
66 GameModIntermode::DoubleTime | GameModIntermode::HalfTime => {
67 return m.clock_rate()
68 }
69 GameModIntermode::Nightcore => 1.5,
70 GameModIntermode::Daycore => 0.75,
71 _ => return None,
72 };
73
74 Some(default * (m.clock_rate()? / default))
75 })
76 .unwrap_or(1.0),
77 Self::Intermode(ref mods) => mods.legacy_clock_rate(),
78 Self::Legacy(mods) => mods.clock_rate(),
79 }
80 }
81
82 pub(crate) fn od_ar_hp_multiplier(&self) -> f64 {
83 if self.hr() {
84 1.4
85 } else if self.ez() {
86 0.5
87 } else {
88 1.0
89 }
90 }
91
92 pub(crate) fn hardrock_offsets(&self) -> bool {
94 fn custom_hardrock_offsets(mods: &GameMods) -> Option<bool> {
95 match mods {
96 GameMods::Lazer(ref mods) => mods.iter().find_map(|gamemod| match gamemod {
97 GameMod::DifficultyAdjustCatch(DifficultyAdjustCatch {
98 hard_rock_offsets,
99 ..
100 }) => *hard_rock_offsets,
101 _ => None,
102 }),
103 GameMods::Intermode(_) | GameMods::Legacy(_) => None,
104 }
105 }
106
107 custom_hardrock_offsets(self).unwrap_or_else(|| self.hr())
108 }
109
110 pub(crate) fn no_slider_head_acc(&self, lazer: bool) -> bool {
111 match self {
112 Self::Lazer(ref mods) => mods
113 .iter()
114 .find_map(|m| match m {
115 GameMod::ClassicOsu(cl) => Some(cl.no_slider_head_accuracy.unwrap_or(true)),
116 _ => None,
117 })
118 .unwrap_or(!lazer),
119 Self::Intermode(ref mods) => mods.contains(GameModIntermode::Classic) || !lazer,
120 Self::Legacy(_) => !lazer,
121 }
122 }
123
124 pub(crate) fn reflection(&self) -> Reflection {
125 match self {
126 Self::Lazer(ref mods) => mods
127 .iter()
128 .find_map(|m| match m {
129 GameMod::HardRockOsu(_) => Some(Reflection::Vertical),
130 GameMod::MirrorOsu(mr) => match mr.reflection.as_deref() {
131 None => Some(Reflection::Horizontal),
132 Some("1") => Some(Reflection::Vertical),
133 Some("2") => Some(Reflection::Both),
134 Some(_) => Some(Reflection::None),
135 },
136 GameMod::MirrorCatch(_) => Some(Reflection::Horizontal),
137 _ => None,
138 })
139 .unwrap_or(Reflection::None),
140 Self::Intermode(ref mods) => {
141 if mods.contains(GameModIntermode::HardRock) {
142 Reflection::Vertical
143 } else {
144 Reflection::None
145 }
146 }
147 Self::Legacy(mods) => {
148 if mods.contains(GameModsLegacy::HardRock) {
149 Reflection::Vertical
150 } else {
151 Reflection::None
152 }
153 }
154 }
155 }
156
157 pub(crate) fn mania_keys(&self) -> Option<f32> {
158 match self {
159 Self::Lazer(ref mods) => {
160 if mods.contains_intermode(GameModIntermode::OneKey) {
161 Some(1.0)
162 } else if mods.contains_intermode(GameModIntermode::TwoKeys) {
163 Some(2.0)
164 } else if mods.contains_intermode(GameModIntermode::ThreeKeys) {
165 Some(3.0)
166 } else if mods.contains_intermode(GameModIntermode::FourKeys) {
167 Some(4.0)
168 } else if mods.contains_intermode(GameModIntermode::FiveKeys) {
169 Some(5.0)
170 } else if mods.contains_intermode(GameModIntermode::SixKeys) {
171 Some(6.0)
172 } else if mods.contains_intermode(GameModIntermode::SevenKeys) {
173 Some(7.0)
174 } else if mods.contains_intermode(GameModIntermode::EightKeys) {
175 Some(8.0)
176 } else if mods.contains_intermode(GameModIntermode::NineKeys) {
177 Some(9.0)
178 } else if mods.contains_intermode(GameModIntermode::TenKeys) {
179 Some(10.0)
180 } else {
181 None
182 }
183 }
184 Self::Intermode(ref mods) => {
185 if mods.contains(GameModIntermode::OneKey) {
186 Some(1.0)
187 } else if mods.contains(GameModIntermode::TwoKeys) {
188 Some(2.0)
189 } else if mods.contains(GameModIntermode::ThreeKeys) {
190 Some(3.0)
191 } else if mods.contains(GameModIntermode::FourKeys) {
192 Some(4.0)
193 } else if mods.contains(GameModIntermode::FiveKeys) {
194 Some(5.0)
195 } else if mods.contains(GameModIntermode::SixKeys) {
196 Some(6.0)
197 } else if mods.contains(GameModIntermode::SevenKeys) {
198 Some(7.0)
199 } else if mods.contains(GameModIntermode::EightKeys) {
200 Some(8.0)
201 } else if mods.contains(GameModIntermode::NineKeys) {
202 Some(9.0)
203 } else if mods.contains(GameModIntermode::TenKeys) {
204 Some(10.0)
205 } else {
206 None
207 }
208 }
209 Self::Legacy(ref mods) => {
210 if mods.contains(GameModsLegacy::Key1) {
211 Some(1.0)
212 } else if mods.contains(GameModsLegacy::Key2) {
213 Some(2.0)
214 } else if mods.contains(GameModsLegacy::Key3) {
215 Some(3.0)
216 } else if mods.contains(GameModsLegacy::Key4) {
217 Some(4.0)
218 } else if mods.contains(GameModsLegacy::Key5) {
219 Some(5.0)
220 } else if mods.contains(GameModsLegacy::Key6) {
221 Some(6.0)
222 } else if mods.contains(GameModsLegacy::Key7) {
223 Some(7.0)
224 } else if mods.contains(GameModsLegacy::Key8) {
225 Some(8.0)
226 } else if mods.contains(GameModsLegacy::Key9) {
227 Some(9.0)
228 } else {
229 None
230 }
231 }
232 }
233 }
234
235 pub(crate) fn scroll_speed(&self) -> Option<f64> {
236 let Self::Lazer(mods) = self else { return None };
237
238 mods.iter()
239 .find_map(|m| match m {
240 GameMod::DifficultyAdjustTaiko(da) => Some(da.scroll_speed),
241 _ => None,
242 })
243 .flatten()
244 }
245
246 pub(crate) fn random_seed(&self) -> Option<i32> {
247 let Self::Lazer(mods) = self else { return None };
248
249 mods.iter()
250 .find_map(|m| match m {
251 GameMod::RandomTaiko(m) => m.seed,
254 GameMod::RandomMania(m) => m.seed,
255 _ => None,
256 })
257 .map(|seed| seed as i32)
258 }
259}
260
261macro_rules! impl_map_attr {
262 ( $( $fn:ident: $field:ident [ $( $mode:ident ),* ] [$s:literal] ;)* ) => {
263 impl GameMods {
264 $(
265 #[doc = "Check whether the mods specify a custom "]
266 #[doc = $s]
267 #[doc = "value."]
268 pub(crate) fn $fn(&self) -> Option<f64> {
269 match self {
270 Self::Lazer(ref mods) => mods.iter().find_map(|gamemod| match gamemod {
271 $( impl_map_attr!( @ $mode $field) => *$field, )*
272 _ => None,
273 }),
274 Self::Intermode(_) | Self::Legacy(_) => None,
275 }
276 }
277 )*
278 }
279 };
280
281 ( @ Osu $field:ident ) => { GameMod::DifficultyAdjustOsu(DifficultyAdjustOsu { $field, .. }) };
282 ( @ Taiko $field:ident ) => { GameMod::DifficultyAdjustTaiko(DifficultyAdjustTaiko { $field, .. }) };
283 ( @ Catch $field:ident ) => { GameMod::DifficultyAdjustCatch(DifficultyAdjustCatch { $field, .. }) };
284 ( @ Mania $field:ident ) => { GameMod::DifficultyAdjustMania(DifficultyAdjustMania { $field, .. }) };
285}
286
287impl_map_attr! {
288 ar: approach_rate [Osu, Catch] ["ar"];
289 cs: circle_size [Osu, Catch] ["cs"];
290 hp: drain_rate [Osu, Taiko, Catch, Mania] ["hp"];
291 od: overall_difficulty [Osu, Taiko, Catch, Mania] ["od"];
292}
293
294macro_rules! impl_has_mod {
295 ( $( $fn:ident: $is_legacy:tt $name:ident [ $s:literal ], )* ) => {
296 impl GameMods {
297 $(
298 #[doc = "Check whether [`GameMods`] contain `"]
300 #[doc = $s]
301 #[doc = "`."]
302 pub(crate) fn $fn(&self) -> bool {
303 match self {
304 Self::Lazer(ref mods) => {
305 mods.contains_intermode(GameModIntermode::$name)
306 },
307 Self::Intermode(ref mods) => {
308 mods.contains(GameModIntermode::$name)
309 },
310 Self::Legacy(_mods) => {
311 impl_has_mod!(LEGACY $is_legacy $name _mods)
312 },
313 }
314 }
315 )*
316 }
317 };
318
319 ( LEGACY + $name:ident $mods:ident ) => {
320 $mods.contains(GameModsLegacy::$name)
321 };
322
323 ( LEGACY - $name:ident $mods:ident ) => {
324 false
325 };
326}
327
328impl_has_mod! {
329 nf: + NoFail ["NoFail"],
330 ez: + Easy ["Easy"],
331 td: + TouchDevice ["TouchDevice"],
332 hd: + Hidden ["Hidden"],
333 hr: + HardRock ["HardRock"],
334 rx: + Relax ["Relax"],
335 fl: + Flashlight ["Flashlight"],
336 so: + SpunOut ["SpunOut"],
337 ap: + Autopilot ["Autopilot"],
338 bl: - Blinds ["Blinds"],
339 cl: - Classic ["Classic"],
340 invert: - Invert ["Invert"],
341 ho: - HoldOff ["HoldOff"],
342 tc: - Traceable ["Traceable"],
343}
344
345impl Default for GameMods {
346 fn default() -> Self {
347 Self::DEFAULT
348 }
349}
350
351impl From<GameModsLazer> for GameMods {
352 fn from(mods: GameModsLazer) -> Self {
353 Self::Lazer(mods)
354 }
355}
356
357impl From<GameModsIntermode> for GameMods {
358 fn from(mods: GameModsIntermode) -> Self {
359 Self::Intermode(mods)
360 }
361}
362
363impl From<&GameModsIntermode> for GameMods {
364 fn from(mods: &GameModsIntermode) -> Self {
365 match mods.checked_bits() {
368 Some(bits) => bits.into(),
369 None => mods.to_owned().into(),
370 }
371 }
372}
373
374impl From<GameModsLegacy> for GameMods {
375 fn from(mods: GameModsLegacy) -> Self {
376 Self::Legacy(mods)
377 }
378}
379
380impl From<u32> for GameMods {
381 fn from(bits: u32) -> Self {
382 GameModsLegacy::from_bits(bits).into()
383 }
384}
385
386#[derive(Copy, Clone, Debug, PartialEq, Eq)]
387pub(crate) enum Reflection {
388 None,
389 Vertical,
390 Horizontal,
391 Both,
392}