1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
#![cfg_attr(docsrs, feature(doc_cfg), deny(broken_intra_doc_links))]

//! A standalone crate to calculate star ratings and performance points for all [osu!](https://osu.ppy.sh/home) gamemodes.
//!
//! Conversions between gamemodes are generally not supported.
//!
//! Async is supported through features, see below.
//!
//! ## Usage
//!
//! ```no_run
//! use std::fs::File;
//! use peace_performance::{Beatmap, BeatmapExt};
//!
//! # /*
//! let file = match File::open("/path/to/file.osu") {
//!     Ok(file) => file,
//!     Err(why) => panic!("Could not open file: {}", why),
//! };
//!
//! // Parse the map yourself
//! let map = match Beatmap::parse(file) {
//!     Ok(map) => map,
//!     Err(why) => panic!("Error while parsing map: {}", why),
//! };
//! # */ let map = Beatmap::default();
//!
//! // If `BeatmapExt` is included, you can make use of
//! // some methods on `Beatmap` to make your life simpler.
//! // If the mode is known, it is recommended to use the
//! // mode's pp calculator, e.g. `TaikoPP`, manually.
//! let result = map.pp()
//!     .mods(24) // HDHR
//!     .combo(1234)
//!     .misses(2)
//!     .accuracy(99.2)
//!     .calculate();
//!
//! println!("PP: {}", result.pp());
//!
//! // If you intend to reuse the current map-mod combination,
//! // make use of the previous result!
//! // If attributes are given, then stars & co don't have to be recalculated.
//! let next_result = map.pp()
//!     .mods(24) // HDHR
//!     .attributes(result) // recycle
//!     .combo(543)
//!     .misses(5)
//!     .n50(3)
//!     .passed_objects(600)
//!     .accuracy(96.5)
//!     .calculate();
//!
//! println!("Next PP: {}", next_result.pp());
//!
//! let stars = map.stars(16, None).stars(); // HR
//! let max_pp = map.max_pp(16).pp();
//!
//! println!("Stars: {} | Max PP: {}", stars, max_pp);
//! ```
//!
//! ## With async
//! If either the `async_tokio` or `async_std` feature is enabled, beatmap parsing will be async.
//!
//! ```no_run
//! use peace_performance::{Beatmap, BeatmapExt};
//! # /*
//! use async_std::fs::File;
//! # */
//! // use tokio::fs::File;
//!
//! # /*
//! let file = match File::open("/path/to/file.osu").await {
//!     Ok(file) => file,
//!     Err(why) => panic!("Could not open file: {}", why),
//! };
//!
//! // Parse the map asynchronously
//! let map = match Beatmap::parse(file).await {
//!     Ok(map) => map,
//!     Err(why) => panic!("Error while parsing map: {}", why),
//! };
//! # */ let map = Beatmap::default();
//!
//! // The rest stays the same
//! let result = map.pp()
//!     .mods(24) // HDHR
//!     .combo(1234)
//!     .misses(2)
//!     .accuracy(99.2)
//!     .calculate();
//!
//! println!("PP: {}", result.pp());
//! ```
//!
//! ## osu!standard versions
//!
//! - `all_included`: Both stack leniency & slider paths are considered so that the difficulty and pp calculation immitates osu! as close as possible. Pro: Most precise; Con: Least performant.
//! - `no_leniency`: The positional offset of notes created by stack leniency is not considered. This means the jump distance inbetween notes might be slightly off, resulting in small inaccuracies. Since calculating these offsets is relatively expensive though, this version is considerably faster than `all_included`.
//! - `no_slider_no_leniency` (i.e. [oppai](https://github.com/Francesco149/oppai-ng)): In addition to not considering the positional offset caused by stack leniency, slider paths are also ignored. This means the travel distance of notes is completely omitted which may cause further inaccuracies. Since the slider paths don't have to be computed though, it is generally faster than `no_leniency`.
//!
//! **Note**: If the `fruits` feature is enabled, sliders will be parsed regardless, resulting in a reduced performance advantage of `no_sliders_no_leniency`.
//!
//! ## Features
//!
//! | Flag | Description |
//! |-----|-----|
//! | `default` | Enable all modes and choose the `no_leniency` version for osu!standard. |
//! | `taiko` | Enable osu!taiko. |
//! | `fruits` | Enable osu!ctb. |
//! | `mania` | Enable osu!mania. |
//! | `osu` | Enable osu!standard. Requires to also enable exactly one of the features `no_leniency`, `no_sliders_no_leniency`, or `all_included`. |
//! | `no_leniency` | When calculating difficulty attributes in osu!standard, ignore stack leniency but consider sliders. Solid middleground between performance and precision, hence the default version. |
//! | `no_sliders_no_leniency` | When calculating difficulty attributes in osu!standard, ignore stack leniency and sliders. Best performance but slightly less precision than `no_leniency`. |
//! | `all_included` | When calculating difficulty attributes in osu!standard, consider both stack leniency and sliders. Best precision but significantly worse performance than `no_leniency`. |
//! | `async_tokio` | Beatmap parsing will be async through [tokio](https://github.com/tokio-rs/tokio) |
//! | `async_std` | Beatmap parsing will be async through [async-std](https://github.com/async-rs/async-std) |
//!
//! ## Roadmap
//!
//! - \[x\] osu sr versions
//!   - \[x\] all included
//!   - \[x\] no_leniency
//!   - \[x\] no_sliders_no_leniency
//! - \[x\] taiko sr
//! - \[x\] ctb sr
//! - \[x\] mania sr
//! ---
//! - \[x\] osu pp
//! - \[x\] taiko pp
//! - \[x\] ctb pp
//! - \[x\] mania pp
//! ---
//! - \[x\] refactoring
//! - \[x\] benchmarking
//! - \[x\] async parsing

