feature-gated modes and osu!standard versions
This commit is contained in:
+12
@@ -3,3 +3,15 @@ name = "rosu-pp"
|
||||
version = "0.1.0"
|
||||
authors = ["MaxOhn <ohn.m@hotmail.de>"]
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
default = ["osu", "taiko", "fruits", "mania", "no_leniency"]
|
||||
|
||||
osu = []
|
||||
taiko = []
|
||||
fruits = []
|
||||
mania = []
|
||||
|
||||
all_included = []
|
||||
no_leniency = []
|
||||
no_sliders_no_leniency = []
|
||||
@@ -4,7 +4,7 @@ A standalone crate to calculate star ratings and performance points for all [osu
|
||||
|
||||
Conversions are generally not supported.
|
||||
|
||||
#### Usage
|
||||
### Usage
|
||||
```rust
|
||||
use std::fs::File;
|
||||
use rosu_pp::{Beatmap, BeatmapExt, GameMode, OsuPP, TaikoPP};
|
||||
@@ -31,8 +31,7 @@ match map.mode {
|
||||
.combo(1234)
|
||||
.misses(2)
|
||||
.accuracy(99.2)
|
||||
// `no_leniency::stars` is the suggested default
|
||||
.calculate(rosu_pp::osu::no_leniency::stars);
|
||||
.calculate();
|
||||
|
||||
println!("PP: {}", result.pp());
|
||||
|
||||
@@ -46,7 +45,7 @@ match map.mode {
|
||||
.misses(5)
|
||||
.n50(3)
|
||||
.accuracy(97.5)
|
||||
.calculate(rosu_pp::osu::no_leniency::stars);
|
||||
.calculate();
|
||||
|
||||
println!("Next PP: {}", next_result.pp());
|
||||
},
|
||||
@@ -66,22 +65,35 @@ match map.mode {
|
||||
|
||||
// If all you want is the map's stars or max pp,
|
||||
// you can make use of the BeatmapExt trait.
|
||||
let stars = map.stars(16, None); // HR
|
||||
let max_pp = map.max_pp(16);
|
||||
let stars = map.stars(16, None).stars(); // HR
|
||||
let max_pp = map.max_pp(16).pp();
|
||||
|
||||
println!("Stars: {} | Max PP: {}", stars, max_pp);
|
||||
```
|
||||
|
||||
#### osu!standard versions
|
||||
### osu!standard versions
|
||||
- `all_included`: WIP
|
||||
- `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): In addtion 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 should generally be faster than `no_leniency`.
|
||||
- `no_slider_no_leniency` (i.e. [oppai](https://github.com/Francesco149/oppai-ng)): In addtion 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 should generally be faster than `no_leniency`.
|
||||
|
||||
#### Roadmap
|
||||
### 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, suggested 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`. |
|
||||
|
||||
### Roadmap
|
||||
- osu sr versions
|
||||
- [ ] all included
|
||||
- [x] no_leniency
|
||||
- [x] no_sliders_no_leniency (i.e. oppai)
|
||||
- [x] no_sliders_no_leniency (i.e. [oppai](https://github.com/Francesco149/oppai-ng))
|
||||
- [x] taiko sr
|
||||
- [x] ctb sr
|
||||
- [x] mania sr
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
#![cfg(any(
|
||||
feature = "fruits",
|
||||
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||
))]
|
||||
|
||||
use crate::{math_util, Pos2};
|
||||
|
||||
const SLIDER_QUALITY: f32 = 50.0;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "fruits")]
|
||||
|
||||
mod catch_object;
|
||||
mod control_point_iter;
|
||||
mod difficulty_object;
|
||||
|
||||
@@ -13,6 +13,7 @@ impl FruitsAttributeProvider for DifficultyAttributes {
|
||||
|
||||
impl FruitsAttributeProvider for StarResult {
|
||||
fn attributes(self) -> Option<DifficultyAttributes> {
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
if let Self::Fruits(attributes) = self {
|
||||
Some(attributes)
|
||||
} else {
|
||||
|
||||
+207
-19
@@ -29,8 +29,7 @@
|
||||
//! .combo(1234)
|
||||
//! .misses(2)
|
||||
//! .accuracy(99.2)
|
||||
//! // `no_leniency::stars` is the suggested default
|
||||
//! .calculate(rosu_pp::osu::no_leniency::stars);
|
||||
//! .calculate();
|
||||
//!
|
||||
//! println!("PP: {}", result.pp());
|
||||
//!
|
||||
@@ -44,7 +43,7 @@
|
||||
//! .misses(5)
|
||||
//! .n50(3)
|
||||
//! .accuracy(97.5)
|
||||
//! .calculate(rosu_pp::osu::no_leniency::stars);
|
||||
//! .calculate();
|
||||
//!
|
||||
//! println!("Next PP: {}", next_result.pp());
|
||||
//! },
|
||||
@@ -73,13 +72,26 @@
|
||||
//! ### osu!standard versions
|
||||
//! - `all_included`: WIP
|
||||
//! - `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): In addtion 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 should generally be faster than `no_leniency`.
|
||||
//! - `no_slider_no_leniency` (i.e. [oppai](https://github.com/Francesco149/oppai-ng)): In addtion 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 should generally be faster than `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, suggested 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`. |
|
||||
//!
|
||||
//! ### Roadmap
|
||||
//! - osu sr versions
|
||||
//! - [ ] all included
|
||||
//! - [x] no_leniency
|
||||
//! - [x] no_sliders_no_leniency (i.e. oppai)
|
||||
//! - [x] no_sliders_no_leniency (i.e. [oppai](https://github.com/Francesco149/oppai-ng))
|
||||
//! - [x] taiko sr
|
||||
//! - [x] ctb sr
|
||||
//! - [x] mania sr
|
||||
@@ -102,9 +114,16 @@ mod curve;
|
||||
mod math_util;
|
||||
mod mods;
|
||||
|
||||
#[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;
|
||||
@@ -140,28 +159,150 @@ pub trait BeatmapExt {
|
||||
impl BeatmapExt for Beatmap {
|
||||
fn stars(&self, mods: impl Mods, passed_objects: Option<usize>) -> StarResult {
|
||||
match self.mode {
|
||||
GameMode::STD => osu::no_leniency::stars(self, mods, passed_objects),
|
||||
GameMode::MNA => mania::stars(self, mods, passed_objects),
|
||||
GameMode::TKO => taiko::stars(self, mods, passed_objects),
|
||||
GameMode::CTB => fruits::stars(self, mods, passed_objects),
|
||||
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 => OsuPP::new(self)
|
||||
.mods(mods)
|
||||
.calculate(osu::no_leniency::stars),
|
||||
GameMode::MNA => ManiaPP::new(self).mods(mods).calculate(),
|
||||
GameMode::TKO => TaikoPP::new(self).mods(mods).calculate(),
|
||||
GameMode::CTB => FruitsPP::new(self).mods(mods).calculate(),
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
fn strains(&self, mods: impl Mods) -> Strains {
|
||||
match self.mode {
|
||||
GameMode::STD => osu::no_leniency::strains(self, mods),
|
||||
GameMode::MNA => mania::strains(self, mods),
|
||||
GameMode::TKO => taiko::strains(self, mods),
|
||||
GameMode::CTB => fruits::strains(self, mods),
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,9 +322,13 @@ pub struct Strains {
|
||||
/// 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),
|
||||
}
|
||||
|
||||
@@ -192,9 +337,13 @@ impl StarResult {
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
@@ -221,6 +370,7 @@ impl PpResult {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "osu", feature = "taiko"))]
|
||||
#[inline]
|
||||
fn difficulty_range(val: f32, max: f32, avg: f32, min: f32) -> f32 {
|
||||
if val > 5.0 {
|
||||
@@ -231,3 +381,41 @@ fn difficulty_range(val: f32, max: f32, avg: f32, min: f32) -> f32 {
|
||||
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(feature = "all_included")]
|
||||
compile_error!("The `all_included` version is still WIP and unavailable, use either the `no_leniency` or `no_sliders_no_leniency` feature");
|
||||
|
||||
+4
-2
@@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "mania")]
|
||||
|
||||
mod pp;
|
||||
mod strain;
|
||||
|
||||
@@ -151,7 +153,7 @@ mod tests {
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn mania_single() {
|
||||
let file = match File::open("./maps/1529790.osu") {
|
||||
let file = match File::open("./maps/1355822.osu") {
|
||||
Ok(file) => file,
|
||||
Err(why) => panic!("Could not open file: {}", why),
|
||||
};
|
||||
@@ -161,7 +163,7 @@ mod tests {
|
||||
Err(why) => panic!("Error while parsing map: {}", why),
|
||||
};
|
||||
|
||||
let result = ManiaPP::new(&map).mods(0).calculate();
|
||||
let result = ManiaPP::new(&map).mods(256).calculate();
|
||||
|
||||
println!("Stars: {}", result.stars());
|
||||
println!("PP: {}", result.pp());
|
||||
|
||||
@@ -19,6 +19,7 @@ impl ManiaStarProvider for DifficultyAttributes {
|
||||
|
||||
impl ManiaStarProvider for StarResult {
|
||||
fn attributes(self) -> Option<f32> {
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
if let Self::Mania(attributes) = self {
|
||||
Some(attributes.stars)
|
||||
} else {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
#![cfg(any(
|
||||
feature = "fruits",
|
||||
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||
))]
|
||||
|
||||
use crate::Pos2;
|
||||
|
||||
#[inline]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "osu")]
|
||||
|
||||
mod pp;
|
||||
mod versions;
|
||||
|
||||
|
||||
+25
-4
@@ -13,6 +13,7 @@ impl OsuAttributeProvider for DifficultyAttributes {
|
||||
|
||||
impl OsuAttributeProvider for StarResult {
|
||||
fn attributes(self) -> Option<DifficultyAttributes> {
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
if let Self::Osu(attributes) = self {
|
||||
Some(attributes)
|
||||
} else {
|
||||
@@ -144,6 +145,7 @@ impl<'m> OsuPP<'m> {
|
||||
let acc = acc / 100.0;
|
||||
|
||||
if self.n100.or(self.n50).is_some() {
|
||||
// TODO: Improve this branch
|
||||
self.n300.replace(
|
||||
n_objects - self.n100.unwrap_or(0) - self.n50.unwrap_or(0) - self.n_misses,
|
||||
);
|
||||
@@ -168,13 +170,32 @@ impl<'m> OsuPP<'m> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns an object which contains the pp and [`DifficultyAttributes`](crate::osu::DifficultyAttributes)
|
||||
/// containing stars and other attributes.
|
||||
#[cfg(feature = "no_leniency")]
|
||||
pub fn calculate(self) -> PpResult {
|
||||
self.calculate_with_func(super::no_leniency::stars)
|
||||
}
|
||||
|
||||
/// Returns an object which contains the pp and [`DifficultyAttributes`](crate::osu::DifficultyAttributes)
|
||||
/// containing stars and other attributes.
|
||||
#[cfg(feature = "no_sliders_no_leniency")]
|
||||
pub fn calculate(self) -> PpResult {
|
||||
self.calculate_with_func(super::no_sliders_no_leniency::stars)
|
||||
}
|
||||
|
||||
/// Returns an object which contains the pp and [`DifficultyAttributes`](crate::osu::DifficultyAttributes)
|
||||
/// containing stars and other attributes.
|
||||
#[cfg(feature = "all_included")]
|
||||
pub fn calculate(self) -> PpResult {
|
||||
self.calculate_with_func(super::all_included::stars)
|
||||
}
|
||||
|
||||
/// Returns an object which contains the pp and [`DifficultyAttributes`](crate::osu::DifficultyAttributes)
|
||||
/// containing stars and other attributes.
|
||||
///
|
||||
/// `stars_func` will be used to calculate the difficulty attributes if not already given.
|
||||
/// Only called if `attributes` was not set.
|
||||
/// The default is suggested to be [`no_leniency::stars`](crate::osu::no_leniency::stars).
|
||||
pub fn calculate(
|
||||
/// `stars_func` will be used to calculate the difficulty attributes if they are not yet given.
|
||||
fn calculate_with_func(
|
||||
mut self,
|
||||
stars_func: impl FnOnce(&Beatmap, u32, Option<usize>) -> StarResult,
|
||||
) -> PpResult {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![cfg(feature = "all_included")]
|
||||
#![allow(unused)]
|
||||
|
||||
mod difficulty_object;
|
||||
@@ -11,7 +12,7 @@ use skill::Skill;
|
||||
use skill_kind::SkillKind;
|
||||
|
||||
use super::super::DifficultyAttributes;
|
||||
use crate::{Beatmap, Mods, StarResult};
|
||||
use crate::{Beatmap, Mods, StarResult, Strains};
|
||||
|
||||
const SECTION_LEN: f32 = 400.0;
|
||||
const DIFFICULTY_MULTIPLIER: f32 = 0.0675;
|
||||
@@ -128,7 +129,13 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
||||
StarResult::Osu(attributes)
|
||||
}
|
||||
|
||||
// TODO: strains function
|
||||
/// Essentially the same as the `stars` function but instead of
|
||||
/// evaluating the final strains, it just returns them as is.
|
||||
///
|
||||
/// Suitable to plot the difficulty of a map over time.
|
||||
pub fn strains(_map: &Beatmap, _mods: impl Mods) -> Strains {
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -150,7 +157,7 @@ mod tests {
|
||||
Err(why) => panic!("Error while parsing map: {}", why),
|
||||
};
|
||||
|
||||
let result = OsuPP::new(&map).mods(64).calculate(stars);
|
||||
let result = OsuPP::new(&map).mods(64).calculate();
|
||||
|
||||
println!("Stars: {}", result.stars());
|
||||
println!("PP: {}", result.pp());
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
mod all_included;
|
||||
#[cfg(feature = "all_included")]
|
||||
pub mod all_included;
|
||||
|
||||
#[cfg(feature = "no_leniency")]
|
||||
pub mod no_leniency;
|
||||
|
||||
#[cfg(feature = "no_sliders_no_leniency")]
|
||||
pub mod no_sliders_no_leniency;
|
||||
|
||||
const OSU_OD_MAX: f32 = 20.0;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
//! 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`.
|
||||
|
||||
#![cfg(feature = "no_leniency")]
|
||||
|
||||
use super::super::DifficultyAttributes;
|
||||
|
||||
mod control_point_iter;
|
||||
@@ -271,7 +273,6 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::super::OsuPP;
|
||||
use super::stars;
|
||||
use crate::Beatmap;
|
||||
use std::fs::File;
|
||||
|
||||
@@ -288,7 +289,7 @@ mod tests {
|
||||
Err(why) => panic!("Error while parsing map: {}", why),
|
||||
};
|
||||
|
||||
let result = OsuPP::new(&map).mods(0).calculate(stars);
|
||||
let result = OsuPP::new(&map).mods(0).calculate();
|
||||
|
||||
println!("Stars: {}", result.stars());
|
||||
println!("PP: {}", result.pp());
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
//! 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 should generally be faster than `no_leniency`.
|
||||
|
||||
#![cfg(feature = "no_sliders_no_leniency")]
|
||||
|
||||
use super::super::DifficultyAttributes;
|
||||
|
||||
mod control_point_iter;
|
||||
@@ -285,7 +287,6 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::super::OsuPP;
|
||||
use super::stars;
|
||||
use crate::Beatmap;
|
||||
use std::fs::File;
|
||||
|
||||
@@ -302,7 +303,7 @@ mod tests {
|
||||
Err(why) => panic!("Error while parsing map: {}", why),
|
||||
};
|
||||
|
||||
let result = OsuPP::new(&map).mods(2).calculate(stars);
|
||||
let result = OsuPP::new(&map).mods(2).calculate();
|
||||
|
||||
println!("Stars: {}", result.stars());
|
||||
println!("PP: {}", result.pp());
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
use super::OSU_FILE_HEADER;
|
||||
|
||||
#[cfg(not(all(
|
||||
feature = "osu",
|
||||
feature = "taiko",
|
||||
feature = "fruits",
|
||||
feature = "mania"
|
||||
)))]
|
||||
use super::GameMode;
|
||||
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt;
|
||||
use std::io::Error as IOError;
|
||||
@@ -20,6 +28,14 @@ pub enum ParseError {
|
||||
InvalidTimingSignature,
|
||||
MissingField(&'static str),
|
||||
UnknownHitObjectKind,
|
||||
|
||||
#[cfg(not(all(
|
||||
feature = "osu",
|
||||
feature = "taiko",
|
||||
feature = "fruits",
|
||||
feature = "mania"
|
||||
)))]
|
||||
UnincludedMode(GameMode),
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseError {
|
||||
@@ -38,6 +54,18 @@ impl fmt::Display for ParseError {
|
||||
Self::InvalidTimingSignature => f.write_str("invalid timing signature"),
|
||||
Self::MissingField(field) => write!(f, "missing field `{}`", field),
|
||||
Self::UnknownHitObjectKind => f.write_str("unsupported hitobject kind"),
|
||||
|
||||
#[cfg(not(all(
|
||||
feature = "osu",
|
||||
feature = "taiko",
|
||||
feature = "fruits",
|
||||
feature = "mania"
|
||||
)))]
|
||||
Self::UnincludedMode(mode) => write!(
|
||||
f,
|
||||
"cannot process {:?} map its mode's feature has not been included",
|
||||
mode
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,6 +84,14 @@ impl StdError for ParseError {
|
||||
Self::InvalidTimingSignature => None,
|
||||
Self::MissingField(_) => None,
|
||||
Self::UnknownHitObjectKind => None,
|
||||
|
||||
#[cfg(not(all(
|
||||
feature = "osu",
|
||||
feature = "taiko",
|
||||
feature = "fruits",
|
||||
feature = "mania"
|
||||
)))]
|
||||
Self::UnincludedMode(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
-1
@@ -1,4 +1,10 @@
|
||||
use super::{PathType, Pos2};
|
||||
use super::Pos2;
|
||||
|
||||
#[cfg(any(
|
||||
feature = "fruits",
|
||||
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||
))]
|
||||
use super::PathType;
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
@@ -50,12 +56,24 @@ impl PartialOrd for HitObject {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum HitObjectKind {
|
||||
Circle,
|
||||
#[cfg(any(
|
||||
feature = "fruits",
|
||||
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||
))]
|
||||
Slider {
|
||||
pixel_len: f32,
|
||||
repeats: usize,
|
||||
curve_points: Vec<Pos2>,
|
||||
path_type: PathType,
|
||||
},
|
||||
#[cfg(not(any(
|
||||
feature = "fruits",
|
||||
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||
)))]
|
||||
Slider {
|
||||
pixel_len: f32,
|
||||
repeats: usize,
|
||||
},
|
||||
Spinner {
|
||||
end_time: f32,
|
||||
},
|
||||
|
||||
+136
-40
@@ -98,11 +98,16 @@ pub struct Beatmap {
|
||||
pub hp: f32,
|
||||
pub sv: f32,
|
||||
pub tick_rate: f32,
|
||||
pub stack_leniency: f32,
|
||||
|
||||
pub hit_objects: Vec<HitObject>,
|
||||
|
||||
#[cfg(any(feature = "osu", feature = "fruits"))]
|
||||
pub timing_points: Vec<TimingPoint>,
|
||||
|
||||
#[cfg(any(feature = "osu", feature = "fruits"))]
|
||||
pub difficulty_points: Vec<DifficultyPoint>,
|
||||
|
||||
#[cfg(all(feature = "osu", feature = "all_included"))]
|
||||
pub stack_leniency: f32,
|
||||
}
|
||||
|
||||
pub(crate) const OSU_FILE_HEADER: &str = "osu file format v";
|
||||
@@ -110,11 +115,9 @@ pub(crate) const OSU_FILE_HEADER: &str = "osu file format v";
|
||||
impl Beatmap {
|
||||
const CIRCLE_FLAG: u8 = 1 << 0;
|
||||
const SLIDER_FLAG: u8 = 1 << 1;
|
||||
#[allow(unused)]
|
||||
const NEW_COMBO_FLAG: u8 = 1 << 2;
|
||||
// const NEW_COMBO_FLAG: u8 = 1 << 2;
|
||||
const SPINNER_FLAG: u8 = 1 << 3;
|
||||
#[allow(unused)]
|
||||
const COMBO_OFFSET_FLAG: u8 = (1 << 4) | (1 << 5) | (1 << 6);
|
||||
// const COMBO_OFFSET_FLAG: u8 = (1 << 4) | (1 << 5) | (1 << 6);
|
||||
const HOLD_FLAG: u8 = 1 << 7;
|
||||
|
||||
pub fn parse<R: Read>(input: R) -> ParseResult<Self> {
|
||||
@@ -178,9 +181,11 @@ impl Beatmap {
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
let mut mode = None;
|
||||
let mut stack_leniency = None;
|
||||
let mut empty = true;
|
||||
|
||||
#[cfg(all(feature = "osu", feature = "all_included"))]
|
||||
let mut stack_leniency = None;
|
||||
|
||||
while reader.read_line(buf)? != 0 {
|
||||
let line = line_prepare!(buf);
|
||||
|
||||
@@ -201,7 +206,10 @@ impl Beatmap {
|
||||
"3" => Some(GameMode::MNA),
|
||||
_ => return Err(ParseError::InvalidMode),
|
||||
};
|
||||
} else if key == "StackLeniency" {
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "osu", feature = "all_included"))]
|
||||
if key == "StackLeniency" {
|
||||
stack_leniency = Some(value.parse()?);
|
||||
}
|
||||
|
||||
@@ -209,7 +217,31 @@ impl Beatmap {
|
||||
}
|
||||
|
||||
self.mode = mode.unwrap_or(GameMode::STD);
|
||||
self.stack_leniency = stack_leniency.unwrap_or(0.7);
|
||||
|
||||
#[cfg(not(feature = "osu"))]
|
||||
if self.mode == GameMode::STD {
|
||||
return Err(ParseError::UnincludedMode(GameMode::STD));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "taiko"))]
|
||||
if self.mode == GameMode::TKO {
|
||||
return Err(ParseError::UnincludedMode(GameMode::TKO));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "fruits"))]
|
||||
if self.mode == GameMode::CTB {
|
||||
return Err(ParseError::UnincludedMode(GameMode::CTB));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mania"))]
|
||||
if self.mode == GameMode::MNA {
|
||||
return Err(ParseError::UnincludedMode(GameMode::MNA));
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "osu", feature = "all_included"))]
|
||||
{
|
||||
self.stack_leniency = stack_leniency.unwrap_or(0.7);
|
||||
}
|
||||
|
||||
Ok(empty)
|
||||
}
|
||||
@@ -264,6 +296,32 @@ impl Beatmap {
|
||||
Ok(empty)
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "osu", feature = "fruits")))]
|
||||
fn parse_timingpoints<R: Read>(
|
||||
&mut self,
|
||||
reader: &mut BufReader<R>,
|
||||
buf: &mut String,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
let mut empty = true;
|
||||
|
||||
while reader.read_line(buf)? != 0 {
|
||||
let line = line_prepare!(buf);
|
||||
|
||||
if line.starts_with('[') && line.ends_with(']') {
|
||||
*section = Section::from_str(&line[1..line.len() - 1]);
|
||||
empty = false;
|
||||
buf.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
Ok(empty)
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "osu", feature = "fruits"))]
|
||||
fn parse_timingpoints<R: Read>(
|
||||
&mut self,
|
||||
reader: &mut BufReader<R>,
|
||||
@@ -386,45 +444,67 @@ impl Beatmap {
|
||||
HitObjectKind::Circle
|
||||
} else if kind & Self::SLIDER_FLAG > 0 {
|
||||
self.n_sliders += 1;
|
||||
let mut curve_points = Vec::with_capacity(16);
|
||||
curve_points.push(pos);
|
||||
|
||||
let mut curve_point_iter = next_field!(split.next(), curve points).split('|');
|
||||
#[cfg(any(
|
||||
feature = "fruits",
|
||||
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||
))]
|
||||
{
|
||||
let mut curve_points = Vec::with_capacity(16);
|
||||
curve_points.push(pos);
|
||||
|
||||
let mut path_type: PathType =
|
||||
next_field!(curve_point_iter.next(), path kind).parse()?;
|
||||
let mut curve_point_iter = next_field!(split.next(), curve points).split('|');
|
||||
|
||||
for pos in curve_point_iter {
|
||||
let mut v = pos.split(':').map(str::parse);
|
||||
let mut path_type: PathType =
|
||||
next_field!(curve_point_iter.next(), path kind).parse()?;
|
||||
|
||||
match (v.next(), v.next()) {
|
||||
(Some(Ok(x)), Some(Ok(y))) => curve_points.push(Pos2 { x, y }),
|
||||
_ => return Err(ParseError::InvalidCurvePoints),
|
||||
for pos in curve_point_iter {
|
||||
let mut v = pos.split(':').map(str::parse);
|
||||
|
||||
match (v.next(), v.next()) {
|
||||
(Some(Ok(x)), Some(Ok(y))) => curve_points.push(Pos2 { x, y }),
|
||||
_ => return Err(ParseError::InvalidCurvePoints),
|
||||
}
|
||||
}
|
||||
|
||||
if self.version <= 6 && curve_points.len() >= 2 {
|
||||
if path_type == PathType::Linear {
|
||||
path_type = PathType::Bezier;
|
||||
}
|
||||
|
||||
if curve_points.len() == 2
|
||||
&& (pos == curve_points[0] || pos == curve_points[1])
|
||||
{
|
||||
path_type = PathType::Linear;
|
||||
}
|
||||
}
|
||||
|
||||
if curve_points.is_empty() {
|
||||
HitObjectKind::Circle
|
||||
} else {
|
||||
let repeats = next_field!(split.next(), repeats).parse::<usize>()?;
|
||||
let len: f32 = next_field!(split.next(), pixel len).parse()?;
|
||||
|
||||
HitObjectKind::Slider {
|
||||
repeats,
|
||||
pixel_len: len,
|
||||
curve_points,
|
||||
path_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.version <= 6 && curve_points.len() >= 2 {
|
||||
if path_type == PathType::Linear {
|
||||
path_type = PathType::Bezier;
|
||||
}
|
||||
|
||||
if curve_points.len() == 2 && (pos == curve_points[0] || pos == curve_points[1])
|
||||
{
|
||||
path_type = PathType::Linear;
|
||||
}
|
||||
}
|
||||
|
||||
if curve_points.is_empty() {
|
||||
HitObjectKind::Circle
|
||||
} else {
|
||||
let repeats = next_field!(split.next(), repeats).parse::<usize>()?;
|
||||
#[cfg(not(any(
|
||||
feature = "fruits",
|
||||
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||
)))]
|
||||
{
|
||||
let repeats = next_field!(split.nth(1), repeats).parse::<usize>()?;
|
||||
let len: f32 = next_field!(split.next(), pixel len).parse()?;
|
||||
|
||||
HitObjectKind::Slider {
|
||||
repeats,
|
||||
pixel_len: len,
|
||||
curve_points,
|
||||
path_type,
|
||||
}
|
||||
}
|
||||
} else if kind & Self::SPINNER_FLAG > 0 {
|
||||
@@ -536,7 +616,17 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parsing() {
|
||||
let file = match File::open("./maps/61843.osu") {
|
||||
let map_id = if cfg!(feature = "osu") {
|
||||
61843
|
||||
} else if cfg!(feature = "mania") {
|
||||
1355822
|
||||
} else if cfg!(feature = "fruits") {
|
||||
1977380
|
||||
} else {
|
||||
110219
|
||||
};
|
||||
|
||||
let file = match File::open(format!("./maps/{}.osu", map_id)) {
|
||||
Ok(file) => file,
|
||||
Err(why) => panic!("Could not read file: {}", why),
|
||||
};
|
||||
@@ -556,9 +646,15 @@ mod tests {
|
||||
println!("hp: {}", map.hp);
|
||||
println!("sv: {}", map.sv);
|
||||
println!("tick_rate: {}", map.tick_rate);
|
||||
println!("stack_leniency: {}", map.stack_leniency);
|
||||
println!("hit_objects: {}", map.hit_objects.len());
|
||||
println!("timing_points: {}", map.timing_points.len());
|
||||
println!("difficulty_points: {}", map.difficulty_points.len());
|
||||
|
||||
#[cfg(any(feature = "osu", feature = "fruits"))]
|
||||
{
|
||||
#[cfg(feature = "all_included")]
|
||||
println!("stack_leniency: {}", map.stack_leniency);
|
||||
|
||||
println!("timing_points: {}", map.timing_points.len());
|
||||
println!("difficulty_points: {}", map.difficulty_points.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "taiko")]
|
||||
|
||||
mod difficulty_object;
|
||||
mod hitobject_rhythm;
|
||||
mod limited_queue;
|
||||
|
||||
+2
-3
@@ -19,6 +19,7 @@ impl TaikoStarProvider for DifficultyAttributes {
|
||||
|
||||
impl TaikoStarProvider for StarResult {
|
||||
fn attributes(self) -> Option<f32> {
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
if let StarResult::Taiko(attributes) = self {
|
||||
Some(attributes.stars)
|
||||
} else {
|
||||
@@ -48,13 +49,11 @@ pub struct TaikoPP<'m> {
|
||||
impl<'m> TaikoPP<'m> {
|
||||
#[inline]
|
||||
pub fn new(map: &'m Beatmap) -> Self {
|
||||
let max_combo = map.hit_objects.iter().filter(|h| h.is_circle()).count();
|
||||
|
||||
Self {
|
||||
map,
|
||||
stars: None,
|
||||
mods: 0,
|
||||
max_combo,
|
||||
max_combo: map.n_circles as usize,
|
||||
combo: None,
|
||||
acc: 1.0,
|
||||
n_misses: 0,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "fruits")]
|
||||
|
||||
extern crate rosu_pp;
|
||||
|
||||
use rosu_pp::Beatmap;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "mania")]
|
||||
|
||||
extern crate rosu_pp;
|
||||
|
||||
use rosu_pp::Beatmap;
|
||||
|
||||
+15
-64
@@ -1,6 +1,8 @@
|
||||
#![cfg(feature = "osu")]
|
||||
|
||||
extern crate rosu_pp;
|
||||
|
||||
use rosu_pp::{osu, Beatmap};
|
||||
use rosu_pp::Beatmap;
|
||||
use std::fs::File;
|
||||
|
||||
struct MapResult {
|
||||
@@ -11,9 +13,17 @@ struct MapResult {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osu_no_sliders() {
|
||||
let star_margin = 0.0075;
|
||||
let pp_margin = 0.0075;
|
||||
fn osu() {
|
||||
let margin = if cfg!(feature = "no_leniency") {
|
||||
0.0025
|
||||
} else if cfg!(feature = "no_sliders_no_leniency") {
|
||||
0.0075
|
||||
} else {
|
||||
0.001
|
||||
};
|
||||
|
||||
let star_margin = margin;
|
||||
let pp_margin = margin;
|
||||
|
||||
for result in RESULTS {
|
||||
let MapResult {
|
||||
@@ -33,66 +43,7 @@ fn osu_no_sliders() {
|
||||
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
|
||||
};
|
||||
|
||||
let result = rosu_pp::OsuPP::new(&map)
|
||||
.mods(*mods)
|
||||
.calculate(osu::no_sliders_no_leniency::stars);
|
||||
|
||||
assert!(
|
||||
(result.stars() - stars).abs() < star_margin * stars,
|
||||
"\nStars:\n\
|
||||
Calculated: {calculated} | Expected: {expected}\n \
|
||||
=> {margin} margin ({allowed} allowed)\n\
|
||||
[map {map} | mods {mods}]\n",
|
||||
calculated = result.stars(),
|
||||
expected = stars,
|
||||
margin = (result.stars() - stars).abs(),
|
||||
allowed = star_margin * stars,
|
||||
map = map_id,
|
||||
mods = mods
|
||||
);
|
||||
|
||||
assert!(
|
||||
(result.pp() - pp).abs() < pp_margin * pp,
|
||||
"\nPP:\n\
|
||||
Calculated: {calculated} | Expected: {expected}\n \
|
||||
=> {margin} margin ({allowed} allowed)\n\
|
||||
[map {map} | mods {mods}]\n",
|
||||
calculated = result.pp(),
|
||||
expected = pp,
|
||||
margin = (result.pp() - pp).abs(),
|
||||
allowed = pp_margin * pp,
|
||||
map = map_id,
|
||||
mods = mods
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osu_no_leniency() {
|
||||
let star_margin = 0.0025;
|
||||
let pp_margin = 0.0025;
|
||||
|
||||
for result in RESULTS {
|
||||
let MapResult {
|
||||
map_id,
|
||||
mods,
|
||||
stars,
|
||||
pp,
|
||||
} = result;
|
||||
|
||||
let file = match File::open(format!("./maps/{}.osu", map_id)) {
|
||||
Ok(file) => file,
|
||||
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
|
||||
};
|
||||
|
||||
let map = match Beatmap::parse(file) {
|
||||
Ok(map) => map,
|
||||
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
|
||||
};
|
||||
|
||||
let result = rosu_pp::osu::OsuPP::new(&map)
|
||||
.mods(*mods)
|
||||
.calculate(osu::no_leniency::stars);
|
||||
let result = rosu_pp::OsuPP::new(&map).mods(*mods).calculate();
|
||||
|
||||
assert!(
|
||||
(result.stars() - stars).abs() < star_margin * stars,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![cfg(feature = "taiko")]
|
||||
|
||||
extern crate rosu_pp;
|
||||
|
||||
use rosu_pp::Beatmap;
|
||||
|
||||
Reference in New Issue
Block a user