fixed & extended test cases
This commit is contained in:
@@ -22,14 +22,13 @@ let map = match Beatmap::from_path("/path/to/file.osu") {
|
||||
let result = map.pp()
|
||||
.mods(24) // HDHR
|
||||
.combo(1234)
|
||||
.accuracy(99.2)
|
||||
.misses(2)
|
||||
.accuracy(99.2) // should be called last
|
||||
.calculate();
|
||||
|
||||
println!("PP: {}", result.pp());
|
||||
|
||||
// If you intend to reuse the current map-mod combination,
|
||||
// make use of the previous result!
|
||||
// If you want to reuse the current map-mod combination, make use of the previous result!
|
||||
// If attributes are given, then stars & co don't have to be recalculated.
|
||||
let next_result = map.pp()
|
||||
.mods(24) // HDHR
|
||||
@@ -68,7 +67,7 @@ let map = match Beatmap::from_path("/path/to/file.osu").await {
|
||||
let result = map.pp()
|
||||
.mods(24) // HDHR
|
||||
.combo(1234)
|
||||
.misses(2)
|
||||
.n_misses(2)
|
||||
.accuracy(99.2)
|
||||
.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.
|
||||
let state = ScoreState {
|
||||
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,
|
||||
n100: 0,
|
||||
n50: 0,
|
||||
misses: 0,
|
||||
score: 300, // only relevant for mania
|
||||
n_misses: 0,
|
||||
};
|
||||
|
||||
// Process the score state after the first object
|
||||
@@ -144,7 +143,7 @@ let state = TaikoScoreState {
|
||||
max_combo: 11,
|
||||
n300: 9,
|
||||
n100: 1,
|
||||
misses: 1,
|
||||
n_misses: 1,
|
||||
};
|
||||
|
||||
// Process the next 10 objects in one go
|
||||
@@ -158,29 +157,46 @@ println!("PP after the first 11 objects: {}", curr_performance.pp());
|
||||
|
||||
### Features
|
||||
|
||||
| Flag | Description |
|
||||
|-----|-----|
|
||||
| `default` | Beatmap parsing will be non-async |
|
||||
| `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) |
|
||||
| Flag | Description |
|
||||
| ------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `default` | Beatmap parsing will be non-async |
|
||||
| `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) |
|
||||
|
||||
### 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
|
||||
|
||||
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)
|
||||
<img src="./pp-plot/osu_accuracy.svg">
|
||||
For every (!) comparison of the star and pp values, the error margin was below `0.000000001`, ensuing a great accuracy.
|
||||
|
||||
osu!mania: (close to perfect values)
|
||||
<img src="./pp-plot/mania_accuracy.svg">
|
||||
### Benchmark
|
||||
|
||||
osu!catch: (pretty accurate)
|
||||
<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">
|
||||
To be done
|
||||
|
||||
### Bindings
|
||||
|
||||
|
||||
@@ -209,29 +209,3 @@ impl Iterator for CatchObjectIter<'_> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ impl CatchScoreState {
|
||||
/// // Then comes a miss.
|
||||
/// // Note that state's max combo won't be incremented for
|
||||
/// // 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();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
@@ -114,7 +114,7 @@ impl CatchScoreState {
|
||||
/// state.n_droplets = ...
|
||||
/// state.n_tiny_droplets = ...
|
||||
/// state.n_tiny_droplet_misses = ...
|
||||
/// state.misses = ...
|
||||
/// state.n_misses = ...
|
||||
/// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
/// # */
|
||||
@@ -187,107 +187,3 @@ impl<'map> CatchGradualPerformanceAttributes<'map> {
|
||||
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
@@ -23,8 +23,8 @@ use crate::{Beatmap, DifficultyAttributes, Mods, OsuPP, PerformanceAttributes};
|
||||
/// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars());
|
||||
///
|
||||
/// let next_result = CatchPP::new(&map)
|
||||
/// .attributes(pp_result) // reusing previous results for performance
|
||||
/// .mods(8 + 64) // has to be the same to reuse attributes
|
||||
/// .attributes(pp_result) // reusing previous results for performance
|
||||
/// .mods(8 + 64) // has to be the same to reuse attributes
|
||||
/// .accuracy(99.5)
|
||||
/// .calculate();
|
||||
///
|
||||
|
||||
+1
-4
@@ -214,7 +214,6 @@ impl From<ScoreState> for ManiaScoreState {
|
||||
/// for _ in 0..10 {
|
||||
/// state.n300 += 1;
|
||||
/// state.max_combo += 1;
|
||||
/// state.score += 123;
|
||||
///
|
||||
/// # /*
|
||||
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap();
|
||||
@@ -226,7 +225,7 @@ impl From<ScoreState> for ManiaScoreState {
|
||||
/// // Then comes a miss.
|
||||
/// // Note that state's max combo won't be incremented for
|
||||
/// // 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();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
@@ -238,7 +237,6 @@ impl From<ScoreState> for ManiaScoreState {
|
||||
/// state.n300 += 2;
|
||||
/// state.n100 += 7;
|
||||
/// state.n50 += 1;
|
||||
/// state.score += 987;
|
||||
/// // Don't forget state.n_katu
|
||||
/// # /*
|
||||
/// 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.
|
||||
/// state.n300 += 1;
|
||||
/// state.max_combo += 1;
|
||||
/// state.score += 123;
|
||||
/// # /*
|
||||
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
|
||||
+70
-73
@@ -15,25 +15,24 @@
|
||||
//! };
|
||||
//! # */ let map = Beatmap::default();
|
||||
//!
|
||||
//! // If `BeatmapExt` is included, you can make use of
|
||||
//! // some methods on `Beatmap` to make your life simpler.
|
||||
//! // If `BeatmapExt` is included, you can make use of some methods
|
||||
//! // on `Beatmap` to make your life simpler like `BeatmapExt::pp`.
|
||||
//! let result = map.pp()
|
||||
//! .mods(24) // HDHR
|
||||
//! .combo(1234)
|
||||
//! .misses(2)
|
||||
//! .accuracy(99.2) // should be called last
|
||||
//! .accuracy(99.2)
|
||||
//! .n_misses(2)
|
||||
//! .calculate();
|
||||
//!
|
||||
//! println!("PP: {}", result.pp());
|
||||
//!
|
||||
//! // If you intend to reuse the current map-mod combination,
|
||||
//! // make use of the previous result!
|
||||
//! // If you want to reuse the current map-mod combination, make use of the previous result!
|
||||
//! // If attributes are given, then stars & co don't have to be recalculated.
|
||||
//! let next_result = map.pp()
|
||||
//! .mods(24) // HDHR
|
||||
//! .attributes(result) // recycle
|
||||
//! .combo(543)
|
||||
//! .misses(5)
|
||||
//! .n_misses(5)
|
||||
//! .n50(3)
|
||||
//! .accuracy(96.5)
|
||||
//! .calculate();
|
||||
@@ -68,7 +67,7 @@
|
||||
//! let result = map.pp()
|
||||
//! .mods(24) // HDHR
|
||||
//! .combo(1234)
|
||||
//! .misses(2)
|
||||
//! .n_misses(2)
|
||||
//! .accuracy(99.2)
|
||||
//! .calculate();
|
||||
//!
|
||||
@@ -114,12 +113,12 @@
|
||||
//! // The default score state is kinda chunky because it considers all modes.
|
||||
//! let state = ScoreState {
|
||||
//! 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,
|
||||
//! n100: 0,
|
||||
//! n50: 0,
|
||||
//! misses: 0,
|
||||
//! score: 300, // only relevant for mania
|
||||
//! n_misses: 0,
|
||||
//! };
|
||||
//!
|
||||
//! // Process the score state after the first object
|
||||
@@ -145,7 +144,7 @@
|
||||
//! max_combo: 11,
|
||||
//! n300: 9,
|
||||
//! n100: 1,
|
||||
//! misses: 1,
|
||||
//! n_misses: 1,
|
||||
//! };
|
||||
//!
|
||||
//! // Process the next 10 objects in one go
|
||||
@@ -362,23 +361,21 @@ impl DifficultyAttributes {
|
||||
#[inline]
|
||||
pub fn stars(&self) -> f64 {
|
||||
match self {
|
||||
Self::Catch(attributes) => attributes.stars,
|
||||
Self::Mania(attributes) => attributes.stars,
|
||||
Self::Osu(attributes) => attributes.stars,
|
||||
Self::Taiko(attributes) => attributes.stars,
|
||||
Self::Catch(attrs) => attrs.stars,
|
||||
Self::Mania(attrs) => attrs.stars,
|
||||
Self::Osu(attrs) => attrs.stars,
|
||||
Self::Taiko(attrs) => attrs.stars,
|
||||
}
|
||||
}
|
||||
|
||||
/// The maximum combo of the map.
|
||||
///
|
||||
/// This will only be `None` for attributes of osu!mania maps.
|
||||
#[inline]
|
||||
pub fn max_combo(&self) -> Option<usize> {
|
||||
pub fn max_combo(&self) -> usize {
|
||||
match self {
|
||||
Self::Catch(attributes) => Some(attributes.max_combo()),
|
||||
Self::Mania(_) => None,
|
||||
Self::Osu(attributes) => Some(attributes.max_combo),
|
||||
Self::Taiko(attributes) => Some(attributes.max_combo),
|
||||
Self::Catch(attrs) => attrs.max_combo(),
|
||||
Self::Mania(attrs) => attrs.max_combo,
|
||||
Self::Osu(attrs) => attrs.max_combo,
|
||||
Self::Taiko(attrs) => attrs.max_combo,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -429,10 +426,10 @@ impl PerformanceAttributes {
|
||||
#[inline]
|
||||
pub fn pp(&self) -> f64 {
|
||||
match self {
|
||||
Self::Catch(attributes) => attributes.pp,
|
||||
Self::Mania(attributes) => attributes.pp,
|
||||
Self::Osu(attributes) => attributes.pp,
|
||||
Self::Taiko(attributes) => attributes.pp,
|
||||
Self::Catch(attrs) => attrs.pp,
|
||||
Self::Mania(attrs) => attrs.pp,
|
||||
Self::Osu(attrs) => attrs.pp,
|
||||
Self::Taiko(attrs) => attrs.pp,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,10 +437,10 @@ impl PerformanceAttributes {
|
||||
#[inline]
|
||||
pub fn stars(&self) -> f64 {
|
||||
match self {
|
||||
Self::Catch(attributes) => attributes.stars(),
|
||||
Self::Mania(attributes) => attributes.stars(),
|
||||
Self::Osu(attributes) => attributes.stars(),
|
||||
Self::Taiko(attributes) => attributes.stars(),
|
||||
Self::Catch(attrs) => attrs.stars(),
|
||||
Self::Mania(attrs) => attrs.stars(),
|
||||
Self::Osu(attrs) => attrs.stars(),
|
||||
Self::Taiko(attrs) => attrs.stars(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,23 +448,21 @@ impl PerformanceAttributes {
|
||||
#[inline]
|
||||
pub fn difficulty_attributes(&self) -> DifficultyAttributes {
|
||||
match self {
|
||||
Self::Catch(attributes) => DifficultyAttributes::Catch(attributes.difficulty.clone()),
|
||||
Self::Mania(attributes) => DifficultyAttributes::Mania(attributes.difficulty),
|
||||
Self::Osu(attributes) => DifficultyAttributes::Osu(attributes.difficulty.clone()),
|
||||
Self::Taiko(attributes) => DifficultyAttributes::Taiko(attributes.difficulty.clone()),
|
||||
Self::Catch(attrs) => DifficultyAttributes::Catch(attrs.difficulty.clone()),
|
||||
Self::Mania(attrs) => DifficultyAttributes::Mania(attrs.difficulty),
|
||||
Self::Osu(attrs) => DifficultyAttributes::Osu(attrs.difficulty.clone()),
|
||||
Self::Taiko(attrs) => DifficultyAttributes::Taiko(attrs.difficulty.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// The maximum combo of the map.
|
||||
///
|
||||
/// This will only be `None` for attributes of osu!mania maps.
|
||||
pub fn max_combo(&self) -> Option<usize> {
|
||||
pub fn max_combo(&self) -> usize {
|
||||
match self {
|
||||
Self::Catch(f) => Some(f.difficulty.max_combo()),
|
||||
Self::Mania(_) => None,
|
||||
Self::Osu(o) => Some(o.difficulty.max_combo),
|
||||
Self::Taiko(t) => Some(t.difficulty.max_combo),
|
||||
Self::Catch(attrs) => attrs.difficulty.max_combo(),
|
||||
Self::Mania(attrs) => attrs.difficulty.max_combo,
|
||||
Self::Osu(attrs) => attrs.difficulty.max_combo,
|
||||
Self::Taiko(attrs) => attrs.difficulty.max_combo,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -476,10 +471,10 @@ impl From<PerformanceAttributes> for DifficultyAttributes {
|
||||
#[inline]
|
||||
fn from(attributes: PerformanceAttributes) -> Self {
|
||||
match attributes {
|
||||
PerformanceAttributes::Catch(attributes) => Self::Catch(attributes.difficulty),
|
||||
PerformanceAttributes::Mania(attributes) => Self::Mania(attributes.difficulty),
|
||||
PerformanceAttributes::Osu(attributes) => Self::Osu(attributes.difficulty),
|
||||
PerformanceAttributes::Taiko(attributes) => Self::Taiko(attributes.difficulty),
|
||||
PerformanceAttributes::Catch(attrs) => Self::Catch(attrs.difficulty),
|
||||
PerformanceAttributes::Mania(attrs) => Self::Mania(attrs.difficulty),
|
||||
PerformanceAttributes::Osu(attrs) => Self::Osu(attrs.difficulty),
|
||||
PerformanceAttributes::Taiko(attrs) => Self::Taiko(attrs.difficulty),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -521,7 +516,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn custom() {
|
||||
let path = "F:/osu!/beatmaps/1186086.osu";
|
||||
let path = "F:/osu!/beatmaps/1028484.osu";
|
||||
let map = Beatmap::from_path(path).unwrap();
|
||||
|
||||
let attrs = match OsuPP::new(&map).mode(GameMode::Taiko).mods(0).calculate() {
|
||||
@@ -529,31 +524,33 @@ mod tests {
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
println!(
|
||||
"difficulty:\n\
|
||||
stamina={}\n\
|
||||
rhythm={}\n\
|
||||
colour={}\n\
|
||||
peak={}\n\
|
||||
hit_window={}\n\
|
||||
max_combo={}\n\
|
||||
stars={}\n\
|
||||
performance:\n\
|
||||
difficulty={}\n\
|
||||
acc={}\n\
|
||||
effective={}\n\
|
||||
pp={}\n",
|
||||
attrs.difficulty.stamina,
|
||||
attrs.difficulty.rhythm,
|
||||
attrs.difficulty.colour,
|
||||
attrs.difficulty.peak,
|
||||
attrs.difficulty.hit_window,
|
||||
attrs.difficulty.max_combo,
|
||||
attrs.difficulty.stars,
|
||||
attrs.pp_difficulty,
|
||||
attrs.pp_acc,
|
||||
attrs.effective_miss_count,
|
||||
attrs.pp,
|
||||
);
|
||||
println!("{attrs:#?}");
|
||||
|
||||
// println!(
|
||||
// "difficulty:\n\
|
||||
// stamina={}\n\
|
||||
// rhythm={}\n\
|
||||
// colour={}\n\
|
||||
// peak={}\n\
|
||||
// hit_window={}\n\
|
||||
// max_combo={}\n\
|
||||
// stars={}\n\
|
||||
// performance:\n\
|
||||
// difficulty={}\n\
|
||||
// acc={}\n\
|
||||
// effective={}\n\
|
||||
// pp={}\n",
|
||||
// attrs.difficulty.stamina,
|
||||
// attrs.difficulty.rhythm,
|
||||
// attrs.difficulty.colour,
|
||||
// attrs.difficulty.peak,
|
||||
// attrs.difficulty.hit_window,
|
||||
// attrs.difficulty.max_combo,
|
||||
// attrs.difficulty.stars,
|
||||
// attrs.pp_difficulty,
|
||||
// attrs.pp_acc,
|
||||
// attrs.effective_miss_count,
|
||||
// attrs.pp,
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::{
|
||||
difficulty_object::ManiaDifficultyObject,
|
||||
@@ -52,7 +57,6 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
|
||||
/// Create a new difficulty attributes iterator for osu!mania maps.
|
||||
pub fn new(map: &'map Beatmap, mods: u32) -> Self {
|
||||
let total_columns = map.cs.round_even().max(1.0);
|
||||
|
||||
let clock_rate = mods.clock_rate();
|
||||
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 base = ManiaObject::new(h, total_columns, &mut params);
|
||||
let diff_object = ManiaDifficultyObject::new(&base, &*last, clock_rate, i);
|
||||
@@ -90,17 +96,6 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
|
||||
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));
|
||||
diff_objects.extend(diff_objects_iter);
|
||||
|
||||
@@ -114,6 +109,24 @@ impl<'map> ManiaGradualDifficultyAttributes<'map> {
|
||||
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<'_> {
|
||||
@@ -124,16 +137,7 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
|
||||
self.idx += 1;
|
||||
|
||||
if let Some(h) = self.map.hit_objects.get(self.idx) {
|
||||
match &h.kind {
|
||||
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::increment_combo(h, curr, &mut self.curr_combo, self.clock_rate);
|
||||
}
|
||||
|
||||
self.strain.process(curr, &self.diff_objects);
|
||||
@@ -151,6 +155,23 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
|
||||
|
||||
(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<'_> {
|
||||
@@ -159,29 +180,3 @@ impl ExactSizeIterator for ManiaGradualDifficultyAttributes<'_> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ use crate::{Beatmap, ManiaPP};
|
||||
|
||||
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)]
|
||||
pub struct ManiaScoreState {
|
||||
/// Amount of current 320s.
|
||||
@@ -65,7 +68,7 @@ impl ManiaScoreState {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use rosu_pp::{Beatmap, mania::ManiaGradualPerformanceAttributes};
|
||||
/// use rosu_pp::{Beatmap, mania::{ManiaGradualPerformanceAttributes, ManiaScoreState}};
|
||||
///
|
||||
/// # /*
|
||||
/// let map: Beatmap = ...
|
||||
@@ -74,45 +77,51 @@ impl ManiaScoreState {
|
||||
///
|
||||
/// let mods = 64; // DT
|
||||
/// 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 {
|
||||
/// score += 123;
|
||||
/// state.n320 += 1;
|
||||
///
|
||||
/// # /*
|
||||
/// let performance = gradual_perf.process_next_object(score).unwrap();
|
||||
/// 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();
|
||||
/// 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.
|
||||
/// score += 987;
|
||||
/// // The next 10 objects will be a mixture of 320s and 100s.
|
||||
/// // 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();
|
||||
/// 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
|
||||
/// # /*
|
||||
/// score = ...
|
||||
/// let final_performance = gradual_perf.process_next_n_objects(score, usize::MAX).unwrap();
|
||||
/// state.max_combo = ...
|
||||
/// 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);
|
||||
/// # */
|
||||
/// # 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,
|
||||
/// // 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)]
|
||||
pub struct ManiaGradualPerformanceAttributes<'map> {
|
||||
@@ -152,140 +161,17 @@ impl<'map> ManiaGradualPerformanceAttributes<'map> {
|
||||
state: ManiaScoreState,
|
||||
n: usize,
|
||||
) -> Option<ManiaPerformanceAttributes> {
|
||||
let n = n.min(self.difficulty.len()).saturating_sub(1);
|
||||
let difficulty = self.difficulty.nth(n)?;
|
||||
|
||||
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 sub = (self.difficulty.idx == 0) as usize;
|
||||
let difficulty = self.difficulty.nth(n.saturating_sub(sub))?;
|
||||
|
||||
let performance = self
|
||||
.performance
|
||||
.clone()
|
||||
.attributes(difficulty)
|
||||
.state(state)
|
||||
.passed_objects(self.difficulty.idx)
|
||||
.calculate();
|
||||
|
||||
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
@@ -175,7 +175,7 @@ fn calculate_result(params: ManiaStars<'_>) -> ManiaResult {
|
||||
let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate());
|
||||
let mut strain = Strain::new(total_columns as usize);
|
||||
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() {
|
||||
Some(h) => ManiaObject::new(h, total_columns, &mut params),
|
||||
@@ -247,6 +247,12 @@ impl ManiaPerformanceAttributes {
|
||||
pub fn pp(&self) -> f64 {
|
||||
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 {
|
||||
|
||||
+647
-72
@@ -20,15 +20,21 @@ use crate::{
|
||||
///
|
||||
/// let pp_result = ManiaPP::new(&map)
|
||||
/// .mods(64) // DT
|
||||
/// .score(765_432)
|
||||
/// .n_misses(1)
|
||||
/// .accuracy(98.5)
|
||||
/// .calculate();
|
||||
///
|
||||
/// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars());
|
||||
///
|
||||
/// let next_result = ManiaPP::new(&map)
|
||||
/// .attributes(pp_result) // reusing previous results for performance
|
||||
/// .mods(8 + 64) // has to be the same to reuse attributes
|
||||
/// .score(950_000)
|
||||
/// .attributes(pp_result) // reusing previous results for performance
|
||||
/// .mods(8 + 64) // has to be the same to reuse attributes
|
||||
/// .n320(2000)
|
||||
/// .n300(500)
|
||||
/// .n200(200)
|
||||
/// .n100(100)
|
||||
/// .n50(10)
|
||||
/// .n_misses(1)
|
||||
/// .calculate();
|
||||
///
|
||||
/// 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.
|
||||
#[inline]
|
||||
pub fn accuracy(mut self, acc: f64) -> Self {
|
||||
self.acc = Some(acc);
|
||||
self.acc = Some(acc / 100.0);
|
||||
|
||||
self
|
||||
}
|
||||
@@ -238,110 +244,258 @@ impl<'map> ManiaPP<'map> {
|
||||
let n_objects = self.passed_objects.unwrap_or(self.map.hit_objects.len());
|
||||
let priority = self.hitresult_priority.unwrap_or_default();
|
||||
|
||||
let mut state = ManiaScoreState {
|
||||
n320: self.n320.unwrap_or(0),
|
||||
n300: self.n300.unwrap_or(0),
|
||||
n200: self.n200.unwrap_or(0),
|
||||
n100: self.n100.unwrap_or(0),
|
||||
n50: self.n50.unwrap_or(0),
|
||||
n_misses: self.n_misses.unwrap_or(0),
|
||||
};
|
||||
let mut n320 = self.n320.unwrap_or(0);
|
||||
let mut n300 = self.n300.unwrap_or(0);
|
||||
let mut n200 = self.n200.unwrap_or(0);
|
||||
let mut n100 = self.n100.unwrap_or(0);
|
||||
let mut n50 = self.n50.unwrap_or(0);
|
||||
let n_misses = self.n_misses.unwrap_or(0);
|
||||
|
||||
if let Some(acc) = self.acc {
|
||||
// TODO: test
|
||||
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() {
|
||||
delta /= 2;
|
||||
}
|
||||
|
||||
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;
|
||||
match priority {
|
||||
HitResultPriority::BestCase => n320 += remaining,
|
||||
HitResultPriority::WorstCase => n50 += remaining,
|
||||
}
|
||||
}
|
||||
(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
|
||||
if self.n320.or(self.n300).or(self.n100).or(self.n50).is_none() {
|
||||
let n = (state.n320 + state.n300).min(state.n50 / 4);
|
||||
n100 = delta % 5;
|
||||
n50 = n_objects.saturating_sub(n3x0 + n200 + n100 + n_misses);
|
||||
|
||||
if n <= state.n300 {
|
||||
state.n300 -= n;
|
||||
let curr_total = 6 * n3x0 + 4 * n200 + 2 * n100 + n50;
|
||||
|
||||
if curr_total < target_total {
|
||||
let n = (target_total - curr_total).min(n50);
|
||||
n50 -= n;
|
||||
n100 += n;
|
||||
} else {
|
||||
state.n320 -= n - state.n300;
|
||||
state.n300 = 0;
|
||||
};
|
||||
let n = (curr_total - target_total).min(n100);
|
||||
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;
|
||||
state.n50 -= 4 * n;
|
||||
n200 = delta / 3;
|
||||
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 {
|
||||
let remaining = n_objects.saturating_sub(state.total_hits());
|
||||
let remaining = n_objects.saturating_sub(n320 + n300 + n200 + n100 + n50 + n_misses);
|
||||
|
||||
match priority {
|
||||
HitResultPriority::BestCase => {
|
||||
if self.n320.is_none() {
|
||||
state.n320 = remaining;
|
||||
n320 = remaining;
|
||||
} else if self.n300.is_none() {
|
||||
state.n300 = remaining;
|
||||
n300 = remaining;
|
||||
} else if self.n200.is_none() {
|
||||
state.n200 = remaining;
|
||||
n200 = remaining;
|
||||
} else if self.n100.is_none() {
|
||||
state.n100 = remaining;
|
||||
n100 = remaining;
|
||||
} else if self.n50.is_none() {
|
||||
state.n50 = remaining;
|
||||
n50 = remaining;
|
||||
} else {
|
||||
state.n320 = remaining;
|
||||
n320 += remaining;
|
||||
}
|
||||
}
|
||||
HitResultPriority::WorstCase => {
|
||||
if self.n50.is_none() {
|
||||
state.n50 = remaining;
|
||||
n50 = remaining;
|
||||
} else if self.n100.is_none() {
|
||||
state.n100 = remaining;
|
||||
n100 = remaining;
|
||||
} else if self.n200.is_none() {
|
||||
state.n200 = remaining;
|
||||
n200 = remaining;
|
||||
} else if self.n300.is_none() {
|
||||
state.n300 = remaining;
|
||||
n300 = remaining;
|
||||
} else if self.n320.is_none() {
|
||||
state.n320 = remaining;
|
||||
n320 = remaining;
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,9 @@ use super::{
|
||||
pub struct OsuGradualDifficultyAttributes {
|
||||
pub(crate) idx: usize,
|
||||
mods: u32,
|
||||
attributes: OsuDifficultyAttributes,
|
||||
attrs: OsuDifficultyAttributes,
|
||||
// Unused but `diff_objects`' lifetimes secretly depend on it
|
||||
#[allow(unused)]
|
||||
hit_objects: Vec<OsuObject>,
|
||||
diff_objects: Vec<OsuDifficultyObject<'static>>,
|
||||
skills: [Box<dyn Skill>; 4],
|
||||
@@ -60,8 +62,8 @@ impl Debug for OsuGradualDifficultyAttributes {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
f.debug_struct("OsuGradualDifficultyAttributes")
|
||||
.field("idx", &self.idx)
|
||||
.field("attributes", &self.attributes)
|
||||
.field("hit_objects", &self.hit_objects)
|
||||
.field("attrs", &self.attrs)
|
||||
.field("diff_objects", &self.diff_objects)
|
||||
.field("skills", &"<cannot be displayed>")
|
||||
.finish()
|
||||
}
|
||||
@@ -75,7 +77,7 @@ impl OsuGradualDifficultyAttributes {
|
||||
let scaling_factor = ScalingFactor::new(map_attrs.cs);
|
||||
let hr = mods.hr();
|
||||
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
|
||||
// * which uniformly speeds up all animations game wide regardless of AR.
|
||||
@@ -100,7 +102,7 @@ impl OsuGradualDifficultyAttributes {
|
||||
|
||||
let mut params = ObjectParameters {
|
||||
map,
|
||||
attributes: &mut attrs,
|
||||
attrs: &mut attrs,
|
||||
ticks: Vec::new(),
|
||||
curve_bufs: CurveBuffers::default(),
|
||||
};
|
||||
@@ -140,7 +142,7 @@ impl OsuGradualDifficultyAttributes {
|
||||
return Self {
|
||||
idx: 0,
|
||||
mods,
|
||||
attributes: attrs,
|
||||
attrs,
|
||||
hit_objects: Vec::new(),
|
||||
diff_objects: Vec::new(),
|
||||
skills,
|
||||
@@ -149,6 +151,8 @@ impl OsuGradualDifficultyAttributes {
|
||||
}
|
||||
};
|
||||
|
||||
Self::increment_combo(last, &mut attrs);
|
||||
|
||||
let mut last_last = None;
|
||||
|
||||
// Prepare `lazy_travel_dist` and `lazy_end_pos` for `last` manually
|
||||
@@ -182,13 +186,26 @@ impl OsuGradualDifficultyAttributes {
|
||||
Self {
|
||||
idx: 0,
|
||||
mods,
|
||||
attributes: attrs,
|
||||
attrs,
|
||||
diff_objects: extend_lifetime(diff_objects),
|
||||
hit_objects,
|
||||
skills,
|
||||
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(
|
||||
@@ -210,18 +227,7 @@ impl Iterator for OsuGradualDifficultyAttributes {
|
||||
skill.process(curr, &self.diff_objects, self.hit_window);
|
||||
}
|
||||
|
||||
let mut attrs = self.attributes.clone();
|
||||
|
||||
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,
|
||||
}
|
||||
Self::increment_combo(curr.base, &mut self.attrs);
|
||||
|
||||
let [aim, aim_no_sliders, speed, flashlight] = &self.skills;
|
||||
|
||||
@@ -289,6 +295,7 @@ impl Iterator for OsuGradualDifficultyAttributes {
|
||||
0.0
|
||||
};
|
||||
|
||||
let mut attrs = self.attrs.clone();
|
||||
attrs.aim = aim_rating;
|
||||
attrs.speed = speed_rating;
|
||||
attrs.flashlight = flashlight_rating;
|
||||
@@ -301,41 +308,32 @@ impl Iterator for OsuGradualDifficultyAttributes {
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let len = self.hit_objects.len() - self.idx;
|
||||
let len = self.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 {
|
||||
#[inline]
|
||||
fn len(&self) -> usize {
|
||||
self.hit_objects.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
self.diff_objects.len() - self.idx
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ impl OsuScoreState {
|
||||
/// // Then comes a miss.
|
||||
/// // Note that state's max combo won't be incremented for
|
||||
/// // 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();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
@@ -126,7 +126,7 @@ impl OsuScoreState {
|
||||
/// state.n300 = ...
|
||||
/// state.n100 = ...
|
||||
/// state.n50 = ...
|
||||
/// state.misses = ...
|
||||
/// state.n_misses = ...
|
||||
/// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
/// # */
|
||||
@@ -174,118 +174,17 @@ impl<'map> OsuGradualPerformanceAttributes<'map> {
|
||||
state: OsuScoreState,
|
||||
n: usize,
|
||||
) -> Option<OsuPerformanceAttributes> {
|
||||
let n = n.min(self.difficulty.len()).saturating_sub(1);
|
||||
let difficulty = self.difficulty.nth(n)?;
|
||||
let sub = (self.difficulty.idx == 0) as usize;
|
||||
let difficulty = self.difficulty.nth(n.saturating_sub(sub))?;
|
||||
|
||||
let performance = self
|
||||
.performance
|
||||
.clone()
|
||||
.attributes(difficulty)
|
||||
.state(state)
|
||||
.passed_objects(self.difficulty.idx)
|
||||
.passed_objects(self.difficulty.idx + 1)
|
||||
.calculate();
|
||||
|
||||
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
@@ -260,7 +260,7 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
|
||||
400.0 * (time_preempt / PREEMPT_MIN).min(1.0)
|
||||
};
|
||||
|
||||
let mut attributes = OsuDifficultyAttributes {
|
||||
let mut attrs = OsuDifficultyAttributes {
|
||||
ar: map_attrs.ar,
|
||||
hp: map_attrs.hp,
|
||||
od: map_attrs.od,
|
||||
@@ -269,7 +269,7 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
|
||||
|
||||
let mut params = ObjectParameters {
|
||||
map,
|
||||
attributes: &mut attributes,
|
||||
attrs: &mut attrs,
|
||||
ticks: Vec::new(),
|
||||
curve_bufs: CurveBuffers::default(),
|
||||
};
|
||||
@@ -301,7 +301,7 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
|
||||
|
||||
let last = match hit_objects.next() {
|
||||
Some(prev) => prev,
|
||||
None => return (skills, attributes),
|
||||
None => return (skills, attrs),
|
||||
};
|
||||
|
||||
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) {
|
||||
|
||||
@@ -86,7 +86,7 @@ pub(crate) enum NestedObjectKind {
|
||||
|
||||
pub(crate) struct ObjectParameters<'a> {
|
||||
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) curve_bufs: CurveBuffers,
|
||||
}
|
||||
@@ -95,17 +95,17 @@ impl OsuObject {
|
||||
pub(crate) fn new(h: &HitObject, params: &mut ObjectParameters<'_>) -> Option<Self> {
|
||||
let ObjectParameters {
|
||||
map,
|
||||
attributes,
|
||||
attrs,
|
||||
ticks,
|
||||
curve_bufs,
|
||||
} = params;
|
||||
|
||||
attributes.max_combo += 1; // hitcircle, slider head, or spinner
|
||||
attrs.max_combo += 1; // hitcircle, slider head, or spinner
|
||||
let pos = h.pos;
|
||||
|
||||
let obj = match &h.kind {
|
||||
HitObjectKind::Circle => {
|
||||
attributes.n_circles += 1;
|
||||
attrs.n_circles += 1;
|
||||
|
||||
Self {
|
||||
start_time: h.start_time,
|
||||
@@ -121,7 +121,7 @@ impl OsuObject {
|
||||
control_points,
|
||||
..
|
||||
} => {
|
||||
attributes.n_sliders += 1;
|
||||
attrs.n_sliders += 1;
|
||||
|
||||
let timing_point = map.timing_point_at(h.start_time);
|
||||
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),
|
||||
};
|
||||
|
||||
attributes.max_combo += nested_objects.len();
|
||||
attrs.max_combo += nested_objects.len();
|
||||
|
||||
let last_time = nested_objects
|
||||
.last()
|
||||
@@ -318,7 +318,7 @@ impl OsuObject {
|
||||
}
|
||||
}
|
||||
HitObjectKind::Spinner { end_time } => {
|
||||
attributes.n_spinners += 1;
|
||||
attrs.n_spinners += 1;
|
||||
|
||||
Self {
|
||||
start_time: h.start_time,
|
||||
|
||||
+29
-49
@@ -21,15 +21,15 @@ use crate::{
|
||||
/// let pp_result = OsuPP::new(&map)
|
||||
/// .mods(8 + 64) // HDDT
|
||||
/// .combo(1234)
|
||||
/// .misses(1)
|
||||
/// .n_misses(1)
|
||||
/// .accuracy(98.5) // should be set last
|
||||
/// .calculate();
|
||||
///
|
||||
/// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars());
|
||||
///
|
||||
/// let next_result = OsuPP::new(&map)
|
||||
/// .attributes(pp_result) // reusing previous results for performance
|
||||
/// .mods(8 + 64) // has to be the same to reuse attributes
|
||||
/// .attributes(pp_result) // reusing previous results for performance
|
||||
/// .mods(8 + 64) // has to be the same to reuse attributes
|
||||
/// .accuracy(99.5)
|
||||
/// .calculate();
|
||||
///
|
||||
@@ -739,13 +739,17 @@ impl OsuAttributeProvider for PerformanceAttributes {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::Beatmap;
|
||||
|
||||
fn test_attrs() -> OsuDifficultyAttributes {
|
||||
OsuDifficultyAttributes {
|
||||
fn test_data() -> (Beatmap, OsuDifficultyAttributes) {
|
||||
let path = "./maps/2785319.osu";
|
||||
let map = Beatmap::from_path(path).unwrap();
|
||||
|
||||
let attrs = OsuDifficultyAttributes {
|
||||
aim: 2.8693628443424104,
|
||||
speed: 2.533869745015772,
|
||||
flashlight: 2.288770487900865,
|
||||
@@ -759,38 +763,14 @@ mod test {
|
||||
n_spinners: 1,
|
||||
stars: 5.669858729379631,
|
||||
max_combo: 909,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
|
||||
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 }
|
||||
}};
|
||||
(map, attrs)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osu_hitresults_n300_n100_n_misses_best() {
|
||||
let (map, attrs) = test_data!();
|
||||
fn hitresults_n300_n100_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
@@ -814,8 +794,8 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osu_hitresults_n300_n50_n_misses_best() {
|
||||
let (map, attrs) = test_data!();
|
||||
fn hitresults_n300_n50_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
@@ -839,8 +819,8 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osu_hitresults_n50_n_misses_worst() {
|
||||
let (map, attrs) = test_data!();
|
||||
fn hitresults_n50_n_misses_worst() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
@@ -863,8 +843,8 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osu_hitresults_n300_n100_n50_n_misses_worst() {
|
||||
let (map, attrs) = test_data!();
|
||||
fn hitresults_n300_n100_n50_n_misses_worst() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
@@ -889,8 +869,8 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osu_hitresults_acc_n_misses_best() {
|
||||
let (map, attrs) = test_data!();
|
||||
fn hitresults_acc_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
@@ -919,8 +899,8 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osu_hitresults_acc_n100_n_misses_best() {
|
||||
let (map, attrs) = test_data!();
|
||||
fn hitresults_acc_n100_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
@@ -950,8 +930,8 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osu_hitresults_acc_n50_n_misses_best() {
|
||||
let (map, attrs) = test_data!();
|
||||
fn hitresults_acc_n50_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
@@ -981,8 +961,8 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osu_hitresults_acc_best() {
|
||||
let (map, attrs) = test_data!();
|
||||
fn hitresults_acc_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
@@ -1010,8 +990,8 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osu_hitresults_acc_worst() {
|
||||
let (map, attrs) = test_data!();
|
||||
fn hitresults_acc_worst() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::{
|
||||
/// let pp_result = AnyPP::new(&map)
|
||||
/// .mods(8 + 64) // HDDT
|
||||
/// .combo(1234)
|
||||
/// .misses(1)
|
||||
/// .n_misses(1)
|
||||
/// .accuracy(98.5) // should be set last
|
||||
/// .calculate();
|
||||
///
|
||||
@@ -287,10 +287,10 @@ impl AttributeProvider for PerformanceAttributes {
|
||||
#[inline]
|
||||
fn attributes(self) -> DifficultyAttributes {
|
||||
match self {
|
||||
Self::Catch(f) => DifficultyAttributes::Catch(f.difficulty),
|
||||
Self::Mania(m) => DifficultyAttributes::Mania(m.difficulty),
|
||||
Self::Osu(o) => DifficultyAttributes::Osu(o.difficulty),
|
||||
Self::Taiko(t) => DifficultyAttributes::Taiko(t.difficulty),
|
||||
Self::Catch(attrs) => DifficultyAttributes::Catch(attrs.difficulty),
|
||||
Self::Mania(attrs) => DifficultyAttributes::Mania(attrs.difficulty),
|
||||
Self::Osu(attrs) => DifficultyAttributes::Osu(attrs.difficulty),
|
||||
Self::Taiko(attrs) => DifficultyAttributes::Taiko(attrs.difficulty),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ use super::{
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TaikoGradualDifficultyAttributes {
|
||||
pub(crate) idx: usize,
|
||||
attrs: TaikoDifficultyAttributes,
|
||||
hit_objects: IntoIter<Rc<RefCell<TaikoDifficultyObject>>>,
|
||||
lists: ObjectLists,
|
||||
@@ -61,7 +60,7 @@ impl TaikoGradualDifficultyAttributes {
|
||||
.clock_rate(clock_rate)
|
||||
.hit_windows();
|
||||
|
||||
let attrs = TaikoDifficultyAttributes {
|
||||
let mut attrs = TaikoDifficultyAttributes {
|
||||
stamina: 0.0,
|
||||
rhythm: 0.0,
|
||||
colour: 0.0,
|
||||
@@ -71,15 +70,27 @@ impl TaikoGradualDifficultyAttributes {
|
||||
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
|
||||
.taiko_objects()
|
||||
.enumerate()
|
||||
.skip(2)
|
||||
.zip(map.hit_objects.iter().skip(1))
|
||||
.zip(map.hit_objects.iter())
|
||||
.enumerate()
|
||||
.fold(
|
||||
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(
|
||||
base,
|
||||
base_start_time,
|
||||
@@ -109,25 +120,27 @@ impl TaikoGradualDifficultyAttributes {
|
||||
ColourDifficultyPreprocessor::process_and_assign(&mut diff_objects);
|
||||
|
||||
Self {
|
||||
idx: 0,
|
||||
hit_objects: diff_objects.all.clone().into_iter(),
|
||||
lists: diff_objects,
|
||||
peaks,
|
||||
attrs,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn passed_objects(&self) -> usize {
|
||||
self.lists.all.len() - self.hit_objects.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for TaikoGradualDifficultyAttributes {
|
||||
type Item = TaikoDifficultyAttributes;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let curr = self.hit_objects.next()?;
|
||||
|
||||
{
|
||||
let curr = curr.borrow();
|
||||
self.peaks.process(&curr, &self.lists);
|
||||
self.attrs.max_combo += curr.base.is_hit as usize;
|
||||
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;
|
||||
}
|
||||
|
||||
let PeaksDifficultyValues {
|
||||
@@ -174,6 +187,19 @@ impl Iterator for TaikoGradualDifficultyAttributes {
|
||||
|
||||
(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 {
|
||||
@@ -182,29 +208,3 @@ impl ExactSizeIterator for TaikoGradualDifficultyAttributes {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ impl TaikoScoreState {
|
||||
/// // Then comes a miss.
|
||||
/// // Note that state's max combo won't be incremented for
|
||||
/// // 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();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
@@ -122,7 +122,7 @@ impl TaikoScoreState {
|
||||
/// state.max_combo = ...
|
||||
/// state.n300 = ...
|
||||
/// state.n100 = ...
|
||||
/// state.misses = ...
|
||||
/// state.n_misses = ...
|
||||
/// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
/// # */
|
||||
@@ -170,115 +170,17 @@ impl<'map> TaikoGradualPerformanceAttributes<'map> {
|
||||
state: TaikoScoreState,
|
||||
n: usize,
|
||||
) -> Option<TaikoPerformanceAttributes> {
|
||||
let n = n.min(self.difficulty.len()).saturating_sub(1);
|
||||
let difficulty = self.difficulty.nth(n)?;
|
||||
let sub = 2 * (self.difficulty.passed_objects() == 0) as usize;
|
||||
let difficulty = self.difficulty.nth(n.saturating_sub(sub))?;
|
||||
|
||||
let performance = self
|
||||
.performance
|
||||
.clone()
|
||||
.attributes(difficulty)
|
||||
.state(state)
|
||||
.passed_objects(self.difficulty.idx)
|
||||
.passed_objects(self.difficulty.passed_objects() + 2)
|
||||
.calculate();
|
||||
|
||||
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
@@ -20,15 +20,15 @@ use crate::{
|
||||
/// let pp_result = TaikoPP::new(&map)
|
||||
/// .mods(8 + 64) // HDDT
|
||||
/// .combo(1234)
|
||||
/// .misses(1)
|
||||
/// .n_misses(1)
|
||||
/// .accuracy(98.5)
|
||||
/// .calculate();
|
||||
///
|
||||
/// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars());
|
||||
///
|
||||
/// let next_result = TaikoPP::new(&map)
|
||||
/// .attributes(pp_result) // reusing previous results for performance
|
||||
/// .mods(8 + 64) // has to be the same to reuse attributes
|
||||
/// .attributes(pp_result) // reusing previous results for performance
|
||||
/// .mods(8 + 64) // has to be the same to reuse attributes
|
||||
/// .accuracy(99.5)
|
||||
/// .calculate();
|
||||
///
|
||||
@@ -225,7 +225,6 @@ impl<'map> TaikoPP<'map> {
|
||||
let n_misses = self.n_misses.unwrap_or(0);
|
||||
|
||||
if let Some(acc) = self.acc {
|
||||
// TODO: test
|
||||
match (self.n300, self.n100) {
|
||||
(Some(_), Some(_)) => {
|
||||
let remaining = total_result_count.saturating_sub(n300 + n100 + n_misses);
|
||||
@@ -249,15 +248,19 @@ impl<'map> TaikoPP<'map> {
|
||||
match priority {
|
||||
HitResultPriority::BestCase => match (self.n300, self.n100) {
|
||||
(Some(_), None) => n100 = remaining,
|
||||
_ => n300 = remaining,
|
||||
(Some(_), Some(_)) => n300 += remaining,
|
||||
(None, _) => n300 = remaining,
|
||||
},
|
||||
HitResultPriority::WorstCase => match (self.n300, self.n100) {
|
||||
(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 {
|
||||
max_combo,
|
||||
n300,
|
||||
@@ -462,61 +465,100 @@ impl TaikoAttributeProvider for PerformanceAttributes {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::Beatmap;
|
||||
|
||||
fn test_attrs() -> TaikoDifficultyAttributes {
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
|
||||
fn test_map() -> (Beatmap, TaikoDifficultyAttributes) {
|
||||
fn test_data() -> (Beatmap, TaikoDifficultyAttributes) {
|
||||
let path = "./maps/1028484.osu";
|
||||
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"))]
|
||||
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 }
|
||||
}};
|
||||
(map, attrs)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn taiko_hitresults_n300_n_misses_best() {
|
||||
let (map, attrs) = test_data!();
|
||||
fn hitresults_n300_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = TaikoPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.combo(500)
|
||||
.n300(300)
|
||||
.combo(100)
|
||||
.n300(150)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults(max_combo);
|
||||
|
||||
let expected = TaikoScoreState {
|
||||
max_combo: 500,
|
||||
n300: 300,
|
||||
n100: 20,
|
||||
max_combo: 100,
|
||||
n300: 150,
|
||||
n100: 137,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,12 @@ impl Peaks {
|
||||
}
|
||||
|
||||
pub(crate) fn difficulty_values(self) -> PeaksDifficultyValues {
|
||||
let colour_rating =
|
||||
StrainSkill::difficulty_value(self.colour.clone()) * Self::COLOUR_SKILL_MULTIPLIER;
|
||||
let rhythm_rating =
|
||||
StrainSkill::difficulty_value(self.rhythm.clone()) * Self::RHYTHM_SKILL_MULTIPLIER;
|
||||
let stamina_rating =
|
||||
StrainSkill::difficulty_value(self.stamina.clone()) * Self::STAMINA_SKILL_MULTIPLIER;
|
||||
let colour_rating = <Colour as StrainSkill>::difficulty_value(self.colour.clone())
|
||||
* Self::COLOUR_SKILL_MULTIPLIER;
|
||||
let rhythm_rating = <Rhythm as StrainSkill>::difficulty_value(self.rhythm.clone())
|
||||
* Self::RHYTHM_SKILL_MULTIPLIER;
|
||||
let stamina_rating = <Stamina as StrainSkill>::difficulty_value(self.stamina.clone())
|
||||
* Self::STAMINA_SKILL_MULTIPLIER;
|
||||
|
||||
PeaksDifficultyValues {
|
||||
colour_rating,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user