#[cfg(feature = "fruits")]
#[cfg_attr(docsrs, doc(cfg(feature = "fruits")))]
pub mod fruits;

#[cfg(feature = "mania")]
#[cfg_attr(docsrs, doc(cfg(feature = "mania")))]
pub mod mania;

#[cfg(feature = "osu")]
#[cfg_attr(docsrs, doc(cfg(feature = "osu")))]
pub mod osu;

#[cfg(feature = "taiko")]
#[cfg_attr(docsrs, doc(cfg(feature = "taiko")))]
pub mod taiko;

pub mod parse;

mod pp;
pub use pp::{AnyPP, AttributeProvider};

mod curve;
mod math_util;
mod mods;

#[cfg(any(feature = "osu", feature = "fruits"))]
pub(crate) mod control_point_iter;

#[cfg(any(feature = "osu", feature = "fruits"))]
pub(crate) use control_point_iter::{ControlPoint, ControlPointIter};

#[cfg(feature = "fruits")]
pub use fruits::FruitsPP;

#[cfg(feature = "mania")]
pub use mania::ManiaPP;

#[cfg(feature = "osu")]
pub use osu::OsuPP;

#[cfg(feature = "taiko")]
pub use taiko::TaikoPP;

pub use mods::Mods;
pub use parse::{Beatmap, BeatmapAttributes, GameMode, ParseError, ParseResult};

pub trait BeatmapExt {
    /// Calculate the stars and other attributes of a beatmap which are required for pp calculation.
    fn stars(&self, mods: impl Mods, passed_objects: Option<usize>) -> StarResult;

    /// Calculate the max pp of a beatmap.
    ///
    /// If you seek more fine-tuning and options you need to match on the map's
    /// mode and use the mode's corresponding calculator, e.g. [`TaikoPP`](crate::TaikoPP) for taiko.
    fn max_pp(&self, mods: u32) -> PpResult;

    /// Returns a builder to calculate pp and difficulty values.
    ///
    /// Convenient method that matches on the map's mode to choose the appropriate calculator.
    fn pp(&self) -> AnyPP;

    /// Calculate the strains of a map.
    /// This essentially performs the same calculation as a `stars` function but
    /// instead of evaluating the final strains, they are just returned as is.
    ///
    /// Suitable to plot the difficulty of a map over time.
    fn strains(&self, mods: impl Mods) -> Strains;
}

