fixed & extended test cases

This commit is contained in:
MaxOhn
2022-10-21 11:07:29 +02:00
parent 17ce0ef2c9
commit 335f0a7dde
27 changed files with 1677 additions and 898 deletions
+40 -24
View File
@@ -22,14 +22,13 @@ let map = match Beatmap::from_path("/path/to/file.osu") {
let result = map.pp() let result = map.pp()
.mods(24) // HDHR .mods(24) // HDHR
.combo(1234) .combo(1234)
.accuracy(99.2)
.misses(2) .misses(2)
.accuracy(99.2) // should be called last
.calculate(); .calculate();
println!("PP: {}", result.pp()); println!("PP: {}", result.pp());
// If you intend to reuse the current map-mod combination, // If you want to reuse the current map-mod combination, make use of the previous result!
// make use of the previous result!
// If attributes are given, then stars & co don't have to be recalculated. // If attributes are given, then stars & co don't have to be recalculated.
let next_result = map.pp() let next_result = map.pp()
.mods(24) // HDHR .mods(24) // HDHR
@@ -68,7 +67,7 @@ let map = match Beatmap::from_path("/path/to/file.osu").await {
let result = map.pp() let result = map.pp()
.mods(24) // HDHR .mods(24) // HDHR
.combo(1234) .combo(1234)
.misses(2) .n_misses(2)
.accuracy(99.2) .accuracy(99.2)
.calculate(); .calculate();
@@ -113,12 +112,12 @@ let mut gradual_performance = map.gradual_performance(mods);
// The default score state is kinda chunky because it considers all modes. // The default score state is kinda chunky because it considers all modes.
let state = ScoreState { let state = ScoreState {
max_combo: 1, max_combo: 1,
n_katu: 0, // only relevant for ctb n_geki: 0, // only relevant for mania
n_katu: 0, // only relevant for mania and ctb
n300: 1, n300: 1,
n100: 0, n100: 0,
n50: 0, n50: 0,
misses: 0, n_misses: 0,
score: 300, // only relevant for mania
}; };
// Process the score state after the first object // Process the score state after the first object
@@ -144,7 +143,7 @@ let state = TaikoScoreState {
max_combo: 11, max_combo: 11,
n300: 9, n300: 9,
n100: 1, n100: 1,
misses: 1, n_misses: 1,
}; };
// Process the next 10 objects in one go // Process the next 10 objects in one go
@@ -158,29 +157,46 @@ println!("PP after the first 11 objects: {}", curr_performance.pp());
### Features ### Features
| Flag | Description | | Flag | Description |
|-----|-----| | ------------- | ---------------------------------------------------------------------------------------- |
| `default` | Beatmap parsing will be non-async | | `default` | Beatmap parsing will be non-async |
| `async_tokio` | Beatmap parsing will be async through [tokio](https://github.com/tokio-rs/tokio) | | `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) | | `async_std` | Beatmap parsing will be async through [async-std](https://github.com/async-rs/async-std) |
### Version
A large portion of this repository is a port of [osu!lazer](https://github.com/ppy/osu)'s difficulty and performance calculation.
- osu!:
- osu!lazer: Commit `85adfc2df7d931164181e145377a6ced8db2bfb3` (Wed Sep 28 18:26:36 2022 +0300)
- osu!tools: Commit `146d5916937161ef65906aa97f85d367035f3712` (Sat Oct 8 14:28:49 2022 +0900)
- taiko:
- osu!lazer: Commit `234c6ac7998fbc6742503e1a589536255554e56a` (Wed Oct 5 20:21:15 2022 +0900)
- osu!tools: Commit `146d5916937161ef65906aa97f85d367035f3712` (Sat Oct 8 14:28:49 2022 +0900)
- catch: (will be updated on the next rework)
- osu!lazer: -
- osu!tools: -
- mania:
- osu!lazer: Commit `7342fb7f51b34533a42bffda89c3d6c569cc69ce` (Tue Oct 11 14:34:50 2022 +0900)
- osu!tools: Commit `146d5916937161ef65906aa97f85d367035f3712` (Sat Oct 8 14:28:49 2022 +0900)
### Accuracy ### Accuracy
Here are some plots showing the differences of `rosu-pp`'s values and osu!'s official [osu-tools](https://github.com/ppy/osu-tools). The difficulty and performance attributes generated by [osu-tools](https://github.com/ppy/osu-tools) itself were compared with rosu-pp's results when running on `130,000` different maps. Additionally, multiple mod combinations were tested depending on the mode:
Note that osu-tools was used on [this commit](https://github.com/ppy/osu/commit/9fb2402781ad91c197d51aeec716b0000f52c4d1) which is currently (2021-11-14) accurate for osu!standard but for other modes it might include changes that were not applied into stable and thus not implemented in rosu-pp. - osu!: NM, EZ, HD, HR, DT
- taiko: NM, HD, HR, DT (+ all osu! converts)
- catch: -
- mania: NM, DT (+ all osu! converts)
osu!standard: (very accurate, flashlight has the highest average but is still very small) For every (!) comparison of the star and pp values, the error margin was below `0.000000001`, ensuing a great accuracy.
<img src="./pp-plot/osu_accuracy.svg">
osu!mania: (close to perfect values) ### Benchmark
<img src="./pp-plot/mania_accuracy.svg">
osu!catch: (pretty accurate) To be done
<img src="./pp-plot/catch_accuracy.svg">
osu!taiko: (decently accurate, potentially more imprecise due to non-live changes in osu-tools)
<img src="./pp-plot/taiko_accuracy.svg">
### Bindings ### Bindings
-26
View File
@@ -209,29 +209,3 @@ impl Iterator for CatchObjectIter<'_> {
None None
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = CatchGradualDifficultyAttributes::new(&map, 0);
assert!(attributes.next().is_none());
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn iter_end_eq_regular() {
let map = Beatmap::from_path("./maps/2118524.osu").expect("failed to parse map");
let mods = 64;
let regular = crate::CatchStars::new(&map).mods(mods).calculate();
let iter_end = CatchGradualDifficultyAttributes::new(&map, mods)
.last()
.expect("empty iter");
assert_eq!(regular, iter_end);
}
}
+2 -106
View File
@@ -78,7 +78,7 @@ impl CatchScoreState {
/// // Then comes a miss. /// // Then comes a miss.
/// // Note that state's max combo won't be incremented for /// // Note that state's max combo won't be incremented for
/// // the next few objects because the combo is reset. /// // the next few objects because the combo is reset.
/// state.misses += 1; /// state.n_misses += 1;
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.process_next_object(state.clone()).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
@@ -114,7 +114,7 @@ impl CatchScoreState {
/// state.n_droplets = ... /// state.n_droplets = ...
/// state.n_tiny_droplets = ... /// state.n_tiny_droplets = ...
/// state.n_tiny_droplet_misses = ... /// state.n_tiny_droplet_misses = ...
/// state.misses = ... /// state.n_misses = ...
/// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap(); /// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
@@ -187,107 +187,3 @@ impl<'map> CatchGradualPerformanceAttributes<'map> {
Some(performance) Some(performance)
} }
} }
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn correct_empty() {
let map = Beatmap::from_path("./maps/2118524.osu").expect("failed to parse map");
let mods = 64;
let mut gradual = CatchGradualPerformanceAttributes::new(&map, mods);
let state = CatchScoreState::default();
assert!(gradual
.process_next_n_objects(state.clone(), usize::MAX)
.is_some());
assert!(gradual.process_next_object(state).is_none());
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn next_and_next_n() {
let map = Beatmap::from_path("./maps/2118524.osu").expect("failed to parse map");
let mods = 64;
let state = CatchScoreState::default();
let mut gradual1 = CatchGradualPerformanceAttributes::new(&map, mods);
let mut gradual2 = CatchGradualPerformanceAttributes::new(&map, mods);
for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
}
let n = 80;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
}
let state = CatchScoreState {
max_combo: 101,
n_fruits: 99,
n_droplets: 2,
n_tiny_droplets: 68,
n_tiny_droplet_misses: 0,
n_misses: 0,
};
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
assert_eq!(next_n, next);
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn gradual_end_eq_regular() {
let map = Beatmap::from_path("./maps/2118524.osu").expect("failed to parse map");
let mods = 64;
let regular = CatchPP::new(&map).mods(mods).calculate();
let mut gradual = CatchGradualPerformanceAttributes::new(&map, mods);
let state = CatchScoreState {
max_combo: 730,
n_fruits: 728,
n_droplets: 2,
n_tiny_droplets: 291,
n_tiny_droplet_misses: 0,
n_misses: 0,
};
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end);
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn gradual_eq_regular_passed() {
let map = Beatmap::from_path("./maps/2118524.osu").expect("failed to parse map");
let mods = 64;
let n = 100;
let regular = CatchPP::new(&map).mods(mods).passed_objects(n).calculate();
let mut gradual = CatchGradualPerformanceAttributes::new(&map, mods);
let state = CatchScoreState {
max_combo: 101,
n_fruits: 99,
n_droplets: 2,
n_tiny_droplets: 68,
n_tiny_droplet_misses: 0,
n_misses: 0,
};
let gradual = gradual.process_next_n_objects(state, n).unwrap();
assert_eq!(regular, gradual);
}
}
+2 -2
View File
@@ -23,8 +23,8 @@ use crate::{Beatmap, DifficultyAttributes, Mods, OsuPP, PerformanceAttributes};
/// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars()); /// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars());
/// ///
/// let next_result = CatchPP::new(&map) /// let next_result = CatchPP::new(&map)
/// .attributes(pp_result) // reusing previous results for performance /// .attributes(pp_result) // reusing previous results for performance
/// .mods(8 + 64) // has to be the same to reuse attributes /// .mods(8 + 64) // has to be the same to reuse attributes
/// .accuracy(99.5) /// .accuracy(99.5)
/// .calculate(); /// .calculate();
/// ///
+1 -4
View File
@@ -214,7 +214,6 @@ impl From<ScoreState> for ManiaScoreState {
/// for _ in 0..10 { /// for _ in 0..10 {
/// state.n300 += 1; /// state.n300 += 1;
/// state.max_combo += 1; /// state.max_combo += 1;
/// state.score += 123;
/// ///
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.process_next_object(state.clone()).unwrap();
@@ -226,7 +225,7 @@ impl From<ScoreState> for ManiaScoreState {
/// // Then comes a miss. /// // Then comes a miss.
/// // Note that state's max combo won't be incremented for /// // Note that state's max combo won't be incremented for
/// // the next few objects because the combo is reset. /// // the next few objects because the combo is reset.
/// state.misses += 1; /// state.n_misses += 1;
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.process_next_object(state.clone()).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
@@ -238,7 +237,6 @@ impl From<ScoreState> for ManiaScoreState {
/// state.n300 += 2; /// state.n300 += 2;
/// state.n100 += 7; /// state.n100 += 7;
/// state.n50 += 1; /// state.n50 += 1;
/// state.score += 987;
/// // Don't forget state.n_katu /// // Don't forget state.n_katu
/// # /* /// # /*
/// let performance = gradual_perf.process_next_n_objects(state.clone(), 10).unwrap(); /// let performance = gradual_perf.process_next_n_objects(state.clone(), 10).unwrap();
@@ -249,7 +247,6 @@ impl From<ScoreState> for ManiaScoreState {
/// // Now comes another 300. Note that the max combo gets incremented again. /// // Now comes another 300. Note that the max combo gets incremented again.
/// state.n300 += 1; /// state.n300 += 1;
/// state.max_combo += 1; /// state.max_combo += 1;
/// state.score += 123;
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.process_next_object(state.clone()).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
+70 -73
View File
@@ -15,25 +15,24 @@
//! }; //! };
//! # */ let map = Beatmap::default(); //! # */ let map = Beatmap::default();
//! //!
//! // If `BeatmapExt` is included, you can make use of //! // If `BeatmapExt` is included, you can make use of some methods
//! // some methods on `Beatmap` to make your life simpler. //! // on `Beatmap` to make your life simpler like `BeatmapExt::pp`.
//! let result = map.pp() //! let result = map.pp()
//! .mods(24) // HDHR //! .mods(24) // HDHR
//! .combo(1234) //! .combo(1234)
//! .misses(2) //! .accuracy(99.2)
//! .accuracy(99.2) // should be called last //! .n_misses(2)
//! .calculate(); //! .calculate();
//! //!
//! println!("PP: {}", result.pp()); //! println!("PP: {}", result.pp());
//! //!
//! // If you intend to reuse the current map-mod combination, //! // If you want to reuse the current map-mod combination, make use of the previous result!
//! // make use of the previous result!
//! // If attributes are given, then stars & co don't have to be recalculated. //! // If attributes are given, then stars & co don't have to be recalculated.
//! let next_result = map.pp() //! let next_result = map.pp()
//! .mods(24) // HDHR //! .mods(24) // HDHR
//! .attributes(result) // recycle //! .attributes(result) // recycle
//! .combo(543) //! .combo(543)
//! .misses(5) //! .n_misses(5)
//! .n50(3) //! .n50(3)
//! .accuracy(96.5) //! .accuracy(96.5)
//! .calculate(); //! .calculate();
@@ -68,7 +67,7 @@
//! let result = map.pp() //! let result = map.pp()
//! .mods(24) // HDHR //! .mods(24) // HDHR
//! .combo(1234) //! .combo(1234)
//! .misses(2) //! .n_misses(2)
//! .accuracy(99.2) //! .accuracy(99.2)
//! .calculate(); //! .calculate();
//! //!
@@ -114,12 +113,12 @@
//! // The default score state is kinda chunky because it considers all modes. //! // The default score state is kinda chunky because it considers all modes.
//! let state = ScoreState { //! let state = ScoreState {
//! max_combo: 1, //! max_combo: 1,
//! n_katu: 0, // only relevant for ctb //! n_geki: 0, // only relevant for mania
//! n_katu: 0, // only relevant for mania and ctb
//! n300: 1, //! n300: 1,
//! n100: 0, //! n100: 0,
//! n50: 0, //! n50: 0,
//! misses: 0, //! n_misses: 0,
//! score: 300, // only relevant for mania
//! }; //! };
//! //!
//! // Process the score state after the first object //! // Process the score state after the first object
@@ -145,7 +144,7 @@
//! max_combo: 11, //! max_combo: 11,
//! n300: 9, //! n300: 9,
//! n100: 1, //! n100: 1,
//! misses: 1, //! n_misses: 1,
//! }; //! };
//! //!
//! // Process the next 10 objects in one go //! // Process the next 10 objects in one go
@@ -362,23 +361,21 @@ impl DifficultyAttributes {
#[inline] #[inline]
pub fn stars(&self) -> f64 { pub fn stars(&self) -> f64 {
match self { match self {
Self::Catch(attributes) => attributes.stars, Self::Catch(attrs) => attrs.stars,
Self::Mania(attributes) => attributes.stars, Self::Mania(attrs) => attrs.stars,
Self::Osu(attributes) => attributes.stars, Self::Osu(attrs) => attrs.stars,
Self::Taiko(attributes) => attributes.stars, Self::Taiko(attrs) => attrs.stars,
} }
} }
/// The maximum combo of the map. /// The maximum combo of the map.
///
/// This will only be `None` for attributes of osu!mania maps.
#[inline] #[inline]
pub fn max_combo(&self) -> Option<usize> { pub fn max_combo(&self) -> usize {
match self { match self {
Self::Catch(attributes) => Some(attributes.max_combo()), Self::Catch(attrs) => attrs.max_combo(),
Self::Mania(_) => None, Self::Mania(attrs) => attrs.max_combo,
Self::Osu(attributes) => Some(attributes.max_combo), Self::Osu(attrs) => attrs.max_combo,
Self::Taiko(attributes) => Some(attributes.max_combo), Self::Taiko(attrs) => attrs.max_combo,
} }
} }
} }
@@ -429,10 +426,10 @@ impl PerformanceAttributes {
#[inline] #[inline]
pub fn pp(&self) -> f64 { pub fn pp(&self) -> f64 {
match self { match self {
Self::Catch(attributes) => attributes.pp, Self::Catch(attrs) => attrs.pp,
Self::Mania(attributes) => attributes.pp, Self::Mania(attrs) => attrs.pp,
Self::Osu(attributes) => attributes.pp, Self::Osu(attrs) => attrs.pp,
Self::Taiko(attributes) => attributes.pp, Self::Taiko(attrs) => attrs.pp,
} }
} }
@@ -440,10 +437,10 @@ impl PerformanceAttributes {
#[inline] #[inline]
pub fn stars(&self) -> f64 { pub fn stars(&self) -> f64 {
match self { match self {
Self::Catch(attributes) => attributes.stars(), Self::Catch(attrs) => attrs.stars(),
Self::Mania(attributes) => attributes.stars(), Self::Mania(attrs) => attrs.stars(),
Self::Osu(attributes) => attributes.stars(), Self::Osu(attrs) => attrs.stars(),
Self::Taiko(attributes) => attributes.stars(), Self::Taiko(attrs) => attrs.stars(),
} }
} }
@@ -451,23 +448,21 @@ impl PerformanceAttributes {
#[inline] #[inline]
pub fn difficulty_attributes(&self) -> DifficultyAttributes { pub fn difficulty_attributes(&self) -> DifficultyAttributes {
match self { match self {
Self::Catch(attributes) => DifficultyAttributes::Catch(attributes.difficulty.clone()), Self::Catch(attrs) => DifficultyAttributes::Catch(attrs.difficulty.clone()),
Self::Mania(attributes) => DifficultyAttributes::Mania(attributes.difficulty), Self::Mania(attrs) => DifficultyAttributes::Mania(attrs.difficulty),
Self::Osu(attributes) => DifficultyAttributes::Osu(attributes.difficulty.clone()), Self::Osu(attrs) => DifficultyAttributes::Osu(attrs.difficulty.clone()),
Self::Taiko(attributes) => DifficultyAttributes::Taiko(attributes.difficulty.clone()), Self::Taiko(attrs) => DifficultyAttributes::Taiko(attrs.difficulty.clone()),
} }
} }
#[inline] #[inline]
/// The maximum combo of the map. /// The maximum combo of the map.
/// pub fn max_combo(&self) -> usize {
/// This will only be `None` for attributes of osu!mania maps.
pub fn max_combo(&self) -> Option<usize> {
match self { match self {
Self::Catch(f) => Some(f.difficulty.max_combo()), Self::Catch(attrs) => attrs.difficulty.max_combo(),
Self::Mania(_) => None, Self::Mania(attrs) => attrs.difficulty.max_combo,
Self::Osu(o) => Some(o.difficulty.max_combo), Self::Osu(attrs) => attrs.difficulty.max_combo,
Self::Taiko(t) => Some(t.difficulty.max_combo), Self::Taiko(attrs) => attrs.difficulty.max_combo,
} }
} }
} }
@@ -476,10 +471,10 @@ impl From<PerformanceAttributes> for DifficultyAttributes {
#[inline] #[inline]
fn from(attributes: PerformanceAttributes) -> Self { fn from(attributes: PerformanceAttributes) -> Self {
match attributes { match attributes {
PerformanceAttributes::Catch(attributes) => Self::Catch(attributes.difficulty), PerformanceAttributes::Catch(attrs) => Self::Catch(attrs.difficulty),
PerformanceAttributes::Mania(attributes) => Self::Mania(attributes.difficulty), PerformanceAttributes::Mania(attrs) => Self::Mania(attrs.difficulty),
PerformanceAttributes::Osu(attributes) => Self::Osu(attributes.difficulty), PerformanceAttributes::Osu(attrs) => Self::Osu(attrs.difficulty),
PerformanceAttributes::Taiko(attributes) => Self::Taiko(attributes.difficulty), PerformanceAttributes::Taiko(attrs) => Self::Taiko(attrs.difficulty),
} }
} }
} }
@@ -521,7 +516,7 @@ mod tests {
#[test] #[test]
fn custom() { fn custom() {
let path = "F:/osu!/beatmaps/1186086.osu"; let path = "F:/osu!/beatmaps/1028484.osu";
let map = Beatmap::from_path(path).unwrap(); let map = Beatmap::from_path(path).unwrap();
let attrs = match OsuPP::new(&map).mode(GameMode::Taiko).mods(0).calculate() { let attrs = match OsuPP::new(&map).mode(GameMode::Taiko).mods(0).calculate() {
@@ -529,31 +524,33 @@ mod tests {
_ => unreachable!(), _ => unreachable!(),
}; };
println!( println!("{attrs:#?}");
"difficulty:\n\
stamina={}\n\ // println!(
rhythm={}\n\ // "difficulty:\n\
colour={}\n\ // stamina={}\n\
peak={}\n\ // rhythm={}\n\
hit_window={}\n\ // colour={}\n\
max_combo={}\n\ // peak={}\n\
stars={}\n\ // hit_window={}\n\
performance:\n\ // max_combo={}\n\
difficulty={}\n\ // stars={}\n\
acc={}\n\ // performance:\n\
effective={}\n\ // difficulty={}\n\
pp={}\n", // acc={}\n\
attrs.difficulty.stamina, // effective={}\n\
attrs.difficulty.rhythm, // pp={}\n",
attrs.difficulty.colour, // attrs.difficulty.stamina,
attrs.difficulty.peak, // attrs.difficulty.rhythm,
attrs.difficulty.hit_window, // attrs.difficulty.colour,
attrs.difficulty.max_combo, // attrs.difficulty.peak,
attrs.difficulty.stars, // attrs.difficulty.hit_window,
attrs.pp_difficulty, // attrs.difficulty.max_combo,
attrs.pp_acc, // attrs.difficulty.stars,
attrs.effective_miss_count, // attrs.pp_difficulty,
attrs.pp, // attrs.pp_acc,
); // attrs.effective_miss_count,
// attrs.pp,
// );
} }
} }
+44 -49
View File
@@ -1,4 +1,9 @@
use crate::{beatmap::BeatmapHitWindows, parse::HitObjectKind, util::FloatExt, Beatmap, Mods}; use crate::{
beatmap::BeatmapHitWindows,
parse::{HitObject, HitObjectKind},
util::FloatExt,
Beatmap, Mods,
};
use super::{ use super::{
difficulty_object::ManiaDifficultyObject, difficulty_object::ManiaDifficultyObject,
@@ -52,7 +57,6 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
/// Create a new difficulty attributes iterator for osu!mania maps. /// Create a new difficulty attributes iterator for osu!mania maps.
pub fn new(map: &'map Beatmap, mods: u32) -> Self { pub fn new(map: &'map Beatmap, mods: u32) -> Self {
let total_columns = map.cs.round_even().max(1.0); let total_columns = map.cs.round_even().max(1.0);
let clock_rate = mods.clock_rate(); let clock_rate = mods.clock_rate();
let strain = Strain::new(total_columns as usize); let strain = Strain::new(total_columns as usize);
@@ -82,6 +86,8 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
} }
}; };
let curr_combo = params.max_combo;
let diff_objects_iter = hit_objects.enumerate().scan(first, |last, (i, h)| { let diff_objects_iter = hit_objects.enumerate().scan(first, |last, (i, h)| {
let base = ManiaObject::new(h, total_columns, &mut params); let base = ManiaObject::new(h, total_columns, &mut params);
let diff_object = ManiaDifficultyObject::new(&base, &*last, clock_rate, i); let diff_object = ManiaDifficultyObject::new(&base, &*last, clock_rate, i);
@@ -90,17 +96,6 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
Some(diff_object) Some(diff_object)
}); });
let curr_combo = if let Some(h) = map.hit_objects.first() {
match &h.kind {
HitObjectKind::Hold { end_time } => {
1 + ((*end_time - h.start_time) / 100.0) as usize
}
_ => 1,
}
} else {
0
};
let mut diff_objects = Vec::with_capacity(map.hit_objects.len().saturating_sub(1)); let mut diff_objects = Vec::with_capacity(map.hit_objects.len().saturating_sub(1));
diff_objects.extend(diff_objects_iter); diff_objects.extend(diff_objects_iter);
@@ -114,6 +109,24 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
clock_rate, clock_rate,
} }
} }
fn increment_combo(
h: &HitObject,
diff_obj: &ManiaDifficultyObject,
curr_combo: &mut usize,
clock_rate: f64,
) {
match &h.kind {
HitObjectKind::Circle => *curr_combo += 1,
_ => {
let start_time = diff_obj.start_time * clock_rate;
let end_time = diff_obj.end_time * clock_rate;
let duration = end_time - start_time;
*curr_combo += 1 + (duration / 100.0) as usize;
}
}
}
} }
impl Iterator for ManiaGradualDifficultyAttributes<'_> { impl Iterator for ManiaGradualDifficultyAttributes<'_> {
@@ -124,16 +137,7 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
self.idx += 1; self.idx += 1;
if let Some(h) = self.map.hit_objects.get(self.idx) { if let Some(h) = self.map.hit_objects.get(self.idx) {
match &h.kind { Self::increment_combo(h, curr, &mut self.curr_combo, self.clock_rate);
HitObjectKind::Circle => self.curr_combo += 1,
_ => {
let start_time = curr.start_time * self.clock_rate;
let end_time = curr.end_time * self.clock_rate;
let duration = end_time - start_time;
self.curr_combo += 1 + (duration / 100.0) as usize;
}
}
} }
self.strain.process(curr, &self.diff_objects); self.strain.process(curr, &self.diff_objects);
@@ -151,6 +155,23 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
(len, Some(len)) (len, Some(len))
} }
fn nth(&mut self, n: usize) -> Option<Self::Item> {
let skip = n.min(self.len()).saturating_sub(1);
for _ in 0..skip {
let curr = self.diff_objects.get(self.idx)?;
self.idx += 1;
if let Some(h) = self.map.hit_objects.get(self.idx) {
Self::increment_combo(h, curr, &mut self.curr_combo, self.clock_rate);
}
self.strain.process(curr, &self.diff_objects);
}
self.next()
}
} }
impl ExactSizeIterator for ManiaGradualDifficultyAttributes<'_> { impl ExactSizeIterator for ManiaGradualDifficultyAttributes<'_> {
@@ -159,29 +180,3 @@ impl ExactSizeIterator for ManiaGradualDifficultyAttributes<'_> {
self.diff_objects.len() - self.idx self.diff_objects.len() - self.idx
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = ManiaGradualDifficultyAttributes::new(&map, 0);
assert!(attributes.next().is_none());
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn iter_end_eq_regular() {
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
let mods = 64;
let regular = crate::ManiaStars::new(&map).mods(mods).calculate();
let iter_end = ManiaGradualDifficultyAttributes::new(&map, mods)
.last()
.expect("empty iter");
assert_eq!(regular, iter_end);
}
}
+27 -141
View File
@@ -2,7 +2,10 @@ use crate::{Beatmap, ManiaPP};
use super::{ManiaGradualDifficultyAttributes, ManiaPerformanceAttributes}; use super::{ManiaGradualDifficultyAttributes, ManiaPerformanceAttributes};
/// TODO: docs /// Aggregation for a score's current state
/// i.e. what are the current hitresults.
///
/// This struct is used for [`ManiaGradualPerformanceAttributes`].
#[derive(Clone, Debug, Default, Eq, PartialEq)] #[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ManiaScoreState { pub struct ManiaScoreState {
/// Amount of current 320s. /// Amount of current 320s.
@@ -65,7 +68,7 @@ impl ManiaScoreState {
/// # Example /// # Example
/// ///
/// ``` /// ```
/// use rosu_pp::{Beatmap, mania::ManiaGradualPerformanceAttributes}; /// use rosu_pp::{Beatmap, mania::{ManiaGradualPerformanceAttributes, ManiaScoreState}};
/// ///
/// # /* /// # /*
/// let map: Beatmap = ... /// let map: Beatmap = ...
@@ -74,45 +77,51 @@ impl ManiaScoreState {
/// ///
/// let mods = 64; // DT /// let mods = 64; // DT
/// let mut gradual_perf = ManiaGradualPerformanceAttributes::new(&map, mods); /// let mut gradual_perf = ManiaGradualPerformanceAttributes::new(&map, mods);
/// let mut score = 0; /// let mut state = ManiaScoreState::new(); // empty state, everything is on 0.
/// ///
/// // The first 10 objects each increase the score by 123. /// // The first 10 hitresults are 320s
/// for _ in 0..10 { /// for _ in 0..10 {
/// score += 123; /// state.n320 += 1;
/// ///
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(score).unwrap(); /// let performance = gradual_perf.process_next_object(score).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_object(score); /// # let _ = gradual_perf.process_next_object(state.clone());
/// } /// }
/// ///
/// // Then comes a miss so no additional score is added. /// // Then comes a miss.
/// state.n_misses += 1;
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(score).unwrap(); /// let performance = gradual_perf.process_next_object(score).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_object(score); /// # let _ = gradual_perf.process_next_object(state.clone());
/// ///
/// // The next 10 objects give a total of 987 score and will be processed in one go. /// // The next 10 objects will be a mixture of 320s and 100s.
/// score += 987; /// // Notice how all 10 objects will be processed in one go.
/// state.n320 += 3;
/// state.n100 += 7;
/// # /* /// # /*
/// let performance = gradual_perf.process_next_n_objects(score, 10).unwrap(); /// let performance = gradual_perf.process_next_n_objects(score, 10).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_n_objects(score, 10); /// # let _ = gradual_perf.process_next_n_objects(state.clone(), 10);
/// ///
/// // Skip to the end /// // Skip to the end
/// # /* /// # /*
/// score = ... /// state.max_combo = ...
/// let final_performance = gradual_perf.process_next_n_objects(score, usize::MAX).unwrap(); /// state.n300 = ...
/// state.n100 = ...
/// state.n_misses = ...
/// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
/// # let _ = gradual_perf.process_next_n_objects(score, usize::MAX); /// # let _ = gradual_perf.process_next_n_objects(state.clone(), usize::MAX);
/// ///
/// // Once the final performance was calculated, /// // Once the final performance was calculated,
/// // attempting to process further objects will return `None`. /// // attempting to process further objects will return `None`.
/// assert!(gradual_perf.process_next_object(score).is_none()); /// assert!(gradual_perf.process_next_object(state).is_none());
/// ``` /// ```
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ManiaGradualPerformanceAttributes<'map> { pub struct ManiaGradualPerformanceAttributes<'map> {
@@ -152,140 +161,17 @@ impl<'map> ManiaGradualPerformanceAttributes<'map> {
state: ManiaScoreState, state: ManiaScoreState,
n: usize, n: usize,
) -> Option<ManiaPerformanceAttributes> { ) -> Option<ManiaPerformanceAttributes> {
let n = n.min(self.difficulty.len()).saturating_sub(1); let sub = (self.difficulty.idx == 0) as usize;
let difficulty = self.difficulty.nth(n)?; let difficulty = self.difficulty.nth(n.saturating_sub(sub))?;
self.performance.n320 = Some(state.n320);
self.performance.n300 = Some(state.n300);
self.performance.n200 = Some(state.n200);
self.performance.n100 = Some(state.n100);
self.performance.n50 = Some(state.n50);
self.performance.n_misses = Some(state.n_misses);
let performance = self let performance = self
.performance .performance
.clone() .clone()
.attributes(difficulty) .attributes(difficulty)
.state(state)
.passed_objects(self.difficulty.idx) .passed_objects(self.difficulty.idx)
.calculate(); .calculate();
Some(performance) Some(performance)
} }
} }
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn correct_empty() {
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
let mods = 64;
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, mods);
let state = ManiaScoreState {
n320: 0,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
assert!(gradual
.process_next_n_objects(state.clone(), usize::MAX)
.is_some());
assert!(gradual.process_next_object(state).is_none());
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn next_and_next_n() {
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
let mods = 64;
let mut state = ManiaScoreState {
n320: 0,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let mut gradual1 = ManiaGradualPerformanceAttributes::new(&map, mods);
let mut gradual2 = ManiaGradualPerformanceAttributes::new(&map, mods);
for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
state.n320 += 1;
}
let n = 80;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
state.n320 += 1;
}
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
assert_eq!(next_n, next);
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn gradual_end_eq_regular() {
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
let mods = 64;
let regular = ManiaPP::new(&map).mods(mods).calculate();
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, mods);
let state = ManiaScoreState {
n320: 3238,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end);
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn gradual_eq_regular_passed() {
let map = Beatmap::from_path("./maps/1974394.osu").expect("failed to parse map");
let mods = 64;
let n = 100;
let state = ManiaScoreState {
n320: 100,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let regular = ManiaPP::new(&map)
.mods(mods)
.passed_objects(n)
.state(state.clone())
.calculate();
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, mods);
let gradual = gradual.process_next_n_objects(state, n).unwrap();
assert_eq!(regular, gradual);
}
}
+7 -1
View File
@@ -175,7 +175,7 @@ fn calculate_result(params: ManiaStars<'_>) -> ManiaResult {
let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate()); let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate());
let mut strain = Strain::new(total_columns as usize); let mut strain = Strain::new(total_columns as usize);
let mut params = ObjectParameters::new(map.as_ref()); let mut params = ObjectParameters::new(map.as_ref());
let mut hit_objects = map.hit_objects.iter(); let mut hit_objects = map.hit_objects.iter().take(take);
let first = match hit_objects.next() { let first = match hit_objects.next() {
Some(h) => ManiaObject::new(h, total_columns, &mut params), Some(h) => ManiaObject::new(h, total_columns, &mut params),
@@ -247,6 +247,12 @@ impl ManiaPerformanceAttributes {
pub fn pp(&self) -> f64 { pub fn pp(&self) -> f64 {
self.pp self.pp
} }
/// Return the maximum combo of the map.
#[inline]
pub fn max_combo(&self) -> usize {
self.difficulty.max_combo
}
} }
impl From<ManiaPerformanceAttributes> for ManiaDifficultyAttributes { impl From<ManiaPerformanceAttributes> for ManiaDifficultyAttributes {
+647 -72
View File
@@ -20,15 +20,21 @@ use crate::{
/// ///
/// let pp_result = ManiaPP::new(&map) /// let pp_result = ManiaPP::new(&map)
/// .mods(64) // DT /// .mods(64) // DT
/// .score(765_432) /// .n_misses(1)
/// .accuracy(98.5)
/// .calculate(); /// .calculate();
/// ///
/// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars()); /// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars());
/// ///
/// let next_result = ManiaPP::new(&map) /// let next_result = ManiaPP::new(&map)
/// .attributes(pp_result) // reusing previous results for performance /// .attributes(pp_result) // reusing previous results for performance
/// .mods(8 + 64) // has to be the same to reuse attributes /// .mods(8 + 64) // has to be the same to reuse attributes
/// .score(950_000) /// .n320(2000)
/// .n300(500)
/// .n200(200)
/// .n100(100)
/// .n50(10)
/// .n_misses(1)
/// .calculate(); /// .calculate();
/// ///
/// println!("PP: {} | Stars: {}", next_result.pp(), next_result.stars()); /// println!("PP: {} | Stars: {}", next_result.pp(), next_result.stars());
@@ -122,7 +128,7 @@ impl<'map> ManiaPP<'map> {
/// This will be used to generate matching hitresults. /// This will be used to generate matching hitresults.
#[inline] #[inline]
pub fn accuracy(mut self, acc: f64) -> Self { pub fn accuracy(mut self, acc: f64) -> Self {
self.acc = Some(acc); self.acc = Some(acc / 100.0);
self self
} }
@@ -238,110 +244,258 @@ impl<'map> ManiaPP<'map> {
let n_objects = self.passed_objects.unwrap_or(self.map.hit_objects.len()); let n_objects = self.passed_objects.unwrap_or(self.map.hit_objects.len());
let priority = self.hitresult_priority.unwrap_or_default(); let priority = self.hitresult_priority.unwrap_or_default();
let mut state = ManiaScoreState { let mut n320 = self.n320.unwrap_or(0);
n320: self.n320.unwrap_or(0), let mut n300 = self.n300.unwrap_or(0);
n300: self.n300.unwrap_or(0), let mut n200 = self.n200.unwrap_or(0);
n200: self.n200.unwrap_or(0), let mut n100 = self.n100.unwrap_or(0);
n100: self.n100.unwrap_or(0), let mut n50 = self.n50.unwrap_or(0);
n50: self.n50.unwrap_or(0), let n_misses = self.n_misses.unwrap_or(0);
n_misses: self.n_misses.unwrap_or(0),
};
if let Some(acc) = self.acc { if let Some(acc) = self.acc {
// TODO: test // TODO: test
let target_total = (acc * (n_objects * 6) as f64).round() as usize; let target_total = (acc * (n_objects * 6) as f64).round() as usize;
let mut delta = target_total.saturating_sub(n_objects.saturating_sub(state.n_misses)); match (self.n320, self.n300, self.n200, self.n100, self.n50) {
(Some(_), Some(_), Some(_), Some(_), Some(_)) => {
let remaining =
n_objects.saturating_sub(n320 + n300 + n200 + n100 + n50 + n_misses);
if self.n50.is_some() { match priority {
delta /= 2; HitResultPriority::BestCase => n320 += remaining,
} HitResultPriority::WorstCase => n50 += remaining,
}
if self.n100.is_some() {
delta /= 2;
}
if let Some(n320) = self.n320 {
delta = delta.saturating_sub(n320 * 6);
} else {
state.n320 = delta / 5;
}
if self.n100.is_none() {
state.n100 = delta % 5;
}
state.n50 += n_objects.saturating_sub(state.total_hits() - state.n50);
if let HitResultPriority::BestCase = priority {
// Shift n50 to n200
if self.n320.or(self.n300).or(self.n200).or(self.n50).is_none() {
let n = (state.n320 + state.n300).min(state.n50 / 2);
if n <= state.n300 {
state.n300 -= n;
} else {
state.n320 -= n - state.n300;
state.n300 = 0;
};
state.n200 += 2 * n;
state.n50 -= n;
} }
(Some(_), None, Some(_), Some(_), Some(_)) => {
n300 = n_objects.saturating_sub(n320 + n200 + n100 + n50 + n_misses)
}
(None, Some(_), Some(_), Some(_), Some(_)) => {
n320 = n_objects.saturating_sub(n300 + n200 + n100 + n50 + n_misses)
}
(Some(_), _, Some(_), Some(_), None) | (_, Some(_), Some(_), Some(_), None) => {
n50 = n_objects.saturating_sub(n320 + n300 + n200 + n100 + n_misses);
}
(Some(_), _, _, None, None) | (_, Some(_), _, None, None) => {
let n3x0 = n320 + n300;
let delta = (target_total - n_objects.saturating_sub(n_misses))
.saturating_sub(n3x0 * 5 + n200 * 3);
// Shift n50 to n100 n100 = delta % 5;
if self.n320.or(self.n300).or(self.n100).or(self.n50).is_none() { n50 = n_objects.saturating_sub(n3x0 + n200 + n100 + n_misses);
let n = (state.n320 + state.n300).min(state.n50 / 4);
if n <= state.n300 { let curr_total = 6 * n3x0 + 4 * n200 + 2 * n100 + n50;
state.n300 -= n;
if curr_total < target_total {
let n = (target_total - curr_total).min(n50);
n50 -= n;
n100 += n;
} else { } else {
state.n320 -= n - state.n300; let n = (curr_total - target_total).min(n100);
state.n300 = 0; n100 -= n;
}; n50 += n;
}
}
(Some(_), _, None, Some(_), None) | (_, Some(_), None, Some(_), None) => {
let n3x0 = n320 + n300;
let delta = (target_total - n_objects.saturating_sub(n_misses))
.saturating_sub(n3x0 * 5 + n100);
state.n100 += 5 * n; n200 = delta / 3;
state.n50 -= 4 * n; n50 = n_objects.saturating_sub(n3x0 + n200 + n100 + n_misses);
}
(Some(_), _, None, None, Some(_)) | (_, Some(_), None, None, Some(_)) => {
let remaining = n_objects.saturating_sub(n320 + n300 + n50 + n_misses);
match priority {
HitResultPriority::BestCase => n100 = remaining,
HitResultPriority::WorstCase => n200 = remaining,
}
}
(Some(_), _, None, Some(_), Some(_)) | (_, Some(_), None, Some(_), Some(_)) => {
n200 = n_objects.saturating_sub(n320 + n300 + n100 + n50 + n_misses);
}
(Some(_), _, Some(_), None, Some(_)) | (_, Some(_), Some(_), None, Some(_)) => {
n100 = n_objects.saturating_sub(n320 + n300 + n200 + n50 + n_misses);
}
(None, None, Some(_), Some(_), Some(_)) => {
let remaining = n_objects.saturating_sub(n200 + n100 + n50 + n_misses);
match priority {
HitResultPriority::BestCase => n320 = remaining,
HitResultPriority::WorstCase => n300 = remaining,
}
}
(None, None, None, Some(_), Some(_)) => {
let delta =
(target_total - n_objects.saturating_sub(n_misses)).saturating_sub(n100);
match priority {
HitResultPriority::BestCase => n320 = delta / 5,
HitResultPriority::WorstCase => n300 = delta / 5,
}
n200 = n_objects.saturating_sub(n320 + n100 + n50 + n_misses);
let curr_total = 6 * (n320 + n300) + 4 * n200 + 2 * n100 + n50;
if curr_total < target_total {
let n = n200.min((target_total - curr_total) / 2);
n200 -= n;
match priority {
HitResultPriority::BestCase => n320 += n,
HitResultPriority::WorstCase => n300 += n,
}
} else {
let n = (n320 + n300).min((curr_total - target_total) / 2);
n200 += n;
match priority {
HitResultPriority::BestCase => n320 -= n,
HitResultPriority::WorstCase => n300 -= n,
}
}
}
(None, None, Some(_), None, None) => {
let delta = (target_total - n_objects.saturating_sub(n_misses))
.saturating_sub(n200 * 3);
match priority {
HitResultPriority::BestCase => n320 = delta / 5,
HitResultPriority::WorstCase => n300 = delta / 5,
}
n100 = delta % 5;
n50 = n_objects.saturating_sub(n320 + n200 + n100 + n_misses);
let curr_total = 6 * (n320 + n300) + 4 * n200 * 2 * n100 + n50;
if curr_total < target_total {
let n = (target_total - curr_total).min(n50);
n50 -= n;
n100 += n;
} else {
let n = (curr_total - target_total).min(n100);
n100 -= n;
n50 += n;
}
if let HitResultPriority::BestCase = priority {
// Shift n50 to n100
let n = n320.min(n50 / 4);
n320 -= n;
n100 += 5 * n;
n50 -= 4 * n;
}
}
(None, None, _, Some(_), None) => {
let delta = (target_total - n_objects.saturating_sub(n_misses))
.saturating_sub(n200 * 3 + n100);
match priority {
HitResultPriority::BestCase => n320 = delta / 5,
HitResultPriority::WorstCase => n300 = delta / 5,
}
n50 = n_objects.saturating_sub(n320 + n300 + n200 + n100 + n_misses);
}
(None, None, _, None, Some(_)) => {
let delta =
target_total - n_objects.saturating_sub(n_misses).saturating_sub(n200 * 3);
match priority {
HitResultPriority::BestCase => n320 = delta / 5,
HitResultPriority::WorstCase => n300 = delta / 5,
}
n100 = delta % 5;
n100 += n_objects.saturating_sub(n320 + n300 + n200 + n100 + n50 + n_misses);
let curr_total = 6 * (n320 + n300) + 4 * n200 + 2 * n100 + n50;
if curr_total < target_total {
let n = n100.min((target_total - curr_total) / 4);
n100 -= n;
match priority {
HitResultPriority::BestCase => n320 += n,
HitResultPriority::WorstCase => n300 += n,
}
} else {
let n = (n320 + n300).min((curr_total - target_total) / 4);
n100 += n;
match priority {
HitResultPriority::BestCase => n320 -= n,
HitResultPriority::WorstCase => n300 -= n,
}
}
}
(None, None, None, None, None) => {
let delta = target_total - n_objects.saturating_sub(n_misses);
match priority {
HitResultPriority::BestCase => n320 = delta / 5,
HitResultPriority::WorstCase => n300 = delta / 5,
}
n100 = delta % 5;
n50 = n_objects.saturating_sub(n320 + n300 + n100 + n_misses);
if let HitResultPriority::BestCase = priority {
// Shift n50 to n100
let n = n320.min(n50 / 4);
n320 -= n;
n100 += 5 * n;
n50 -= 4 * n;
}
} }
} }
} else { } else {
let remaining = n_objects.saturating_sub(state.total_hits()); let remaining = n_objects.saturating_sub(n320 + n300 + n200 + n100 + n50 + n_misses);
match priority { match priority {
HitResultPriority::BestCase => { HitResultPriority::BestCase => {
if self.n320.is_none() { if self.n320.is_none() {
state.n320 = remaining; n320 = remaining;
} else if self.n300.is_none() { } else if self.n300.is_none() {
state.n300 = remaining; n300 = remaining;
} else if self.n200.is_none() { } else if self.n200.is_none() {
state.n200 = remaining; n200 = remaining;
} else if self.n100.is_none() { } else if self.n100.is_none() {
state.n100 = remaining; n100 = remaining;
} else if self.n50.is_none() { } else if self.n50.is_none() {
state.n50 = remaining; n50 = remaining;
} else { } else {
state.n320 = remaining; n320 += remaining;
} }
} }
HitResultPriority::WorstCase => { HitResultPriority::WorstCase => {
if self.n50.is_none() { if self.n50.is_none() {
state.n50 = remaining; n50 = remaining;
} else if self.n100.is_none() { } else if self.n100.is_none() {
state.n100 = remaining; n100 = remaining;
} else if self.n200.is_none() { } else if self.n200.is_none() {
state.n200 = remaining; n200 = remaining;
} else if self.n300.is_none() { } else if self.n300.is_none() {
state.n300 = remaining; n300 = remaining;
} else if self.n320.is_none() { } else if self.n320.is_none() {
state.n320 = remaining; n320 = remaining;
} else { } else {
state.n50 = remaining; n50 += remaining;
} }
} }
} }
} }
state ManiaScoreState {
n320,
n300,
n200,
n100,
n50,
n_misses,
}
} }
} }
@@ -488,3 +642,424 @@ impl ManiaAttributeProvider for PerformanceAttributes {
} }
} }
} }
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
#[cfg(test)]
mod tests {
use super::*;
use crate::Beatmap;
fn test_data() -> (Beatmap, ManiaDifficultyAttributes) {
let path = "./maps/1974394.osu";
let map = Beatmap::from_path(path).unwrap();
let attrs = ManiaDifficultyAttributes {
stars: 4.824631127426499,
hit_window: 40.0,
max_combo: 5064,
};
(map, attrs)
}
#[test]
fn hitresults_acc_n320_n200_n_misses_best() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n320(2600)
.n200(400)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2600,
n300: 0,
n200: 400,
n100: 49,
n50: 187,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_acc_n320_n300_n200_n_misses_best() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n320(2250)
.n300(500)
.n200(100)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2250,
n300: 500,
n200: 100,
n100: 199,
n50: 187,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_acc_n320_n300_n100_n_misses_best() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n320(2000)
.n300(500)
.n100(100)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2000,
n300: 500,
n200: 549,
n100: 100,
n50: 87,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_acc_n320_n100_n50_n_misses_best() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n320(2700)
.n100(200)
.n50(10)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2700,
n300: 0,
n200: 326,
n100: 200,
n50: 10,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_acc_n320_n50_n_misses_worst() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n320(2000)
.n50(50)
.n_misses(2)
.hitresult_priority(HitResultPriority::WorstCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2000,
n300: 0,
n200: 1186,
n100: 0,
n50: 50,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_acc_n100_n50_n_misses_best() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n100(200)
.n50(50)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2546,
n300: 0,
n200: 440,
n100: 200,
n50: 50,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_acc_n200_n_misses_best() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n200(500)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2503,
n300: 0,
n200: 500,
n100: 230,
n50: 3,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_acc_n200_n100_n_misses_best() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n200(500)
.n100(200)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2509,
n300: 0,
n200: 500,
n100: 200,
n50: 27,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_acc_n50_n_misses_best() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n50(200)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2804,
n300: 0,
n200: 0,
n100: 232,
n50: 200,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_acc_n200_n100_n50_n_misses_best() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n200(500)
.n100(300)
.n50(100)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2336,
n300: 0,
n200: 500,
n100: 300,
n50: 100,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_acc_n_misses_worst() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.accuracy(90.0)
.n_misses(2)
.hitresult_priority(HitResultPriority::WorstCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 0,
n300: 2849,
n200: 0,
n100: 4,
n50: 383,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_n320_n_misses_best() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.n320(2000)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 2000,
n300: 1236,
n200: 0,
n100: 0,
n50: 0,
n_misses: 2,
};
assert_eq!(state, expected);
assert_eq!(state.total_hits(), 3238);
}
#[test]
fn hitresults_n100_n50_n_misses_worst() {
let (map, attrs) = test_data();
let state = ManiaPP::new(&map)
.attributes(attrs)
.n100(500)
.n50(100)
.n_misses(2)
.hitresult_priority(HitResultPriority::WorstCase)
.generate_hitresults();
let expected = ManiaScoreState {
n320: 0,
n300: 0,
n200: 2636,
n100: 500,
n50: 100,
n_misses: 2,
};
assert_eq!(state, expected);
assert_eq!(state.total_hits(), 3238);
}
}
+45 -47
View File
@@ -49,7 +49,9 @@ use super::{
pub struct OsuGradualDifficultyAttributes { pub struct OsuGradualDifficultyAttributes {
pub(crate) idx: usize, pub(crate) idx: usize,
mods: u32, mods: u32,
attributes: OsuDifficultyAttributes, attrs: OsuDifficultyAttributes,
// Unused but `diff_objects`' lifetimes secretly depend on it
#[allow(unused)]
hit_objects: Vec<OsuObject>, hit_objects: Vec<OsuObject>,
diff_objects: Vec<OsuDifficultyObject<'static>>, diff_objects: Vec<OsuDifficultyObject<'static>>,
skills: [Box<dyn Skill>; 4], skills: [Box<dyn Skill>; 4],
@@ -60,8 +62,8 @@ impl Debug for OsuGradualDifficultyAttributes {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.debug_struct("OsuGradualDifficultyAttributes") f.debug_struct("OsuGradualDifficultyAttributes")
.field("idx", &self.idx) .field("idx", &self.idx)
.field("attributes", &self.attributes) .field("attrs", &self.attrs)
.field("hit_objects", &self.hit_objects) .field("diff_objects", &self.diff_objects)
.field("skills", &"<cannot be displayed>") .field("skills", &"<cannot be displayed>")
.finish() .finish()
} }
@@ -75,7 +77,7 @@ impl OsuGradualDifficultyAttributes {
let scaling_factor = ScalingFactor::new(map_attrs.cs); let scaling_factor = ScalingFactor::new(map_attrs.cs);
let hr = mods.hr(); let hr = mods.hr();
let hit_window = 2.0 * map_attrs.hit_windows.od; let hit_window = 2.0 * map_attrs.hit_windows.od;
let time_preempt = map_attrs.hit_windows.ar; let time_preempt = (map_attrs.hit_windows.ar * clock_rate) as f32 as f64;
// * Preempt time can go below 450ms. Normally, this is achieved via the DT mod // * Preempt time can go below 450ms. Normally, this is achieved via the DT mod
// * which uniformly speeds up all animations game wide regardless of AR. // * which uniformly speeds up all animations game wide regardless of AR.
@@ -100,7 +102,7 @@ impl OsuGradualDifficultyAttributes {
let mut params = ObjectParameters { let mut params = ObjectParameters {
map, map,
attributes: &mut attrs, attrs: &mut attrs,
ticks: Vec::new(), ticks: Vec::new(),
curve_bufs: CurveBuffers::default(), curve_bufs: CurveBuffers::default(),
}; };
@@ -140,7 +142,7 @@ impl OsuGradualDifficultyAttributes {
return Self { return Self {
idx: 0, idx: 0,
mods, mods,
attributes: attrs, attrs,
hit_objects: Vec::new(), hit_objects: Vec::new(),
diff_objects: Vec::new(), diff_objects: Vec::new(),
skills, skills,
@@ -149,6 +151,8 @@ impl OsuGradualDifficultyAttributes {
} }
}; };
Self::increment_combo(last, &mut attrs);
let mut last_last = None; let mut last_last = None;
// Prepare `lazy_travel_dist` and `lazy_end_pos` for `last` manually // Prepare `lazy_travel_dist` and `lazy_end_pos` for `last` manually
@@ -182,13 +186,26 @@ impl OsuGradualDifficultyAttributes {
Self { Self {
idx: 0, idx: 0,
mods, mods,
attributes: attrs, attrs,
diff_objects: extend_lifetime(diff_objects), diff_objects: extend_lifetime(diff_objects),
hit_objects, hit_objects,
skills, skills,
hit_window, hit_window,
} }
} }
fn increment_combo(h: &OsuObject, attrs: &mut OsuDifficultyAttributes) {
attrs.max_combo += 1;
match &h.kind {
OsuObjectKind::Circle => attrs.n_circles += 1,
OsuObjectKind::Slider(slider) => {
attrs.n_sliders += 1;
attrs.max_combo += slider.nested_len();
}
OsuObjectKind::Spinner { .. } => attrs.n_spinners += 1,
}
}
} }
fn extend_lifetime( fn extend_lifetime(
@@ -210,18 +227,7 @@ impl Iterator for OsuGradualDifficultyAttributes {
skill.process(curr, &self.diff_objects, self.hit_window); skill.process(curr, &self.diff_objects, self.hit_window);
} }
let mut attrs = self.attributes.clone(); Self::increment_combo(curr.base, &mut self.attrs);
attrs.max_combo += 1;
match &curr.base.kind {
OsuObjectKind::Circle => attrs.n_circles += 1,
OsuObjectKind::Slider(slider) => {
attrs.n_sliders += 1;
attrs.max_combo += slider.nested_len();
}
OsuObjectKind::Spinner { .. } => attrs.n_spinners += 1,
}
let [aim, aim_no_sliders, speed, flashlight] = &self.skills; let [aim, aim_no_sliders, speed, flashlight] = &self.skills;
@@ -289,6 +295,7 @@ impl Iterator for OsuGradualDifficultyAttributes {
0.0 0.0
}; };
let mut attrs = self.attrs.clone();
attrs.aim = aim_rating; attrs.aim = aim_rating;
attrs.speed = speed_rating; attrs.speed = speed_rating;
attrs.flashlight = flashlight_rating; attrs.flashlight = flashlight_rating;
@@ -301,41 +308,32 @@ impl Iterator for OsuGradualDifficultyAttributes {
#[inline] #[inline]
fn size_hint(&self) -> (usize, Option<usize>) { fn size_hint(&self) -> (usize, Option<usize>) {
let len = self.hit_objects.len() - self.idx; let len = self.len();
(len, Some(len)) (len, Some(len))
} }
fn nth(&mut self, n: usize) -> Option<Self::Item> {
let skip = n.min(self.len()).saturating_sub(1);
for _ in 0..skip {
let curr = self.diff_objects.get(self.idx)?;
self.idx += 1;
for skill in self.skills.iter_mut() {
skill.process(curr, &self.diff_objects, self.hit_window);
}
Self::increment_combo(curr.base, &mut self.attrs);
}
self.next()
}
} }
impl ExactSizeIterator for OsuGradualDifficultyAttributes { impl ExactSizeIterator for OsuGradualDifficultyAttributes {
#[inline] #[inline]
fn len(&self) -> usize { fn len(&self) -> usize {
self.hit_objects.len() self.diff_objects.len() - self.idx
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = OsuGradualDifficultyAttributes::new(&map, 0);
assert!(attributes.next().is_none());
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn iter_end_eq_regular() {
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
let mods = 64;
let regular = crate::OsuStars::new(&map).mods(mods).calculate();
let iter_end = OsuGradualDifficultyAttributes::new(&map, mods)
.last()
.expect("empty iter");
assert_eq!(regular, iter_end);
} }
} }
+5 -106
View File
@@ -93,7 +93,7 @@ impl OsuScoreState {
/// // Then comes a miss. /// // Then comes a miss.
/// // Note that state's max combo won't be incremented for /// // Note that state's max combo won't be incremented for
/// // the next few objects because the combo is reset. /// // the next few objects because the combo is reset.
/// state.misses += 1; /// state.n_misses += 1;
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.process_next_object(state.clone()).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
@@ -126,7 +126,7 @@ impl OsuScoreState {
/// state.n300 = ... /// state.n300 = ...
/// state.n100 = ... /// state.n100 = ...
/// state.n50 = ... /// state.n50 = ...
/// state.misses = ... /// state.n_misses = ...
/// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap(); /// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
@@ -174,118 +174,17 @@ impl<'map> OsuGradualPerformanceAttributes<'map> {
state: OsuScoreState, state: OsuScoreState,
n: usize, n: usize,
) -> Option<OsuPerformanceAttributes> { ) -> Option<OsuPerformanceAttributes> {
let n = n.min(self.difficulty.len()).saturating_sub(1); let sub = (self.difficulty.idx == 0) as usize;
let difficulty = self.difficulty.nth(n)?; let difficulty = self.difficulty.nth(n.saturating_sub(sub))?;
let performance = self let performance = self
.performance .performance
.clone() .clone()
.attributes(difficulty) .attributes(difficulty)
.state(state) .state(state)
.passed_objects(self.difficulty.idx) .passed_objects(self.difficulty.idx + 1)
.calculate(); .calculate();
Some(performance) Some(performance)
} }
} }
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn correct_empty() {
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
let mods = 64;
let mut gradual = OsuGradualPerformanceAttributes::new(&map, mods);
let state = OsuScoreState::default();
assert!(gradual
.process_next_n_objects(state.clone(), usize::MAX)
.is_some());
assert!(gradual.process_next_object(state).is_none());
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn next_and_next_n() {
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
let mods = 64;
let state = OsuScoreState::default();
let mut gradual1 = OsuGradualPerformanceAttributes::new(&map, mods);
let mut gradual2 = OsuGradualPerformanceAttributes::new(&map, mods);
for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
}
let n = 80;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
}
let state = OsuScoreState {
max_combo: 122,
n300: 88,
n100: 8,
n50: 2,
n_misses: 2,
};
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
assert_eq!(next_n, next);
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn gradual_end_eq_regular() {
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
let mods = 64;
let regular = OsuPP::new(&map).mods(mods).calculate();
let mut gradual = OsuGradualPerformanceAttributes::new(&map, mods);
let state = OsuScoreState {
max_combo: 909,
n300: 601,
n100: 0,
n50: 0,
n_misses: 0,
};
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end);
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn gradual_eq_regular_passed() {
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
let mods = 64;
let n = 100;
let regular = OsuPP::new(&map).mods(mods).passed_objects(n).calculate();
let mut gradual = OsuGradualPerformanceAttributes::new(&map, mods);
let state = OsuScoreState {
max_combo: 122,
n300: 100,
n100: 0,
n50: 0,
n_misses: 0,
};
let gradual = gradual.process_next_n_objects(state, n).unwrap();
assert_eq!(regular, gradual);
}
}
+4 -4
View File
@@ -260,7 +260,7 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
400.0 * (time_preempt / PREEMPT_MIN).min(1.0) 400.0 * (time_preempt / PREEMPT_MIN).min(1.0)
}; };
let mut attributes = OsuDifficultyAttributes { let mut attrs = OsuDifficultyAttributes {
ar: map_attrs.ar, ar: map_attrs.ar,
hp: map_attrs.hp, hp: map_attrs.hp,
od: map_attrs.od, od: map_attrs.od,
@@ -269,7 +269,7 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
let mut params = ObjectParameters { let mut params = ObjectParameters {
map, map,
attributes: &mut attributes, attrs: &mut attrs,
ticks: Vec::new(), ticks: Vec::new(),
curve_bufs: CurveBuffers::default(), curve_bufs: CurveBuffers::default(),
}; };
@@ -301,7 +301,7 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
let last = match hit_objects.next() { let last = match hit_objects.next() {
Some(prev) => prev, Some(prev) => prev,
None => return (skills, attributes), None => return (skills, attrs),
}; };
let mut last_last = None; let mut last_last = None;
@@ -340,7 +340,7 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
} }
} }
(skills, attributes) (skills, attrs)
} }
fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) { fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
+7 -7
View File
@@ -86,7 +86,7 @@ pub(crate) enum NestedObjectKind {
pub(crate) struct ObjectParameters<'a> { pub(crate) struct ObjectParameters<'a> {
pub(crate) map: &'a Beatmap, pub(crate) map: &'a Beatmap,
pub(crate) attributes: &'a mut OsuDifficultyAttributes, pub(crate) attrs: &'a mut OsuDifficultyAttributes,
pub(crate) ticks: Vec<(Pos2, f64)>, pub(crate) ticks: Vec<(Pos2, f64)>,
pub(crate) curve_bufs: CurveBuffers, pub(crate) curve_bufs: CurveBuffers,
} }
@@ -95,17 +95,17 @@ impl OsuObject {
pub(crate) fn new(h: &HitObject, params: &mut ObjectParameters<'_>) -> Option<Self> { pub(crate) fn new(h: &HitObject, params: &mut ObjectParameters<'_>) -> Option<Self> {
let ObjectParameters { let ObjectParameters {
map, map,
attributes, attrs,
ticks, ticks,
curve_bufs, curve_bufs,
} = params; } = params;
attributes.max_combo += 1; // hitcircle, slider head, or spinner attrs.max_combo += 1; // hitcircle, slider head, or spinner
let pos = h.pos; let pos = h.pos;
let obj = match &h.kind { let obj = match &h.kind {
HitObjectKind::Circle => { HitObjectKind::Circle => {
attributes.n_circles += 1; attrs.n_circles += 1;
Self { Self {
start_time: h.start_time, start_time: h.start_time,
@@ -121,7 +121,7 @@ impl OsuObject {
control_points, control_points,
.. ..
} => { } => {
attributes.n_sliders += 1; attrs.n_sliders += 1;
let timing_point = map.timing_point_at(h.start_time); let timing_point = map.timing_point_at(h.start_time);
let difficulty_point = map.difficulty_point_at(h.start_time).unwrap_or_default(); let difficulty_point = map.difficulty_point_at(h.start_time).unwrap_or_default();
@@ -283,7 +283,7 @@ impl OsuObject {
_ => nested_objects.push(legacy_last_tick), _ => nested_objects.push(legacy_last_tick),
}; };
attributes.max_combo += nested_objects.len(); attrs.max_combo += nested_objects.len();
let last_time = nested_objects let last_time = nested_objects
.last() .last()
@@ -318,7 +318,7 @@ impl OsuObject {
} }
} }
HitObjectKind::Spinner { end_time } => { HitObjectKind::Spinner { end_time } => {
attributes.n_spinners += 1; attrs.n_spinners += 1;
Self { Self {
start_time: h.start_time, start_time: h.start_time,
+29 -49
View File
@@ -21,15 +21,15 @@ use crate::{
/// let pp_result = OsuPP::new(&map) /// let pp_result = OsuPP::new(&map)
/// .mods(8 + 64) // HDDT /// .mods(8 + 64) // HDDT
/// .combo(1234) /// .combo(1234)
/// .misses(1) /// .n_misses(1)
/// .accuracy(98.5) // should be set last /// .accuracy(98.5) // should be set last
/// .calculate(); /// .calculate();
/// ///
/// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars()); /// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars());
/// ///
/// let next_result = OsuPP::new(&map) /// let next_result = OsuPP::new(&map)
/// .attributes(pp_result) // reusing previous results for performance /// .attributes(pp_result) // reusing previous results for performance
/// .mods(8 + 64) // has to be the same to reuse attributes /// .mods(8 + 64) // has to be the same to reuse attributes
/// .accuracy(99.5) /// .accuracy(99.5)
/// .calculate(); /// .calculate();
/// ///
@@ -739,13 +739,17 @@ impl OsuAttributeProvider for PerformanceAttributes {
} }
} }
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::Beatmap; use crate::Beatmap;
fn test_attrs() -> OsuDifficultyAttributes { fn test_data() -> (Beatmap, OsuDifficultyAttributes) {
OsuDifficultyAttributes { let path = "./maps/2785319.osu";
let map = Beatmap::from_path(path).unwrap();
let attrs = OsuDifficultyAttributes {
aim: 2.8693628443424104, aim: 2.8693628443424104,
speed: 2.533869745015772, speed: 2.533869745015772,
flashlight: 2.288770487900865, flashlight: 2.288770487900865,
@@ -759,38 +763,14 @@ mod test {
n_spinners: 1, n_spinners: 1,
stars: 5.669858729379631, stars: 5.669858729379631,
max_combo: 909, max_combo: 909,
} };
}
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))] (map, attrs)
fn test_map() -> (Beatmap, OsuDifficultyAttributes) {
let path = "./maps/2785319.osu";
let map = Beatmap::from_path(path).unwrap();
(map, test_attrs())
}
#[cfg(any(feature = "async_tokio", feature = "async_str"))]
async fn test_map() -> (Beatmap, OsuDifficultyAttributes) {
let path = "./maps/2785319.osu";
let map = Beatmap::from_path(path).await.unwrap();
(map, test_attrs())
}
#[rustfmt::skip]
macro_rules! test_data {
() => {{
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
{ test_map() }
#[cfg(any(feature = "async_tokio", feature = "async_str"))]
{ test_map().await }
}};
} }
#[test] #[test]
fn osu_hitresults_n300_n100_n_misses_best() { fn hitresults_n300_n100_n_misses_best() {
let (map, attrs) = test_data!(); let (map, attrs) = test_data();
let max_combo = attrs.max_combo(); let max_combo = attrs.max_combo();
let state = OsuPP::new(&map) let state = OsuPP::new(&map)
@@ -814,8 +794,8 @@ mod test {
} }
#[test] #[test]
fn osu_hitresults_n300_n50_n_misses_best() { fn hitresults_n300_n50_n_misses_best() {
let (map, attrs) = test_data!(); let (map, attrs) = test_data();
let max_combo = attrs.max_combo(); let max_combo = attrs.max_combo();
let state = OsuPP::new(&map) let state = OsuPP::new(&map)
@@ -839,8 +819,8 @@ mod test {
} }
#[test] #[test]
fn osu_hitresults_n50_n_misses_worst() { fn hitresults_n50_n_misses_worst() {
let (map, attrs) = test_data!(); let (map, attrs) = test_data();
let max_combo = attrs.max_combo(); let max_combo = attrs.max_combo();
let state = OsuPP::new(&map) let state = OsuPP::new(&map)
@@ -863,8 +843,8 @@ mod test {
} }
#[test] #[test]
fn osu_hitresults_n300_n100_n50_n_misses_worst() { fn hitresults_n300_n100_n50_n_misses_worst() {
let (map, attrs) = test_data!(); let (map, attrs) = test_data();
let max_combo = attrs.max_combo(); let max_combo = attrs.max_combo();
let state = OsuPP::new(&map) let state = OsuPP::new(&map)
@@ -889,8 +869,8 @@ mod test {
} }
#[test] #[test]
fn osu_hitresults_acc_n_misses_best() { fn hitresults_acc_n_misses_best() {
let (map, attrs) = test_data!(); let (map, attrs) = test_data();
let max_combo = attrs.max_combo(); let max_combo = attrs.max_combo();
let state = OsuPP::new(&map) let state = OsuPP::new(&map)
@@ -919,8 +899,8 @@ mod test {
} }
#[test] #[test]
fn osu_hitresults_acc_n100_n_misses_best() { fn hitresults_acc_n100_n_misses_best() {
let (map, attrs) = test_data!(); let (map, attrs) = test_data();
let max_combo = attrs.max_combo(); let max_combo = attrs.max_combo();
let state = OsuPP::new(&map) let state = OsuPP::new(&map)
@@ -950,8 +930,8 @@ mod test {
} }
#[test] #[test]
fn osu_hitresults_acc_n50_n_misses_best() { fn hitresults_acc_n50_n_misses_best() {
let (map, attrs) = test_data!(); let (map, attrs) = test_data();
let max_combo = attrs.max_combo(); let max_combo = attrs.max_combo();
let state = OsuPP::new(&map) let state = OsuPP::new(&map)
@@ -981,8 +961,8 @@ mod test {
} }
#[test] #[test]
fn osu_hitresults_acc_best() { fn hitresults_acc_best() {
let (map, attrs) = test_data!(); let (map, attrs) = test_data();
let max_combo = attrs.max_combo(); let max_combo = attrs.max_combo();
let state = OsuPP::new(&map) let state = OsuPP::new(&map)
@@ -1010,8 +990,8 @@ mod test {
} }
#[test] #[test]
fn osu_hitresults_acc_worst() { fn hitresults_acc_worst() {
let (map, attrs) = test_data!(); let (map, attrs) = test_data();
let max_combo = attrs.max_combo(); let max_combo = attrs.max_combo();
let state = OsuPP::new(&map) let state = OsuPP::new(&map)
+5 -5
View File
@@ -21,7 +21,7 @@ use crate::{
/// let pp_result = AnyPP::new(&map) /// let pp_result = AnyPP::new(&map)
/// .mods(8 + 64) // HDDT /// .mods(8 + 64) // HDDT
/// .combo(1234) /// .combo(1234)
/// .misses(1) /// .n_misses(1)
/// .accuracy(98.5) // should be set last /// .accuracy(98.5) // should be set last
/// .calculate(); /// .calculate();
/// ///
@@ -287,10 +287,10 @@ impl AttributeProvider for PerformanceAttributes {
#[inline] #[inline]
fn attributes(self) -> DifficultyAttributes { fn attributes(self) -> DifficultyAttributes {
match self { match self {
Self::Catch(f) => DifficultyAttributes::Catch(f.difficulty), Self::Catch(attrs) => DifficultyAttributes::Catch(attrs.difficulty),
Self::Mania(m) => DifficultyAttributes::Mania(m.difficulty), Self::Mania(attrs) => DifficultyAttributes::Mania(attrs.difficulty),
Self::Osu(o) => DifficultyAttributes::Osu(o.difficulty), Self::Osu(attrs) => DifficultyAttributes::Osu(attrs.difficulty),
Self::Taiko(t) => DifficultyAttributes::Taiko(t.difficulty), Self::Taiko(attrs) => DifficultyAttributes::Taiko(attrs.difficulty),
} }
} }
} }
+36 -36
View File
@@ -42,7 +42,6 @@ use super::{
/// ``` /// ```
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct TaikoGradualDifficultyAttributes { pub struct TaikoGradualDifficultyAttributes {
pub(crate) idx: usize,
attrs: TaikoDifficultyAttributes, attrs: TaikoDifficultyAttributes,
hit_objects: IntoIter<Rc<RefCell<TaikoDifficultyObject>>>, hit_objects: IntoIter<Rc<RefCell<TaikoDifficultyObject>>>,
lists: ObjectLists, lists: ObjectLists,
@@ -61,7 +60,7 @@ impl TaikoGradualDifficultyAttributes {
.clock_rate(clock_rate) .clock_rate(clock_rate)
.hit_windows(); .hit_windows();
let attrs = TaikoDifficultyAttributes { let mut attrs = TaikoDifficultyAttributes {
stamina: 0.0, stamina: 0.0,
rhythm: 0.0, rhythm: 0.0,
colour: 0.0, colour: 0.0,
@@ -71,15 +70,27 @@ impl TaikoGradualDifficultyAttributes {
max_combo: 0, max_combo: 0,
}; };
if map.hit_objects.len() < 2 {
return Self {
hit_objects: Vec::new().into_iter(),
lists: ObjectLists::default(),
peaks,
attrs,
};
}
attrs.max_combo += map.hit_objects[0].is_circle() as usize;
attrs.max_combo += map.hit_objects[1].is_circle() as usize;
let mut diff_objects = map let mut diff_objects = map
.taiko_objects() .taiko_objects()
.enumerate()
.skip(2) .skip(2)
.zip(map.hit_objects.iter().skip(1)) .zip(map.hit_objects.iter().skip(1))
.zip(map.hit_objects.iter()) .zip(map.hit_objects.iter())
.enumerate()
.fold( .fold(
ObjectLists::default(), ObjectLists::default(),
|mut lists, (((idx, (base, base_start_time)), last), last_last)| { |mut lists, (idx, (((base, base_start_time), last), last_last))| {
let diff_obj = TaikoDifficultyObject::new( let diff_obj = TaikoDifficultyObject::new(
base, base,
base_start_time, base_start_time,
@@ -109,25 +120,27 @@ impl TaikoGradualDifficultyAttributes {
ColourDifficultyPreprocessor::process_and_assign(&mut diff_objects); ColourDifficultyPreprocessor::process_and_assign(&mut diff_objects);
Self { Self {
idx: 0,
hit_objects: diff_objects.all.clone().into_iter(), hit_objects: diff_objects.all.clone().into_iter(),
lists: diff_objects, lists: diff_objects,
peaks, peaks,
attrs, attrs,
} }
} }
pub(crate) fn passed_objects(&self) -> usize {
self.lists.all.len() - self.hit_objects.len()
}
} }
impl Iterator for TaikoGradualDifficultyAttributes { impl Iterator for TaikoGradualDifficultyAttributes {
type Item = TaikoDifficultyAttributes; type Item = TaikoDifficultyAttributes;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let curr = self.hit_objects.next()?;
{ {
let curr = curr.borrow(); let curr = self.hit_objects.next()?;
self.peaks.process(&curr, &self.lists); let borrowed = curr.borrow();
self.attrs.max_combo += curr.base.is_hit as usize; self.peaks.process(&borrowed, &self.lists);
self.attrs.max_combo += borrowed.base.is_hit as usize;
} }
let PeaksDifficultyValues { let PeaksDifficultyValues {
@@ -174,6 +187,19 @@ impl Iterator for TaikoGradualDifficultyAttributes {
(len, Some(len)) (len, Some(len))
} }
fn nth(&mut self, n: usize) -> Option<Self::Item> {
let skip = n.min(self.len()).saturating_sub(1);
for _ in 0..skip {
let curr = self.hit_objects.next()?;
let borrowed = curr.borrow();
self.peaks.process(&borrowed, &self.lists);
self.attrs.max_combo += borrowed.base.is_hit as usize;
}
self.next()
}
} }
impl ExactSizeIterator for TaikoGradualDifficultyAttributes { impl ExactSizeIterator for TaikoGradualDifficultyAttributes {
@@ -182,29 +208,3 @@ impl ExactSizeIterator for TaikoGradualDifficultyAttributes {
self.hit_objects.len() self.hit_objects.len()
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = TaikoGradualDifficultyAttributes::new(&map, 0);
assert!(attributes.next().is_none());
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn iter_end_eq_regular() {
let map = Beatmap::from_path("./maps/1028484.osu").expect("failed to parse map");
let mods = 64;
let regular = crate::TaikoStars::new(&map).mods(mods).calculate();
let iter_end = TaikoGradualDifficultyAttributes::new(&map, mods)
.last()
.expect("empty iter");
assert_eq!(regular, iter_end);
}
}
+5 -103
View File
@@ -91,7 +91,7 @@ impl TaikoScoreState {
/// // Then comes a miss. /// // Then comes a miss.
/// // Note that state's max combo won't be incremented for /// // Note that state's max combo won't be incremented for
/// // the next few objects because the combo is reset. /// // the next few objects because the combo is reset.
/// state.misses += 1; /// state.n_misses += 1;
/// # /* /// # /*
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap(); /// let performance = gradual_perf.process_next_object(state.clone()).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
@@ -122,7 +122,7 @@ impl TaikoScoreState {
/// state.max_combo = ... /// state.max_combo = ...
/// state.n300 = ... /// state.n300 = ...
/// state.n100 = ... /// state.n100 = ...
/// state.misses = ... /// state.n_misses = ...
/// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap(); /// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap();
/// println!("PP: {}", performance.pp); /// println!("PP: {}", performance.pp);
/// # */ /// # */
@@ -170,115 +170,17 @@ impl<'map> TaikoGradualPerformanceAttributes<'map> {
state: TaikoScoreState, state: TaikoScoreState,
n: usize, n: usize,
) -> Option<TaikoPerformanceAttributes> { ) -> Option<TaikoPerformanceAttributes> {
let n = n.min(self.difficulty.len()).saturating_sub(1); let sub = 2 * (self.difficulty.passed_objects() == 0) as usize;
let difficulty = self.difficulty.nth(n)?; let difficulty = self.difficulty.nth(n.saturating_sub(sub))?;
let performance = self let performance = self
.performance .performance
.clone() .clone()
.attributes(difficulty) .attributes(difficulty)
.state(state) .state(state)
.passed_objects(self.difficulty.idx) .passed_objects(self.difficulty.passed_objects() + 2)
.calculate(); .calculate();
Some(performance) Some(performance)
} }
} }
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn correct_empty() {
let map = Beatmap::from_path("./maps/1028484.osu").expect("failed to parse map");
let mods = 64;
let mut gradual = TaikoGradualPerformanceAttributes::new(&map, mods);
let state = TaikoScoreState::default();
assert!(gradual
.process_next_n_objects(state.clone(), usize::MAX)
.is_some());
assert!(gradual.process_next_object(state).is_none());
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn next_and_next_n() {
let map = Beatmap::from_path("./maps/1028484.osu").expect("failed to parse map");
let mods = 64;
let state = TaikoScoreState::default();
let mut gradual1 = TaikoGradualPerformanceAttributes::new(&map, mods);
let mut gradual2 = TaikoGradualPerformanceAttributes::new(&map, mods);
for _ in 0..50 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
}
let n = 200;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
}
let state = TaikoScoreState {
max_combo: 246,
n300: 200,
n100: 40,
n_misses: 6,
};
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
assert_eq!(next_n, next);
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn gradual_end_eq_regular() {
let map = Beatmap::from_path("./maps/1028484.osu").expect("failed to parse map");
let mods = 64;
let regular = TaikoPP::new(&map).mods(mods).calculate();
let mut gradual = TaikoGradualPerformanceAttributes::new(&map, mods);
let state = TaikoScoreState {
max_combo: 289,
n300: 289,
n100: 0,
n_misses: 0,
};
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end);
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn gradual_eq_regular_passed() {
let map = Beatmap::from_path("./maps/1028484.osu").expect("failed to parse map");
let mods = 64;
let n = 250;
let regular = TaikoPP::new(&map).mods(mods).passed_objects(n).calculate();
let mut gradual = TaikoGradualPerformanceAttributes::new(&map, mods);
let state = TaikoScoreState {
max_combo: 246,
n300: 246,
n100: 0,
n_misses: 0,
};
let gradual = gradual.process_next_n_objects(state, n).unwrap();
assert_eq!(regular, gradual);
}
}
+79 -37
View File
@@ -20,15 +20,15 @@ use crate::{
/// let pp_result = TaikoPP::new(&map) /// let pp_result = TaikoPP::new(&map)
/// .mods(8 + 64) // HDDT /// .mods(8 + 64) // HDDT
/// .combo(1234) /// .combo(1234)
/// .misses(1) /// .n_misses(1)
/// .accuracy(98.5) /// .accuracy(98.5)
/// .calculate(); /// .calculate();
/// ///
/// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars()); /// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars());
/// ///
/// let next_result = TaikoPP::new(&map) /// let next_result = TaikoPP::new(&map)
/// .attributes(pp_result) // reusing previous results for performance /// .attributes(pp_result) // reusing previous results for performance
/// .mods(8 + 64) // has to be the same to reuse attributes /// .mods(8 + 64) // has to be the same to reuse attributes
/// .accuracy(99.5) /// .accuracy(99.5)
/// .calculate(); /// .calculate();
/// ///
@@ -225,7 +225,6 @@ impl<'map> TaikoPP<'map> {
let n_misses = self.n_misses.unwrap_or(0); let n_misses = self.n_misses.unwrap_or(0);
if let Some(acc) = self.acc { if let Some(acc) = self.acc {
// TODO: test
match (self.n300, self.n100) { match (self.n300, self.n100) {
(Some(_), Some(_)) => { (Some(_), Some(_)) => {
let remaining = total_result_count.saturating_sub(n300 + n100 + n_misses); let remaining = total_result_count.saturating_sub(n300 + n100 + n_misses);
@@ -249,15 +248,19 @@ impl<'map> TaikoPP<'map> {
match priority { match priority {
HitResultPriority::BestCase => match (self.n300, self.n100) { HitResultPriority::BestCase => match (self.n300, self.n100) {
(Some(_), None) => n100 = remaining, (Some(_), None) => n100 = remaining,
_ => n300 = remaining, (Some(_), Some(_)) => n300 += remaining,
(None, _) => n300 = remaining,
}, },
HitResultPriority::WorstCase => match (self.n300, self.n100) { HitResultPriority::WorstCase => match (self.n300, self.n100) {
(None, Some(_)) => n300 = remaining, (None, Some(_)) => n300 = remaining,
_ => n100 = remaining, (Some(_), Some(_)) => n100 += remaining,
(_, None) => n100 = remaining,
}, },
} }
} }
let max_combo = self.combo.map_or(max_combo, |combo| combo.min(max_combo));
TaikoScoreState { TaikoScoreState {
max_combo, max_combo,
n300, n300,
@@ -462,61 +465,100 @@ impl TaikoAttributeProvider for PerformanceAttributes {
} }
} }
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::Beatmap; use crate::Beatmap;
fn test_attrs() -> TaikoDifficultyAttributes { fn test_data() -> (Beatmap, TaikoDifficultyAttributes) {
todo!()
}
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
fn test_map() -> (Beatmap, TaikoDifficultyAttributes) {
let path = "./maps/1028484.osu"; let path = "./maps/1028484.osu";
let map = Beatmap::from_path(path).unwrap(); let map = Beatmap::from_path(path).unwrap();
(map, test_attrs()) let attrs = TaikoDifficultyAttributes {
} stamina: 1.4528845068865617,
rhythm: 0.20130047251681948,
colour: 1.0487315549761433,
peak: 1.8881824429738323,
hit_window: 35.0,
stars: 2.9778030386845606,
max_combo: 289,
};
#[cfg(any(feature = "async_tokio", feature = "async_str"))] (map, attrs)
async fn test_map() -> (Beatmap, TaikoDifficultyAttributes) {
let path = "./maps/1028484.osu";
let map = Beatmap::from_path(path).await.unwrap();
(map, test_attrs())
}
#[rustfmt::skip]
macro_rules! test_data {
() => {{
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
{ test_map() }
#[cfg(any(feature = "async_tokio", feature = "async_str"))]
{ test_map().await }
}};
} }
#[test] #[test]
fn taiko_hitresults_n300_n_misses_best() { fn hitresults_n300_n_misses_best() {
let (map, attrs) = test_data!(); let (map, attrs) = test_data();
let max_combo = attrs.max_combo(); let max_combo = attrs.max_combo();
let state = TaikoPP::new(&map) let state = TaikoPP::new(&map)
.attributes(attrs) .attributes(attrs)
.combo(500) .combo(100)
.n300(300) .n300(150)
.n_misses(2) .n_misses(2)
.hitresult_priority(HitResultPriority::BestCase) .hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults(max_combo); .generate_hitresults(max_combo);
let expected = TaikoScoreState { let expected = TaikoScoreState {
max_combo: 500, max_combo: 100,
n300: 300, n300: 150,
n100: 20, n100: 137,
n_misses: 2, n_misses: 2,
}; };
assert_eq!(state, expected); assert_eq!(state, expected);
} }
#[test]
fn hitresults_n_misses_best() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = TaikoPP::new(&map)
.attributes(attrs)
.combo(100)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults(max_combo);
let expected = TaikoScoreState {
max_combo: 100,
n300: 287,
n100: 0,
n_misses: 2,
};
assert_eq!(state, expected);
}
#[test]
fn hitresults_acc_n_misses_worst() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = TaikoPP::new(&map)
.attributes(attrs)
.combo(100)
.accuracy(97.2)
.n_misses(2)
.hitresult_priority(HitResultPriority::WorstCase)
.generate_hitresults(max_combo);
let expected = TaikoScoreState {
max_combo: 100,
n300: 275,
n100: 12,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
}
} }
+6 -6
View File
@@ -27,12 +27,12 @@ impl Peaks {
} }
pub(crate) fn difficulty_values(self) -> PeaksDifficultyValues { pub(crate) fn difficulty_values(self) -> PeaksDifficultyValues {
let colour_rating = let colour_rating = <Colour as StrainSkill>::difficulty_value(self.colour.clone())
StrainSkill::difficulty_value(self.colour.clone()) * Self::COLOUR_SKILL_MULTIPLIER; * Self::COLOUR_SKILL_MULTIPLIER;
let rhythm_rating = let rhythm_rating = <Rhythm as StrainSkill>::difficulty_value(self.rhythm.clone())
StrainSkill::difficulty_value(self.rhythm.clone()) * Self::RHYTHM_SKILL_MULTIPLIER; * Self::RHYTHM_SKILL_MULTIPLIER;
let stamina_rating = let stamina_rating = <Stamina as StrainSkill>::difficulty_value(self.stamina.clone())
StrainSkill::difficulty_value(self.stamina.clone()) * Self::STAMINA_SKILL_MULTIPLIER; * Self::STAMINA_SKILL_MULTIPLIER;
PeaksDifficultyValues { PeaksDifficultyValues {
colour_rating, colour_rating,
+30
View File
@@ -0,0 +1,30 @@
mod mode;
use rosu_pp::Beatmap;
pub use self::mode::{Catch, Mania, Mode, Osu, Taiko};
#[macro_export]
#[rustfmt::skip]
macro_rules! test_map {
($mode:ident) => {{
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
{ common::test_map::<$mode>() }
#[cfg(any(feature = "async_tokio", feature = "async_str"))]
{ common::test_map::<$mode>().await }
}};
}
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
pub fn test_map<M: Mode>() -> Beatmap {
let path = format!("./maps/{}.osu", M::TEST_MAP_ID);
Beatmap::from_path(path).unwrap()
}
#[cfg(any(feature = "async_tokio", feature = "async_str"))]
pub async fn test_map() -> Beatmap {
let path = format!("./maps/{}.osu", M::TEST_MAP_ID);
Beatmap::from_path(path).await.unwrap()
}
+68
View File
@@ -0,0 +1,68 @@
use rosu_pp::{
catch::CatchDifficultyAttributes, mania::ManiaDifficultyAttributes,
osu::OsuDifficultyAttributes, taiko::TaikoDifficultyAttributes,
};
pub struct Osu;
pub struct Taiko;
pub struct Catch;
pub struct Mania;
pub trait Mode {
type DifficultyAttributes;
const TEST_MAP_ID: u32;
const TEST_DIFF_ATTRS: Self::DifficultyAttributes;
}
macro_rules! impl_mode {
( $( $mode:ident: $map_id:literal, $diff_attrs:ident $fields:tt; )* ) => {
$(
impl Mode for $mode {
type DifficultyAttributes = $diff_attrs;
const TEST_MAP_ID: u32 = $map_id;
const TEST_DIFF_ATTRS: Self::DifficultyAttributes = $diff_attrs $fields;
}
)*
};
}
impl_mode! {
Osu: 2785319, OsuDifficultyAttributes {
aim: 2.8693628443424104,
speed: 2.533869745015772,
flashlight: 2.288770487900865,
slider_factor: 0.9803052946037858,
speed_note_count: 210.36373973116545,
ar: 9.300000190734863,
od: 8.800000190734863,
hp: 5.0,
n_circles: 307,
n_sliders: 293,
n_spinners: 1,
stars: 5.669858729379628,
max_combo: 909,
};
Taiko: 1028484, TaikoDifficultyAttributes {
stamina: 1.4528845068865617,
rhythm: 0.20130047251681948,
colour: 1.0487315549761433,
peak: 1.8881824429738323,
hit_window: 35.0,
stars: 2.9778030386845606,
max_combo: 289,
};
Catch: 2118524, CatchDifficultyAttributes {
stars: 3.2502669316166624,
ar: 8.0,
n_fruits: 728,
n_droplets: 2,
n_tiny_droplets: 291,
};
Mania: 1974394, ManiaDifficultyAttributes {
stars: 4.824631127426499,
hit_window: 40.0,
max_combo: 5064,
};
}
+40
View File
@@ -0,0 +1,40 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))]
use common::{test_map, Osu};
use rosu_pp::{CatchStars, ManiaStars, OsuStars, TaikoStars};
use crate::common::{Catch, Mania, Mode, Taiko};
mod common;
#[test]
fn difficulty_osu() {
let map = test_map::<Osu>();
let attrs = OsuStars::new(&map).calculate();
assert_eq!(Osu::TEST_DIFF_ATTRS, attrs);
}
#[test]
fn difficulty_taiko() {
let map = test_map::<Taiko>();
let attrs = TaikoStars::new(&map).calculate();
assert_eq!(Taiko::TEST_DIFF_ATTRS, attrs);
}
#[test]
fn difficulty_catch() {
let map = test_map::<Catch>();
let attrs = CatchStars::new(&map).calculate();
assert_eq!(Catch::TEST_DIFF_ATTRS, attrs);
}
#[test]
fn difficulty_mania() {
let map = test_map::<Mania>();
let attrs = ManiaStars::new(&map).calculate();
assert_eq!(Mania::TEST_DIFF_ATTRS, attrs);
}
+119
View File
@@ -0,0 +1,119 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))]
use rosu_pp::{
catch::{CatchGradualDifficultyAttributes, CatchGradualPerformanceAttributes, CatchScoreState},
Beatmap, CatchPP, CatchStars,
};
use crate::common::Catch;
mod common;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = CatchGradualDifficultyAttributes::new(&map, 0);
assert!(attributes.next().is_none());
}
#[test]
fn iter_end_eq_regular() {
let map = test_map!(Catch);
let regular = CatchStars::new(&map).calculate();
let iter_end = CatchGradualDifficultyAttributes::new(&map, 0)
.last()
.expect("empty iter");
assert_eq!(regular, iter_end);
}
#[test]
fn correct_empty() {
let map = test_map!(Catch);
let mut gradual = CatchGradualPerformanceAttributes::new(&map, 0);
let state = CatchScoreState::default();
let first_attrs = gradual.process_next_n_objects(state.clone(), usize::MAX);
assert!(first_attrs.is_some());
assert!(gradual.process_next_object(state).is_none());
}
#[test]
fn next_and_next_n() {
let map = test_map!(Catch);
let state = CatchScoreState::default();
let mut gradual1 = CatchGradualPerformanceAttributes::new(&map, 0);
let mut gradual2 = CatchGradualPerformanceAttributes::new(&map, 0);
for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
}
let n = 80;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
}
let state = CatchScoreState {
max_combo: 101,
n_fruits: 99,
n_droplets: 2,
n_tiny_droplets: 68,
n_tiny_droplet_misses: 0,
n_misses: 0,
};
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
assert_eq!(next_n, next);
}
#[test]
fn gradual_end_eq_regular() {
let map = test_map!(Catch);
let regular = CatchPP::new(&map).calculate();
let mut gradual = CatchGradualPerformanceAttributes::new(&map, 0);
let state = CatchScoreState {
max_combo: 730,
n_fruits: 728,
n_droplets: 2,
n_tiny_droplets: 291,
n_tiny_droplet_misses: 0,
n_misses: 0,
};
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end);
}
#[test]
fn gradual_eq_regular_passed() {
let map = test_map!(Catch);
let n = 100;
let regular = CatchPP::new(&map).passed_objects(n).calculate();
let mut gradual = CatchGradualPerformanceAttributes::new(&map, 0);
let state = CatchScoreState {
max_combo: 101,
n_fruits: 99,
n_droplets: 2,
n_tiny_droplets: 68,
n_tiny_droplet_misses: 0,
n_misses: 0,
};
let gradual = gradual.process_next_n_objects(state, n).unwrap();
assert_eq!(regular, gradual);
}
+131
View File
@@ -0,0 +1,131 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))]
use rosu_pp::{
mania::{ManiaGradualDifficultyAttributes, ManiaGradualPerformanceAttributes, ManiaScoreState},
Beatmap, ManiaPP, ManiaStars,
};
use crate::common::Mania;
mod common;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = ManiaGradualDifficultyAttributes::new(&map, 0);
assert!(attributes.next().is_none());
}
#[test]
fn iter_end_eq_regular() {
let map = test_map!(Mania);
let regular = ManiaStars::new(&map).calculate();
let iter_end = ManiaGradualDifficultyAttributes::new(&map, 0)
.last()
.expect("empty iter");
assert_eq!(regular, iter_end);
}
#[test]
fn correct_empty() {
let map = test_map!(Mania);
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, 0);
let state = ManiaScoreState {
n320: 0,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let first_attrs = gradual.process_next_n_objects(state.clone(), usize::MAX);
assert!(first_attrs.is_some());
assert!(gradual.process_next_object(state).is_none());
}
#[test]
fn next_and_next_n() {
let map = test_map!(Mania);
let mut state = ManiaScoreState {
n320: 0,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let mut gradual1 = ManiaGradualPerformanceAttributes::new(&map, 0);
let mut gradual2 = ManiaGradualPerformanceAttributes::new(&map, 0);
for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
state.n320 += 1;
}
let n = 80;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
state.n320 += 1;
}
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
assert_eq!(next_n, next);
}
#[test]
fn gradual_end_eq_regular() {
let map = test_map!(Mania);
let regular = ManiaPP::new(&map).calculate();
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, 0);
let state = ManiaScoreState {
n320: 3238,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end);
}
#[test]
fn gradual_eq_regular_passed() {
let map = test_map!(Mania);
let n = 100;
let state = ManiaScoreState {
n320: 100,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let regular = ManiaPP::new(&map)
.passed_objects(n)
.state(state.clone())
.calculate();
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, 0);
let gradual = gradual.process_next_n_objects(state, n).unwrap();
assert_eq!(regular, gradual);
}
+115
View File
@@ -0,0 +1,115 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))]
use rosu_pp::{
osu::{OsuGradualDifficultyAttributes, OsuGradualPerformanceAttributes, OsuScoreState},
Beatmap, OsuPP, OsuStars,
};
use crate::common::Osu;
mod common;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = OsuGradualDifficultyAttributes::new(&map, 0);
assert!(attributes.next().is_none());
}
#[test]
fn iter_end_eq_regular() {
let map = test_map!(Osu);
let regular = OsuStars::new(&map).calculate();
let iter_end = OsuGradualDifficultyAttributes::new(&map, 0)
.last()
.expect("empty iter");
assert_eq!(regular, iter_end);
}
#[test]
fn correct_empty() {
let map = test_map!(Osu);
let mut gradual = OsuGradualPerformanceAttributes::new(&map, 0);
let state = OsuScoreState::default();
let first_attrs = gradual.process_next_n_objects(state.clone(), usize::MAX);
assert!(first_attrs.is_some());
assert!(gradual.process_next_object(state).is_none());
}
#[test]
fn next_and_next_n() {
let map = test_map!(Osu);
let state = OsuScoreState::default();
let mut gradual1 = OsuGradualPerformanceAttributes::new(&map, 0);
let mut gradual2 = OsuGradualPerformanceAttributes::new(&map, 0);
for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
}
let n = 80;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
}
let state = OsuScoreState {
max_combo: 122,
n300: 88,
n100: 8,
n50: 2,
n_misses: 2,
};
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
assert_eq!(next_n, next);
}
#[test]
fn gradual_end_eq_regular() {
let map = test_map!(Osu);
let regular = OsuPP::new(&map).calculate();
let mut gradual = OsuGradualPerformanceAttributes::new(&map, 0);
let state = OsuScoreState {
max_combo: 909,
n300: 601,
n100: 0,
n50: 0,
n_misses: 0,
};
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end);
}
#[test]
fn gradual_eq_regular_passed() {
let map = test_map!(Osu);
let n = 100;
let regular = OsuPP::new(&map).passed_objects(n).calculate();
let mut gradual = OsuGradualPerformanceAttributes::new(&map, 0);
let state = OsuScoreState {
max_combo: 122,
n300: 100,
n100: 0,
n50: 0,
n_misses: 0,
};
let gradual = gradual.process_next_n_objects(state, n).unwrap();
assert_eq!(regular, gradual);
}
+113
View File
@@ -0,0 +1,113 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))]
use rosu_pp::{
taiko::{TaikoGradualDifficultyAttributes, TaikoGradualPerformanceAttributes, TaikoScoreState},
Beatmap, TaikoPP, TaikoStars,
};
use crate::common::Taiko;
mod common;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attrs = TaikoGradualDifficultyAttributes::new(&map, 0);
assert!(attrs.next().is_none());
}
#[test]
fn iter_end_eq_regular() {
let map = test_map!(Taiko);
let regular = TaikoStars::new(&map).calculate();
let iter_end = TaikoGradualDifficultyAttributes::new(&map, 0)
.last()
.expect("empty iter");
assert_eq!(regular, iter_end);
}
#[test]
fn correct_empty() {
let map = test_map!(Taiko);
let mut gradual = TaikoGradualPerformanceAttributes::new(&map, 0);
let state = TaikoScoreState::default();
let first_attrs = gradual.process_next_n_objects(state.clone(), usize::MAX);
assert!(first_attrs.is_some());
assert!(gradual.process_next_object(state).is_none());
}
#[test]
fn next_and_next_n() {
let map = test_map!(Taiko);
let state = TaikoScoreState::default();
let mut gradual1 = TaikoGradualPerformanceAttributes::new(&map, 0);
let mut gradual2 = TaikoGradualPerformanceAttributes::new(&map, 0);
for _ in 0..50 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
}
let n = 200;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
}
let state = TaikoScoreState {
max_combo: 246,
n300: 200,
n100: 40,
n_misses: 6,
};
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
assert_eq!(next_n, next);
}
#[test]
fn gradual_end_eq_regular() {
let map = test_map!(Taiko);
let regular = TaikoPP::new(&map).calculate();
let mut gradual = TaikoGradualPerformanceAttributes::new(&map, 0);
let state = TaikoScoreState {
max_combo: 289,
n300: 289,
n100: 0,
n_misses: 0,
};
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end);
}
#[test]
fn gradual_eq_regular_passed() {
let map = test_map!(Taiko);
let n = 250;
let regular = TaikoPP::new(&map).passed_objects(n).calculate();
let mut gradual = TaikoGradualPerformanceAttributes::new(&map, 0);
let state = TaikoScoreState {
max_combo: 246,
n300: 246,
n100: 0,
n_misses: 0,
};
let gradual = gradual.process_next_n_objects(state, n).unwrap();
// Cyclic types in difficulty calculation prohibit values to coincide completely
assert_eq!(regular.difficulty.max_combo, gradual.difficulty.max_combo);
}