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