impl BeatmapExt for Beatmap {
    fn stars(&self, mods: impl Mods, passed_objects: Option<usize>) -> StarResult {
        match self.mode {
            GameMode::STD => {
                #[cfg(not(feature = "osu"))]
                panic!("`osu` feature is not enabled");

                #[cfg(feature = "osu")]
                {
                    #[cfg(feature = "no_leniency")]
                    {
                        osu::no_leniency::stars(self, mods, passed_objects)
                    }

                    #[cfg(all(not(feature = "no_leniency"), feature = "no_sliders_no_leniency"))]
                    {
                        osu::no_sliders_no_leniency::stars(self, mods, passed_objects)
                    }

                    #[cfg(all(
                        not(feature = "no_leniency"),
                        not(feature = "no_sliders_no_leniency"),
                        feature = "all_included"
                    ))]
                    {
                        osu::all_included::stars(self, mods, passed_objects)
                    }

                    #[cfg(not(any(
                        feature = "no_leniency",
                        feature = "no_sliders_no_leniency",
                        feature = "all_included"
                    )))]
                    panic!("either of the features `no_leniency`, `no_sliders_no_leniency`, or `all_included` must be enabled");
                }
            }
            GameMode::MNA => {
                #[cfg(not(feature = "mania"))]
                panic!("`mania` feature is not enabled");

                #[cfg(feature = "mania")]
                mania::stars(self, mods, passed_objects)
            }
            GameMode::TKO => {
                #[cfg(not(feature = "taiko"))]
                panic!("`osu` feature is not enabled");

                #[cfg(feature = "taiko")]
                taiko::stars(self, mods, passed_objects)
            }
            GameMode::CTB => {
                #[cfg(not(feature = "fruits"))]
                panic!("`fruits` feature is not enabled");

                #[cfg(feature = "fruits")]
                fruits::stars(self, mods, passed_objects)
            }
        }
    }

    fn max_pp(&self, mods: u32) -> PpResult {
        match self.mode {
            GameMode::STD => {
                #[cfg(not(feature = "osu"))]
                panic!("`osu` feature is not enabled");

                #[cfg(feature = "osu")]
                OsuPP::new(self).mods(mods).calculate()
            }
            GameMode::MNA => {
                #[cfg(not(feature = "mania"))]
                panic!("`mania` feature is not enabled");

                #[cfg(feature = "mania")]
                ManiaPP::new(self).mods(mods).calculate()
            }
            GameMode::TKO => {
                #[cfg(not(feature = "taiko"))]
                panic!("`osu` feature is not enabled");

                #[cfg(feature = "taiko")]
                TaikoPP::new(self).mods(mods).calculate()
            }
            GameMode::CTB => {
                #[cfg(not(feature = "fruits"))]
                panic!("`fruits` feature is not enabled");

                #[cfg(feature = "fruits")]
                FruitsPP::new(self).mods(mods).calculate()
            }
        }
    }

    #[inline]
    fn pp(&self) -> AnyPP {
        AnyPP::new(self)
    }

    fn strains(&self, mods: impl Mods) -> Strains {
        match self.mode {
            GameMode::STD => {
                #[cfg(not(feature = "osu"))]
                panic!("`osu` feature is not enabled");

                #[cfg(feature = "osu")]
                {
                    #[cfg(feature = "no_leniency")]
                    {
                        osu::no_leniency::strains(self, mods)
                    }

                    #[cfg(all(not(feature = "no_leniency"), feature = "no_sliders_no_leniency"))]
                    {
                        osu::no_sliders_no_leniency::strains(self, mods)
                    }

                    #[cfg(all(
                        not(feature = "no_leniency"),
                        not(feature = "no_sliders_no_leniency"),
                        feature = "all_included"
                    ))]
                    {
                        osu::all_included::strains(self, mods)
                    }

                    #[cfg(not(any(
                        feature = "no_leniency",
                        feature = "no_sliders_no_leniency",
                        feature = "all_included"
                    )))]
                    panic!("either of the features `no_leniency`, `no_sliders_no_leniency`, or `all_included` must be enabled");
                }
            }
            GameMode::MNA => {
                #[cfg(not(feature = "mania"))]
                panic!("`mania` feature is not enabled");

                #[cfg(feature = "mania")]
                mania::strains(self, mods)
            }
            GameMode::TKO => {
                #[cfg(not(feature = "taiko"))]
                panic!("`osu` feature is not enabled");

                #[cfg(feature = "taiko")]
                taiko::strains(self, mods)
            }
            GameMode::CTB => {
                #[cfg(not(feature = "fruits"))]
                panic!("`fruits` feature is not enabled");

                #[cfg(feature = "fruits")]
                fruits::strains(self, mods)
            }
        }
    }
}

