Merge branch 'next' into main

This commit is contained in:
MaxOhn
2021-02-25 15:24:18 +01:00
35 changed files with 1101 additions and 1467 deletions
+9 -1
View File
@@ -1,6 +1,14 @@
## Upcoming
# v0.1.1
- No changes so far
# v0.2.0
- Async beatmap parsing through features `async_tokio` or `async_std`
- Hide various parsing related types further inwards, i.e. `rosu_pp::parse::some_type` instead of `rosu_pp::some_type`
- Affected types: `DifficultyPoint`, `HitObject`, `Pos2`, `TimingPoint`, `HitObjectKind`, `PathType`, `HitSound`
## v0.1.1
- parse:
- Efficiently handle huge amounts of curvepoints
+22 -6
View File
@@ -1,6 +1,6 @@
[package]
name = "rosu-pp"
version = "0.1.2"
version = "0.2.0"
authors = ["MaxOhn <ohn.m@hotmail.de>"]
edition = "2018"
license = "MIT"
@@ -8,20 +8,36 @@ readme = "README.md"
repository = "https://github.com/MaxOhn/rosu-pp"
documentation = "https://docs.rs/rosu-pp/"
description = "osu! difficulty and pp calculation for all modes"
keywords = ["osu", "pp", "stars", "async", "async-std"]
keywords = ["osu", "pp", "stars", "async"]
[features]
default = ["osu", "taiko", "fruits", "mania", "no_leniency", "async_std"]
default = ["osu", "taiko", "fruits", "mania", "no_leniency"]
# game modes
osu = []
taiko = []
fruits = []
mania = []
# osu!standard version
all_included = []
no_leniency = []
no_sliders_no_leniency = []
async_std = ["async-std"]
[dependencies]
async-std = { version = "1.9.0", optional = true }
# async version
async_std = ["async-std"]
async_tokio = ["tokio"]
[dependencies.async-std]
version = "1.9"
optional = true
[dependencies.tokio]
version = "1.2"
optional = true
default-features = false
features = ["io-util"]
[dev-dependencies.tokio]
version = "1.2"
features = ["fs", "rt-multi-thread"]
+31 -9
View File
@@ -4,8 +4,9 @@
A standalone crate to calculate star ratings and performance points for all [osu!](https://osu.ppy.sh/home) gamemodes.
Conversions are generally not supported.
Async is supported.
Conversions between gamemodes are generally not supported.
Async is supported through features, see below.
### Usage
@@ -60,29 +61,30 @@ println!("Stars: {} | Max PP: {}", stars, max_pp);
```
### With async
Need to enable `async_std` in Cargo.toml to use async parsing `Beatmap::parse_async`.
If either the `async_tokio` or `async_std` feature is enabled, beatmap parsing will be async.
```rust
use async_std::fs::File;
// use tokio::fs::File;
// Async read the map
let file = match File::open("/path/to/file.osu").await {
Ok(file) => file,
Err(why) => panic!("Could not open file: {}", why),
};
// Async Parse the map
let map = match Beatmap::parse_async(file).await {
// Parse the map asynchronously
let map = match Beatmap::parse(file).await {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
// The rest stays the same
let result = map.pp()
.mods(24) // HDHR
.combo(1234)
.misses(2)
.accuracy(99.2)
.calculate_async().await;
.calculate();
println!("PP: {}", result.pp());
```
@@ -107,7 +109,26 @@ println!("PP: {}", result.pp());
| `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_std` | Enable async beatmap parsing with `async-std` |
| `async_tokio` | Beatmap parsing will be async through a [tokio](https://github.com/tokio-rs/tokio) runtime |
| `async_std` | Beatmap parsing will be async through an [async-std](https://github.com/async-rs/async-std) runtime |
### Benchmarks
Comparing the PP calculation speed between [osu-perf](https://gitlab.com/JackRedstonia/osu-perf/), an [oppai-ng](https://github.com/Francesco149/oppai-ng) rust binding, and rosu-pp's `no_sliders_no_leniency`:
<img src="./benchmark_results/crates_pp_calc.svg">
Comparing the PP calculation speed between rosu-pp's `all_included`, `no_leniency`, and `no_sliders_no_leniency` versions:
<img src="./benchmark_results/rosu_pp_calc.svg">
Comparing the PP (in)accuracy between rosu-pp's `all_included`, `no_leniency`, and `no_sliders_no_leniency` versions:
<img src="./benchmark_results/pp_inaccuracy.svg">
Comparing the stars (in)accuracy between rosu-pp's `all_included`, `no_leniency`, and `no_sliders_no_leniency` versions:
<img src="./benchmark_results/stars_inaccuracy.svg">
### Roadmap
@@ -125,4 +146,5 @@ println!("PP: {}", result.pp());
- \[x\] mania pp
---
- \[x\] refactoring
- \[x\] benchmarking
- \[x\] benchmarking
- \[x\] async
-5
View File
@@ -1,5 +0,0 @@
[![crates.io](https://img.shields.io/crates/v/rosu-pp.svg)](https://crates.io/crates/rosu-pp) [![docs](https://docs.rs/rosu-pp/badge.svg)](https://docs.rs/rosu-pp)
# {{crate}}
{{readme}}
+8 -2
View File
@@ -1,6 +1,9 @@
#![cfg(any(feature = "osu", feature = "fruits"))]
use crate::{Beatmap, DifficultyPoint, TimingPoint};
use crate::{
parse::{DifficultyPoint, TimingPoint},
Beatmap,
};
use std::slice::Iter;
@@ -85,7 +88,10 @@ impl<'p> Iterator for ControlPointIter<'p> {
#[cfg(test)]
mod test {
use crate::{Beatmap, ControlPoint, ControlPointIter, DifficultyPoint, TimingPoint};
use crate::{
parse::{DifficultyPoint, TimingPoint},
Beatmap, ControlPoint, ControlPointIter,
};
#[test]
fn control_point_iter() {
+1 -1
View File
@@ -3,7 +3,7 @@
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
))]
use crate::{math_util, Pos2};
use crate::{math_util, parse::Pos2};
const SLIDER_QUALITY: f32 = 50.0;
+1 -1
View File
@@ -1,4 +1,4 @@
use crate::Pos2;
use crate::parse::Pos2;
const PLAYFIELD_WIDTH: f32 = 512.0;
const BASE_SPEED: f32 = 1.0;
+5 -33
View File
@@ -12,7 +12,11 @@ use movement::Movement;
pub use pp::*;
use slider_state::SliderState;
use crate::{curve::Curve, Beatmap, HitObjectKind, Mods, PathType, Pos2, StarResult, Strains};
use crate::{
curve::Curve,
parse::{HitObjectKind, PathType, Pos2},
Beatmap, Mods, StarResult, Strains,
};
use std::convert::identity;
@@ -626,35 +630,3 @@ pub struct DifficultyAttributes {
pub n_droplets: usize,
pub n_tiny_droplets: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
#[test]
#[ignore]
fn fruits_single() {
let file = match File::open("./maps/1587421.osu") {
Ok(file) => file,
Err(why) => panic!("Could not open file: {}", why),
};
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
let result = FruitsPP::new(&map)
.mods(0)
.combo(266)
.fruits(644)
.droplets(33)
.tiny_droplet_misses(12)
.misses(10)
.calculate();
println!("Stars: {}", result.stars());
println!("PP: {}", result.pp());
}
}
-5
View File
@@ -315,11 +315,6 @@ impl<'m> FruitsPP<'m> {
}
}
#[inline]
pub async fn calculate_async(self) -> PpResult {
self.calculate()
}
#[inline]
fn combo_hits(&self) -> usize {
self.n_fruits.unwrap_or(0) + self.n_droplets.unwrap_or(0) + self.n_misses
+4 -1
View File
@@ -44,7 +44,10 @@ impl<'p> SliderState<'p> {
#[cfg(test)]
mod test {
use crate::{Beatmap, DifficultyPoint, TimingPoint};
use crate::{
parse::{DifficultyPoint, TimingPoint},
Beatmap,
};
use super::SliderState;
+49 -7
View File
@@ -2,14 +2,17 @@
//! A standalone crate to calculate star ratings and performance points for all [osu!](https://osu.ppy.sh/home) gamemodes.
//!
//! Conversions are generally not supported.
//! Conversions between gamemodes are generally not supported.
//!
//! Async is supported through features, see below.
//!
//! ## Usage
//!
//! ```rust,no_run
//! ```no_run
//! use std::fs::File;
//! use rosu_pp::{Beatmap, BeatmapExt};
//!
//! # /*
//! let file = match File::open("/path/to/file.osu") {
//! Ok(file) => file,
//! Err(why) => panic!("Could not open file: {}", why),
@@ -20,6 +23,7 @@
//! 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.
@@ -56,6 +60,40 @@
//! 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 rosu_pp::{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.
@@ -76,6 +114,8 @@
//! | `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 a [tokio](https://github.com/tokio-rs/tokio) runtime |
//! | `async_std` | Beatmap parsing will be async through an [async-std](https://github.com/async-rs/async-std) runtime |
//!
//! ## Roadmap
//!
@@ -94,6 +134,7 @@
//! ---
//! - \[x\] refactoring
//! - \[x\] benchmarking
//! - \[x\] async parsing
#[cfg(feature = "fruits")]
#[cfg_attr(docsrs, doc(cfg(feature = "fruits")))]
@@ -111,7 +152,7 @@ pub mod osu;
#[cfg_attr(docsrs, doc(cfg(feature = "taiko")))]
pub mod taiko;
mod parse;
pub mod parse;
mod pp;
pub use pp::{AnyPP, AttributeProvider};
@@ -139,10 +180,7 @@ pub use osu::OsuPP;
pub use taiko::TaikoPP;
pub use mods::Mods;
pub use parse::{
Beatmap, BeatmapAttributes, DifficultyPoint, GameMode, HitObject, HitObjectKind, HitSound,
ParseError, ParseResult, PathType, Pos2, TimingPoint,
};
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.
@@ -261,6 +299,7 @@ impl BeatmapExt for Beatmap {
}
}
#[inline]
fn pp(&self) -> AnyPP {
AnyPP::new(self)
}
@@ -434,3 +473,6 @@ compile_error!("Only one of the features `no_leniency`, `no_sliders_no_leniency`
)
))]
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 async features `async_tokio` and `async_std` should be enabled");
+1 -26
View File
@@ -6,7 +6,7 @@ mod strain;
pub use pp::*;
use strain::Strain;
use crate::{Beatmap, GameMode, HitObject, Mods, StarResult, Strains};
use crate::{parse::HitObject, Beatmap, GameMode, Mods, StarResult, Strains};
const SECTION_LEN: f32 = 400.0;
const STAR_SCALING_FACTOR: f32 = 0.018;
@@ -168,28 +168,3 @@ impl<'o> DifficultyHitObject<'o> {
pub struct DifficultyAttributes {
pub stars: f32,
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
#[test]
#[ignore]
fn mania_single() {
let file = match File::open("./maps/1355822.osu") {
Ok(file) => file,
Err(why) => panic!("Could not open file: {}", why),
};
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
let result = ManiaPP::new(&map).mods(256).calculate();
println!("Stars: {}", result.stars());
println!("PP: {}", result.pp());
}
}
-5
View File
@@ -136,11 +136,6 @@ impl<'m> ManiaPP<'m> {
}
}
#[inline]
pub async fn calculate_async(self) -> PpResult {
self.calculate()
}
fn compute_strain(&self, score: f32, stars: f32) -> f32 {
let mut strain_value = (5.0 * (stars / 0.2).max(1.0) - 4.0).powf(2.2) / 135.0;
+1 -1
View File
@@ -3,7 +3,7 @@
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
))]
use crate::Pos2;
use crate::parse::Pos2;
#[inline]
pub(crate) fn cpn(mut p: i32, n: i32) -> f32 {
-5
View File
@@ -261,11 +261,6 @@ impl<'m> OsuPP<'m> {
unreachable!()
}
#[inline]
pub async fn calculate_async(self) -> PpResult {
self.calculate()
}
/// Returns an object which contains the pp and [`DifficultyAttributes`](crate::osu::DifficultyAttributes)
/// containing stars and other attributes.
///
+1 -28
View File
@@ -5,7 +5,7 @@
#![cfg(feature = "all_included")]
use super::super::DifficultyAttributes;
use crate::Pos2;
use crate::parse::Pos2;
mod difficulty_object;
mod osu_object;
@@ -439,30 +439,3 @@ const OSU_AR_MIN: f32 = 1800.0;
fn difficulty_range_ar(ar: f32) -> f32 {
crate::difficulty_range(ar, OSU_AR_MAX, OSU_AR_AVG, OSU_AR_MIN)
}
#[cfg(test)]
mod tests {
use super::super::super::OsuPP;
use crate::Beatmap;
use std::fs::File;
#[test]
#[ignore]
fn all_included_single() {
// TOCHECK: 114708, 1208872, 1322261
let file = match File::open("./maps/1295309.osu") {
Ok(file) => file,
Err(why) => panic!("Could not open file: {}", why),
};
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
let result = OsuPP::new(&map).mods(0).calculate();
println!("Stars: {}", result.stars());
println!("PP: {}", result.pp());
}
}
+5 -1
View File
@@ -1,7 +1,11 @@
use super::super::super::DifficultyAttributes;
use super::slider_state::SliderState;
use crate::{curve::Curve, Beatmap, HitObject, HitObjectKind, PathType, Pos2};
use crate::{
curve::Curve,
parse::{HitObject, HitObjectKind, PathType, Pos2},
Beatmap,
};
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
@@ -44,7 +44,10 @@ impl<'p> SliderState<'p> {
#[cfg(test)]
mod test {
use crate::{Beatmap, DifficultyPoint, TimingPoint};
use crate::{
parse::{DifficultyPoint, TimingPoint},
Beatmap,
};
use super::SliderState;
-32
View File
@@ -270,35 +270,3 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
strains,
}
}
#[cfg(test)]
mod tests {
use super::super::super::OsuPP;
use crate::Beatmap;
use std::fs::File;
#[test]
#[ignore]
fn no_leniency_single() {
let file = match File::open("./maps/2573164.osu") {
Ok(file) => file,
Err(why) => panic!("Could not open file: {}", why),
};
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
let result = OsuPP::new(&map)
.mods(0)
// .n300(1206)
// .n100(15)
// .n50(0)
// .combo(1643)
.calculate();
println!("Stars: {}", result.stars());
println!("PP: {}", result.pp());
}
}
+5 -1
View File
@@ -1,7 +1,11 @@
use super::super::super::DifficultyAttributes;
use super::slider_state::SliderState;
use crate::{curve::Curve, Beatmap, HitObject, HitObjectKind, PathType, Pos2};
use crate::{
curve::Curve,
parse::{HitObject, HitObjectKind, PathType, Pos2},
Beatmap,
};
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
+4 -1
View File
@@ -44,7 +44,10 @@ impl<'p> SliderState<'p> {
#[cfg(test)]
mod test {
use crate::{Beatmap, DifficultyPoint, TimingPoint};
use crate::{
parse::{DifficultyPoint, TimingPoint},
Beatmap,
};
use super::SliderState;
@@ -1,4 +1,4 @@
use crate::HitObject;
use crate::parse::HitObject;
use std::borrow::Cow;
+4 -27
View File
@@ -16,7 +16,10 @@ use skill::Skill;
use skill_kind::SkillKind;
use slider_state::SliderState;
use crate::{Beatmap, HitObject, HitObjectKind, Mods, StarResult, Strains};
use crate::{
parse::{HitObject, HitObjectKind},
Beatmap, Mods, StarResult, Strains,
};
use std::borrow::Cow;
@@ -282,29 +285,3 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
strains,
}
}
#[cfg(test)]
mod tests {
use super::super::super::OsuPP;
use crate::Beatmap;
use std::fs::File;
#[test]
#[ignore]
fn no_sliders_single() {
let file = match File::open("./maps/1851299.osu") {
Ok(file) => file,
Err(why) => panic!("Could not open file: {}", why),
};
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
let result = OsuPP::new(&map).mods(2).calculate();
println!("Stars: {}", result.stars());
println!("PP: {}", result.pp());
}
}
+662 -947
View File
File diff suppressed because it is too large Load Diff
+9 -22
View File
@@ -55,20 +55,6 @@ impl<'m> AnyPP<'m> {
}
}
#[inline]
pub async fn calculate_async(self) -> PpResult {
match self {
#[cfg(feature = "fruits")]
Self::Fruits(f) => f.calculate_async().await,
#[cfg(feature = "mania")]
Self::Mania(m) => m.calculate_async().await,
#[cfg(feature = "osu")]
Self::Osu(o) => o.calculate_async().await,
#[cfg(feature = "taiko")]
Self::Taiko(t) => t.calculate_async().await,
}
}
/// [`AttributeProvider`] is implemented by [`StarResult`](crate::StarResult)
/// and [`PpResult`](crate::PpResult) meaning you can give the result of a \
/// star calculation or the result of a pp calculation.
@@ -126,8 +112,8 @@ impl<'m> AnyPP<'m> {
/// be sure to call this last before calling `calculate`.
///
/// Irrelevant for osu!mania.
#[allow(unused_variables)]
#[inline]
#[cfg(any(feature = "fruits", feature = "osu", feature = "taiko"))]
pub fn accuracy(self, acc: f32) -> Self {
match self {
#[cfg(feature = "fruits")]
@@ -144,8 +130,8 @@ impl<'m> AnyPP<'m> {
/// Specify the amount of misses of a play.
///
/// Irrelevant for osu!mania.
#[allow(unused_variables)]
#[inline]
#[cfg(any(feature = "fruits", feature = "osu", feature = "taiko"))]
pub fn misses(self, misses: usize) -> Self {
match self {
#[cfg(feature = "fruits")]
@@ -162,8 +148,8 @@ impl<'m> AnyPP<'m> {
/// Specify the max combo of the play.
///
/// Irrelevant for osu!mania.
#[allow(unused_variables)]
#[inline]
#[cfg(any(feature = "fruits", feature = "osu", feature = "taiko"))]
pub fn combo(self, combo: usize) -> Self {
match self {
#[cfg(feature = "fruits")]
@@ -180,8 +166,8 @@ impl<'m> AnyPP<'m> {
/// Specify the amount of 300s of a play.
///
/// Irrelevant for osu!mania.
#[allow(unused_variables)]
#[inline]
#[cfg(any(feature = "fruits", feature = "osu", feature = "taiko"))]
pub fn n300(self, n300: usize) -> Self {
match self {
#[cfg(feature = "fruits")]
@@ -198,8 +184,8 @@ impl<'m> AnyPP<'m> {
/// Specify the amount of 100s of a play.
///
/// Irrelevant for osu!mania.
#[allow(unused_variables)]
#[inline]
#[cfg(any(feature = "fruits", feature = "osu", feature = "taiko"))]
pub fn n100(self, n100: usize) -> Self {
match self {
#[cfg(feature = "fruits")]
@@ -216,8 +202,8 @@ impl<'m> AnyPP<'m> {
/// Specify the amount of 50s of a play.
///
/// Irrelevant for osu!mania and osu!taiko.
#[allow(unused_variables)]
#[inline]
#[cfg(any(feature = "fruits", feature = "osu"))]
pub fn n50(self, n50: usize) -> Self {
match self {
#[cfg(feature = "fruits")]
@@ -235,10 +221,11 @@ impl<'m> AnyPP<'m> {
///
/// This value is only relevant for osu!ctb for which it represent
/// the amount of tiny droplet misses.
#[allow(unused_variables)]
#[inline]
#[cfg(feature = "fruits")]
pub fn n_katu(self, n_katu: usize) -> Self {
match self {
#[cfg(feature = "fruits")]
Self::Fruits(f) => Self::Fruits(f.tiny_droplet_misses(n_katu)),
#[cfg(feature = "mania")]
Self::Mania(_) => self,
@@ -254,8 +241,8 @@ impl<'m> AnyPP<'m> {
/// This value is only relevant for osu!mania.
///
/// On `NoMod` its between 0 and 1,000,000, on `Easy` between 0 and 500,000, etc.
#[allow(unused_variables)]
#[inline]
#[cfg(feature = "mania")]
pub fn score(self, score: u32) -> Self {
match self {
#[cfg(feature = "fruits")]
+1 -1
View File
@@ -1,5 +1,5 @@
use super::{closest_rhythm, HitObjectRhythm};
use crate::HitObject;
use crate::parse::HitObject;
#[derive(Clone, Debug)]
pub(crate) struct DifficultyObject<'o> {
+1 -1
View File
@@ -1,4 +1,4 @@
use crate::HitObject;
use crate::parse::HitObject;
use std::cmp::Ordering;
-25
View File
@@ -265,28 +265,3 @@ fn norm(p: f32, a: f32, b: f32, c: f32) -> f32 {
pub struct DifficultyAttributes {
pub stars: f32,
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
#[test]
#[ignore]
fn taiko_single() {
let file = match File::open("./maps/168450.osu") {
Ok(file) => file,
Err(why) => panic!("Could not open file: {}", why),
};
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
let result = TaikoPP::new(&map).mods(64).calculate();
println!("Stars: {}", result.stars());
println!("PP: {}", result.pp());
}
}
-5
View File
@@ -181,11 +181,6 @@ impl<'m> TaikoPP<'m> {
}
}
#[inline]
pub async fn calculate_async(self) -> PpResult {
self.calculate()
}
fn compute_strain_value(&self, stars: f32) -> f32 {
let exp_base = 5.0 * (stars / 0.0075).max(1.0) - 4.0;
let mut strain = exp_base * exp_base / 100_000.0;
+1 -1
View File
@@ -1,4 +1,4 @@
use crate::{HitObject, HitSound};
use crate::parse::{HitObject, HitSound};
pub(crate) trait Rim {
fn is_rim(&self) -> bool;
+1 -1
View File
@@ -1,6 +1,6 @@
use super::{LimitedQueue, Rim};
use crate::{Beatmap, HitObject};
use crate::{parse::HitObject, Beatmap};
const ROLL_MIN_REPETITIONS: usize = 12;
const TL_MIN_REPETITIONS: isize = 16;
+65 -63
View File
@@ -3,7 +3,6 @@
extern crate rosu_pp;
use rosu_pp::Beatmap;
use std::fs::File;
struct MapResult {
map_id: u32,
@@ -12,8 +11,28 @@ struct MapResult {
pp: f32,
}
macro_rules! assert_result {
($kind:expr => $result:expr, $margin:expr, $expected:ident, $map_id:ident, $mods:ident) => {
assert!(
($result - $expected).abs() < $margin * $expected,
"\n{kind}:\n\
Calculated: {calculated} | Expected: {expected}\n \
=> {margin} margin ({allowed} allowed)\n\
[map {map} | mods {mods}]\n",
kind = $kind,
calculated = $result,
expected = $expected,
margin = ($result - $expected).abs(),
allowed = $margin * $expected,
map = $map_id,
mods = $mods
);
};
}
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
#[test]
fn fruits() {
fn fruits_sync() {
let star_margin = 0.0001;
let pp_margin = 0.0001;
@@ -25,7 +44,7 @@ fn fruits() {
pp,
} = result;
let file = match File::open(format!("./maps/{}.osu", map_id)) {
let file = match std::fs::File::open(format!("./maps/{}.osu", map_id)) {
Ok(file) => file,
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
};
@@ -37,42 +56,50 @@ fn fruits() {
let result = rosu_pp::FruitsPP::new(&map).mods(*mods).calculate();
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
);
assert_result!("Stars" => result.stars(), star_margin, stars, map_id, mods);
assert_result!("PP" => result.pp(), pp_margin, pp, map_id, mods);
}
}
#[cfg(feature = "async_tokio")]
#[test]
fn fruits_async_tokio() {
tokio::runtime::Runtime::new()
.expect("could not start runtime")
.block_on(async {
let star_margin = 0.0001;
let pp_margin = 0.0001;
for result in RESULTS {
let MapResult {
map_id,
mods,
stars,
pp,
} = result;
let file = match tokio::fs::File::open(format!("./maps/{}.osu", map_id)).await {
Ok(file) => file,
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
};
let map = match Beatmap::parse(file).await {
Ok(map) => map,
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
};
let result = rosu_pp::FruitsPP::new(&map).mods(*mods).calculate();
assert_result!("Stars" => result.stars(), star_margin, stars, map_id, mods);
assert_result!("PP" => result.pp(), pp_margin, pp, map_id, mods);
}
});
}
#[cfg(feature = "async_std")]
#[test]
fn fruits_async() {
use async_std::{fs::File, task};
task::block_on(async {
fn fruits_async_std() {
async_std::task::block_on(async {
let star_margin = 0.0001;
let pp_margin = 0.0001;
@@ -84,45 +111,20 @@ fn fruits_async() {
pp,
} = result;
let file = match File::open(format!("./maps/{}.osu", map_id)).await {
let file = match async_std::fs::File::open(format!("./maps/{}.osu", map_id)).await {
Ok(file) => file,
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
};
let map = match Beatmap::parse_async(file).await {
let map = match Beatmap::parse(file).await {
Ok(map) => map,
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
};
let result = rosu_pp::FruitsPP::new(&map).mods(*mods).calculate();
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
);
assert_result!("Stars" => result.stars(), star_margin, stars, map_id, mods);
assert_result!("PP" => result.pp(), pp_margin, pp, map_id, mods);
}
})
}
+65 -63
View File
@@ -3,7 +3,6 @@
extern crate rosu_pp;
use rosu_pp::Beatmap;
use std::fs::File;
struct MapResult {
map_id: u32,
@@ -12,8 +11,28 @@ struct MapResult {
pp: f32,
}
macro_rules! assert_result {
($kind:expr => $result:expr, $margin:expr, $expected:ident, $map_id:ident, $mods:ident) => {
assert!(
($result - $expected).abs() < $margin * $expected,
"\n{kind}:\n\
Calculated: {calculated} | Expected: {expected}\n \
=> {margin} margin ({allowed} allowed)\n\
[map {map} | mods {mods}]\n",
kind = $kind,
calculated = $result,
expected = $expected,
margin = ($result - $expected).abs(),
allowed = $margin * $expected,
map = $map_id,
mods = $mods
);
};
}
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
#[test]
fn mania() {
fn mania_sync() {
let star_margin = 0.00001;
let pp_margin = 0.00001;
@@ -25,7 +44,7 @@ fn mania() {
pp,
} = result;
let file = match File::open(format!("./maps/{}.osu", map_id)) {
let file = match std::fs::File::open(format!("./maps/{}.osu", map_id)) {
Ok(file) => file,
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
};
@@ -37,42 +56,50 @@ fn mania() {
let result = rosu_pp::ManiaPP::new(&map).mods(*mods).calculate();
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
);
assert_result!("Stars" => result.stars(), star_margin, stars, map_id, mods);
assert_result!("PP" => result.pp(), pp_margin, pp, map_id, mods);
}
}
#[cfg(feature = "async_tokio")]
#[test]
fn mania_async_tokio() {
tokio::runtime::Runtime::new()
.expect("could not start runtime")
.block_on(async {
let star_margin = 0.00001;
let pp_margin = 0.00001;
for result in RESULTS {
let MapResult {
map_id,
mods,
stars,
pp,
} = result;
let file = match tokio::fs::File::open(format!("./maps/{}.osu", map_id)).await {
Ok(file) => file,
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
};
let map = match Beatmap::parse(file).await {
Ok(map) => map,
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
};
let result = rosu_pp::ManiaPP::new(&map).mods(*mods).calculate();
assert_result!("Stars" => result.stars(), star_margin, stars, map_id, mods);
assert_result!("PP" => result.pp(), pp_margin, pp, map_id, mods);
}
});
}
#[cfg(feature = "async_std")]
#[test]
fn mania_async() {
use async_std::{fs::File, task};
task::block_on(async {
fn mania_async_std() {
async_std::task::block_on(async {
let star_margin = 0.00001;
let pp_margin = 0.00001;
@@ -84,45 +111,20 @@ fn mania_async() {
pp,
} = result;
let file = match File::open(format!("./maps/{}.osu", map_id)).await {
let file = match async_std::fs::File::open(format!("./maps/{}.osu", map_id)).await {
Ok(file) => file,
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
};
let map = match Beatmap::parse_async(file).await {
let map = match Beatmap::parse(file).await {
Ok(map) => map,
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
};
let result = rosu_pp::ManiaPP::new(&map).mods(*mods).calculate();
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
);
assert_result!("Stars" => result.stars(), star_margin, stars, map_id, mods);
assert_result!("PP" => result.pp(), pp_margin, pp, map_id, mods);
}
})
}
+75 -75
View File
@@ -3,7 +3,6 @@
extern crate rosu_pp;
use rosu_pp::Beatmap;
use std::fs::File;
struct MapResult {
map_id: u32,
@@ -12,9 +11,27 @@ struct MapResult {
pp: f32,
}
#[test]
fn osu() {
let margin = if cfg!(feature = "no_sliders_no_leniency") {
macro_rules! assert_result {
($kind:expr => $result:expr, $margin:expr, $expected:ident, $map_id:ident, $mods:ident) => {
assert!(
($result - $expected).abs() < $margin * $expected,
"\n{kind}:\n\
Calculated: {calculated} | Expected: {expected}\n \
=> {margin} margin ({allowed} allowed)\n\
[map {map} | mods {mods}]\n",
kind = $kind,
calculated = $result,
expected = $expected,
margin = ($result - $expected).abs(),
allowed = $margin * $expected,
map = $map_id,
mods = $mods
);
};
}
fn margin() -> f32 {
if cfg!(feature = "no_sliders_no_leniency") {
0.0075
} else if cfg!(feature = "no_leniency") {
0.0025
@@ -22,7 +39,13 @@ fn osu() {
0.001
} else {
unreachable!()
};
}
}
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
#[test]
fn osu_sync() {
let margin = margin();
let star_margin = margin;
let pp_margin = margin;
@@ -35,7 +58,7 @@ fn osu() {
pp,
} = result;
let file = match File::open(format!("./maps/{}.osu", map_id)) {
let file = match std::fs::File::open(format!("./maps/{}.osu", map_id)) {
Ok(file) => file,
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
};
@@ -47,51 +70,53 @@ fn osu() {
let result = rosu_pp::OsuPP::new(&map).mods(*mods).calculate();
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
);
assert_result!("Stars" => result.stars(), star_margin, stars, map_id, mods);
assert_result!("PP" => result.pp(), pp_margin, pp, map_id, mods);
}
}
#[cfg(feature = "async_tokio")]
#[test]
fn osu_async_tokio() {
tokio::runtime::Runtime::new()
.expect("could not start runtime")
.block_on(async {
let margin = margin();
let star_margin = margin;
let pp_margin = margin;
for result in RESULTS {
let MapResult {
map_id,
mods,
stars,
pp,
} = result;
let file = match tokio::fs::File::open(format!("./maps/{}.osu", map_id)).await {
Ok(file) => file,
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
};
let map = match Beatmap::parse(file).await {
Ok(map) => map,
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
};
let result = rosu_pp::OsuPP::new(&map).mods(*mods).calculate();
assert_result!("Stars" => result.stars(), star_margin, stars, map_id, mods);
assert_result!("PP" => result.pp(), pp_margin, pp, map_id, mods);
}
});
}
#[cfg(feature = "async_std")]
#[test]
fn osu_async() {
use async_std::{fs::File, task};
task::block_on(async {
let margin = if cfg!(feature = "no_sliders_no_leniency") {
0.0075
} else if cfg!(feature = "no_leniency") {
0.0025
} else if cfg!(feature = "all_included") {
0.001
} else {
unreachable!()
};
fn osu_async_std() {
async_std::task::block_on(async {
let margin = margin();
let star_margin = margin;
let pp_margin = margin;
@@ -104,45 +129,20 @@ fn osu_async() {
pp,
} = result;
let file = match File::open(format!("./maps/{}.osu", map_id)).await {
let file = match async_std::fs::File::open(format!("./maps/{}.osu", map_id)).await {
Ok(file) => file,
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
};
let map = match Beatmap::parse_async(file).await {
let map = match Beatmap::parse(file).await {
Ok(map) => map,
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
};
let result = rosu_pp::OsuPP::new(&map).mods(*mods).calculate();
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
);
assert_result!("Stars" => result.stars(), star_margin, stars, map_id, mods);
assert_result!("PP" => result.pp(), pp_margin, pp, map_id, mods);
}
})
}
+65 -63
View File
@@ -3,7 +3,6 @@
extern crate rosu_pp;
use rosu_pp::Beatmap;
use std::fs::File;
struct MapResult {
map_id: u32,
@@ -12,8 +11,28 @@ struct MapResult {
pp: f32,
}
macro_rules! assert_result {
($kind:expr => $result:expr, $margin:expr, $expected:ident, $map_id:ident, $mods:ident) => {
assert!(
($result - $expected).abs() < $margin * $expected,
"\n{kind}:\n\
Calculated: {calculated} | Expected: {expected}\n \
=> {margin} margin ({allowed} allowed)\n\
[map {map} | mods {mods}]\n",
kind = $kind,
calculated = $result,
expected = $expected,
margin = ($result - $expected).abs(),
allowed = $margin * $expected,
map = $map_id,
mods = $mods
);
};
}
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
#[test]
fn taiko() {
fn taiko_sync() {
let star_margin = 0.0001;
let pp_margin = 0.0001;
@@ -25,7 +44,7 @@ fn taiko() {
pp,
} = result;
let file = match File::open(format!("./maps/{}.osu", map_id)) {
let file = match std::fs::File::open(format!("./maps/{}.osu", map_id)) {
Ok(file) => file,
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
};
@@ -37,42 +56,50 @@ fn taiko() {
let result = rosu_pp::TaikoPP::new(&map).mods(*mods).calculate();
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
);
assert_result!("Stars" => result.stars(), star_margin, stars, map_id, mods);
assert_result!("PP" => result.pp(), pp_margin, pp, map_id, mods);
}
}
#[cfg(feature = "async_tokio")]
#[test]
fn taiko_async_tokio() {
tokio::runtime::Runtime::new()
.expect("could not start runtime")
.block_on(async {
let star_margin = 0.0001;
let pp_margin = 0.0001;
for result in RESULTS {
let MapResult {
map_id,
mods,
stars,
pp,
} = result;
let file = match tokio::fs::File::open(format!("./maps/{}.osu", map_id)).await {
Ok(file) => file,
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
};
let map = match Beatmap::parse(file).await {
Ok(map) => map,
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
};
let result = rosu_pp::TaikoPP::new(&map).mods(*mods).calculate();
assert_result!("Stars" => result.stars(), star_margin, stars, map_id, mods);
assert_result!("PP" => result.pp(), pp_margin, pp, map_id, mods);
}
});
}
#[cfg(feature = "async_std")]
#[test]
fn taiko_async() {
use async_std::{fs::File, task};
task::block_on(async {
fn taiko_async_std() {
async_std::task::block_on(async {
let star_margin = 0.0001;
let pp_margin = 0.0001;
@@ -84,45 +111,20 @@ fn taiko_async() {
pp,
} = result;
let file = match File::open(format!("./maps/{}.osu", map_id)).await {
let file = match async_std::fs::File::open(format!("./maps/{}.osu", map_id)).await {
Ok(file) => file,
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
};
let map = match Beatmap::parse_async(file).await {
let map = match Beatmap::parse(file).await {
Ok(map) => map,
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
};
let result = rosu_pp::TaikoPP::new(&map).mods(*mods).calculate();
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
);
assert_result!("Stars" => result.stars(), star_margin, stars, map_id, mods);
assert_result!("PP" => result.pp(), pp_margin, pp, map_id, mods);
}
})
}