/// The result of calculating the strains on a map.
/// Suitable to plot the difficulty of a map over time.
///
/// `strains` will be the summed strains for each skill of the map's mode.
///
/// `section_length` is the time in ms inbetween two strains.
#[derive(Clone, Debug, Default)]
pub struct Strains {
    pub section_length: f32,
    pub strains: Vec<f32>,
}

/// Basic enum containing the result of a star calculation based on the mode.
#[derive(Clone, Debug)]
pub enum StarResult {
    #[cfg(feature = "fruits")]
    Fruits(fruits::DifficultyAttributes),
    #[cfg(feature = "mania")]
    Mania(mania::DifficultyAttributes),
    #[cfg(feature = "osu")]
    Osu(osu::DifficultyAttributes),
    #[cfg(feature = "taiko")]
    Taiko(taiko::DifficultyAttributes),
}

impl StarResult {
    /// The final star value.
    #[inline]
    pub fn stars(&self) -> f32 {
        match self {
            #[cfg(feature = "fruits")]
            Self::Fruits(attributes) => attributes.stars,
            #[cfg(feature = "mania")]
            Self::Mania(attributes) => attributes.stars,
            #[cfg(feature = "osu")]
            Self::Osu(attributes) => attributes.stars,
            #[cfg(feature = "taiko")]
            Self::Taiko(attributes) => attributes.stars,
        }
    }
}

#[derive(Clone, Debug)]
pub struct PpRaw {
    pub aim: Option<f32>,
    pub spd: Option<f32>,
    pub str: Option<f32>,
    pub acc: Option<f32>,
    pub total: f32,
}

impl PpRaw {
    #[inline(always)]
    pub fn new(
        aim: Option<f32>,
        spd: Option<f32>,
        str: Option<f32>,
        acc: Option<f32>,
        total: f32,
    ) -> Self {
        Self {
            aim,
            spd,
            str,
            acc,
            total,
        }
    }
}

/// Basic struct containing the result of a PP calculation.
#[derive(Clone, Debug)]
pub struct PpResult {
    pub mode: u8,
    pub mods: u32,
    pub pp: f32,
    pub raw: PpRaw,
    pub attributes: StarResult,
}

impl PpResult {
    /// The final pp value.
    #[inline]
    pub fn pp(&self) -> f32 {
        self.pp
    }

    /// The final star value.
    #[inline]
    pub fn stars(&self) -> f32 {
        self.attributes.stars()
    }
}

#[cfg(any(feature = "osu", feature = "taiko"))]
#[inline]
fn difficulty_range(val: f32, max: f32, avg: f32, min: f32) -> f32 {
    if val > 5.0 {
        avg + (max - avg) * (val - 5.0) / 5.0
    } else if val < 5.0 {
        avg - (avg - min) * (5.0 - val) / 5.0
    } else {
        avg
    }
}

#[cfg(not(any(
    feature = "osu",
    feature = "taiko",
    feature = "fruits",
    feature = "mania"
)))]
compile_error!("At least one of the features `osu`, `taiko`, `fruits`, `mania` must be enabled");

#[cfg(all(
    feature = "osu",
    not(any(
        feature = "all_included",
        feature = "no_leniency",
        feature = "no_sliders_no_leniency"
    ))
))]
compile_error!("Since the `osu` feature is enabled, either `no_leniency`, `no_sliders_no_leniency`, or `all_included` must be enabled aswell");

#[cfg(any(
    all(feature = "no_leniency", feature = "no_sliders_no_leniency"),
    all(feature = "no_leniency", feature = "all_included"),
    all(feature = "all_included", feature = "no_sliders_no_leniency"),
))]
compile_error!("Only one of the features `no_leniency`, `no_sliders_no_leniency`, `all_included` can be enabled");

#[cfg(all(
    not(feature = "osu"),
    any(
        feature = "no_leniency",
        feature = "no_sliders_no_leniency",
        feature = "all_included"
    )
))]
compile_error!("The features `no_leniency`, `no_sliders_no_leniency`, and `all_included` should only be enabled in combination with the `osu` feature");

#[cfg(all(feature = "async_tokio", feature = "async_std"))]
compile_error!("Only one of the features `async_tokio` and `async_std` should be enabled");