start base

This commit is contained in:
osu!Akatsuki Developer
2022-11-03 18:36:51 +00:00
parent 4137520bbb
commit ceb91025fa
15 changed files with 78 additions and 1046 deletions
+5 -6
View File
@@ -1,12 +1,11 @@
[package]
name = "rosu-pp"
version = "0.9.1"
authors = ["MaxOhn <ohn.m@hotmail.de>"]
edition = "2018"
name = "akatsuki-pp-rs"
version = "0.9.2"
authors = ["MaxOhn <ohn.m@hotmail.de>", "tsunyoku <tsunyoku@gmail.com>"]
edition = "2021"
license = "MIT"
readme = "README.md"
repository = "https://github.com/MaxOhn/rosu-pp"
documentation = "https://docs.rs/rosu-pp/"
repository = "https://github.com/osuAkatsuki/akatsuki-pp-rs"
description = "osu! difficulty and pp calculation for all modes"
keywords = ["osu", "pp", "stars", "async"]
+12 -1
View File
@@ -138,13 +138,18 @@ impl<'map> OsuStars<'map> {
1.0
};
let aim_difficult_strain_count = skills::OsuStrainSkill::count_difficult_strains(&mut aim);
// currently unused as relax pp doesn't include speed
let speed_difficult_strain_count =
skills::OsuStrainSkill::count_difficult_strains(&mut speed);
if mods.td() {
aim_rating = aim_rating.powf(0.8);
flashlight_rating = flashlight_rating.powf(0.8);
}
if mods.rx() {
aim_rating *= 0.9;
speed_rating = 0.0;
flashlight_rating *= 0.7;
}
@@ -176,6 +181,8 @@ impl<'map> OsuStars<'map> {
attrs.speed = speed_rating;
attrs.flashlight = flashlight_rating;
attrs.slider_factor = slider_factor;
attrs.aim_difficult_strain_count = aim_difficult_strain_count;
attrs.speed_difficult_strain_count = speed_difficult_strain_count;
attrs.stars = star_rating;
attrs.speed_note_count = speed_notes;
@@ -508,6 +515,10 @@ pub struct OsuDifficultyAttributes {
pub flashlight: f64,
/// The ratio of the aim strain with and without considering sliders
pub slider_factor: f64,
/// The count of difficult aim strains.
pub aim_difficult_strain_count: f64,
/// The count of difficult speed strains.
pub speed_difficult_strain_count: f64,
/// The number of clickable objects weighted by difficulty.
pub speed_note_count: f64,
/// The approach rate.
+25 -318
View File
@@ -388,7 +388,7 @@ struct OsuPpInner {
}
impl OsuPpInner {
fn calculate(mut self) -> OsuPerformanceAttributes {
fn calculate(self) -> OsuPerformanceAttributes {
let total_hits = self.state.total_hits();
if total_hits == 0 {
@@ -410,28 +410,6 @@ impl OsuPpInner {
multiplier *= 1.0 - (self.attrs.n_spinners as f64 / total_hits).powf(0.85);
}
if self.mods.rx() {
// * https://www.desmos.com/calculator/bc9eybdthb
// * we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0
// * this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11)
let (n100_mult, n50_mult) = if self.attrs.od > 0.0 {
(
1.0 - (self.attrs.od / 13.33).powf(1.8),
1.0 - (self.attrs.od / 13.33).powi(5),
)
} else {
(1.0, 1.0)
};
// * As we're adding Oks and Mehs to an approximated number of combo breaks the result can be
// * higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
self.effective_miss_count = (self.effective_miss_count
+ self.state.n100 as f64
+ n100_mult
+ self.state.n50 as f64 * n50_mult)
.min(total_hits);
}
let aim_value = self.compute_aim_value();
let speed_value = self.compute_speed_value();
let acc_value = self.compute_accuracy_value();
@@ -460,25 +438,30 @@ impl OsuPpInner {
let total_hits = self.total_hits();
let len_bonus = 0.95
+ 0.4 * (total_hits / 2000.0).min(1.0)
+ (total_hits > 2000.0) as u8 as f64 * (total_hits / 2000.0).log10() * 0.5;
let mut len_bonus = 1.0;
if self.mods.rx() {
if self.effective_miss_count > 0.0 {
aim_value *= self.calculate_miss_penalty(self.attrs.aim_difficult_strain_count);
}
} else {
len_bonus = 0.95
+ 0.4 * (total_hits / 2000.0).min(1.0)
+ (total_hits > 2000.0) as u8 as f64 * (total_hits / 2000.0).log10() * 0.5;
aim_value *= len_bonus;
aim_value *= len_bonus;
// * Penalize misses by assessing # of misses relative to the total # of objects.
// * Default a 3% reduction for any # of misses.
if self.effective_miss_count > 0.0 {
aim_value *= 0.97
* (1.0 - (self.effective_miss_count / total_hits).powf(0.775))
.powf(self.effective_miss_count);
// * Penalize misses by assessing # of misses relative to the total # of objects.
// * Default a 3% reduction for any # of misses.
if self.effective_miss_count > 0.0 {
aim_value *= 0.97
* (1.0 - (self.effective_miss_count / total_hits).powf(0.775))
.powf(self.effective_miss_count);
}
aim_value *= self.get_combo_scaling_factor();
}
aim_value *= self.get_combo_scaling_factor();
let ar_factor = if self.mods.rx() {
0.0
} else if self.attrs.ar > 10.33 {
let ar_factor = if self.attrs.ar > 10.33 {
0.3 * (self.attrs.ar - 10.33)
} else if self.attrs.ar < 8.0 {
0.05 * (8.0 - self.attrs.ar)
@@ -671,6 +654,10 @@ impl OsuPpInner {
}
}
fn calculate_miss_penalty(&self, strain_count: f64) -> f64 {
0.94 / ((self.effective_miss_count / (2.0 * strain_count.sqrt())) + 1.0)
}
fn total_hits(&self) -> f64 {
self.state.total_hits() as f64
}
@@ -738,283 +725,3 @@ impl OsuAttributeProvider for PerformanceAttributes {
}
}
}
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
#[cfg(test)]
mod test {
use super::*;
use crate::Beatmap;
fn test_data() -> (Beatmap, OsuDifficultyAttributes) {
let path = "./maps/2785319.osu";
let map = Beatmap::from_path(path).unwrap();
let attrs = OsuDifficultyAttributes {
aim: 2.8693628443424104,
speed: 2.533869745015772,
flashlight: 2.288770487900865,
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.669858729379631,
max_combo: 909,
};
(map, attrs)
}
#[test]
fn hitresults_n300_n100_n_misses_best() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = OsuPP::new(&map)
.attributes(attrs)
.combo(500)
.n300(300)
.n100(20)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults(max_combo);
let expected = OsuScoreState {
max_combo: 500,
n300: 300,
n100: 20,
n50: 279,
n_misses: 2,
};
assert_eq!(state, expected);
}
#[test]
fn hitresults_n300_n50_n_misses_best() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = OsuPP::new(&map)
.attributes(attrs)
.combo(500)
.n300(300)
.n50(10)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults(max_combo);
let expected = OsuScoreState {
max_combo: 500,
n300: 300,
n100: 289,
n50: 10,
n_misses: 2,
};
assert_eq!(state, expected);
}
#[test]
fn hitresults_n50_n_misses_worst() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = OsuPP::new(&map)
.attributes(attrs)
.combo(500)
.n50(10)
.n_misses(2)
.hitresult_priority(HitResultPriority::WorstCase)
.generate_hitresults(max_combo);
let expected = OsuScoreState {
max_combo: 500,
n300: 0,
n100: 589,
n50: 10,
n_misses: 2,
};
assert_eq!(state, expected);
}
#[test]
fn hitresults_n300_n100_n50_n_misses_worst() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = OsuPP::new(&map)
.attributes(attrs)
.combo(500)
.n300(300)
.n100(50)
.n50(10)
.n_misses(2)
.hitresult_priority(HitResultPriority::WorstCase)
.generate_hitresults(max_combo);
let expected = OsuScoreState {
max_combo: 500,
n300: 300,
n100: 50,
n50: 249,
n_misses: 2,
};
assert_eq!(state, expected);
}
#[test]
fn hitresults_acc_n_misses_best() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = OsuPP::new(&map)
.attributes(attrs)
.combo(500)
.accuracy(98.0)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults(max_combo);
let expected = OsuScoreState {
max_combo: 500,
n300: 584,
n100: 15,
n50: 0,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
}
#[test]
fn hitresults_acc_n100_n_misses_best() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = OsuPP::new(&map)
.attributes(attrs)
.combo(500)
.accuracy(95.0)
.n100(15)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults(max_combo);
let expected = OsuScoreState {
max_combo: 500,
n300: 562,
n100: 15,
n50: 22,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
}
#[test]
fn hitresults_acc_n50_n_misses_best() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = OsuPP::new(&map)
.attributes(attrs)
.combo(500)
.accuracy(95.0)
.n50(10)
.n_misses(2)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults(max_combo);
let expected = OsuScoreState {
max_combo: 500,
n300: 560,
n100: 29,
n50: 10,
n_misses: 2,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
}
#[test]
fn hitresults_acc_best() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = OsuPP::new(&map)
.attributes(attrs)
.combo(500)
.accuracy(90.0)
.hitresult_priority(HitResultPriority::BestCase)
.generate_hitresults(max_combo);
let expected = OsuScoreState {
max_combo: 500,
n300: 511,
n100: 89,
n50: 1,
n_misses: 0,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
}
#[test]
fn hitresults_acc_worst() {
let (map, attrs) = test_data();
let max_combo = attrs.max_combo();
let state = OsuPP::new(&map)
.attributes(attrs)
.combo(500)
.accuracy(90.0)
.hitresult_priority(HitResultPriority::WorstCase)
.generate_hitresults(max_combo);
let expected = OsuScoreState {
max_combo: 500,
n300: 528,
n100: 4,
n50: 69,
n_misses: 0,
};
assert_eq!(
state,
expected,
"{}% vs {}%",
state.accuracy(),
expected.accuracy()
);
}
}
+17 -1
View File
@@ -11,6 +11,7 @@ pub(crate) struct Aim {
curr_section_end: f64,
pub(crate) strain_peaks: Vec<f64>,
with_sliders: bool,
pub(crate) object_strains: Vec<f64>,
}
impl Aim {
@@ -24,6 +25,7 @@ impl Aim {
curr_section_end: 0.0,
strain_peaks: Vec::new(),
with_sliders,
object_strains: Vec::new(),
}
}
@@ -74,6 +76,8 @@ impl StrainSkill for Aim {
self.curr_strain += AimEvaluator::evaluate_diff_of(curr, diff_objects, self.with_sliders)
* Self::SKILL_MULTIPLIER;
self.object_strains.push(self.curr_strain);
self.curr_strain
}
@@ -93,7 +97,19 @@ impl StrainSkill for Aim {
}
}
impl OsuStrainSkill for Aim {}
impl OsuStrainSkill for Aim {
fn count_difficult_strains(&mut self) -> f64 {
let top_strain = self
.object_strains
.iter()
.fold(f64::NEG_INFINITY, |prev, curr| prev.max(*curr));
self.object_strains
.iter()
.map(|strain| (strain / top_strain).powi(4))
.sum()
}
}
struct AimEvaluator;
+5 -1
View File
@@ -108,7 +108,11 @@ impl StrainSkill for Flashlight {
}
}
impl OsuStrainSkill for Flashlight {}
impl OsuStrainSkill for Flashlight {
fn count_difficult_strains(&mut self) -> f64 {
0.0
}
}
struct FlashlightEvaluator;
+12
View File
@@ -118,6 +118,18 @@ impl StrainSkill for Speed {
impl OsuStrainSkill for Speed {
const REDUCED_SECTION_COUNT: usize = 5;
const DIFFICULTY_MULTIPLER: f64 = 1.04;
fn count_difficult_strains(&mut self) -> f64 {
let top_strain = self
.object_strains
.iter()
.fold(f64::NEG_INFINITY, |prev, curr| prev.max(*curr));
self.object_strains
.iter()
.map(|strain| (strain / top_strain).powi(4))
.sum()
}
}
struct SpeedEvaluator;
+2
View File
@@ -90,6 +90,8 @@ pub(crate) trait OsuStrainSkill: StrainSkill + Sized {
const REDUCED_STRAIN_BASELINE: f64 = 0.75;
const DIFFICULTY_MULTIPLER: f64 = 1.06;
fn count_difficult_strains(&mut self) -> f64;
fn difficulty_value(&mut self) -> f64 {
let mut difficulty = 0.0;
let mut weight = 1.0;
-30
View File
@@ -1,30 +0,0 @@
mod mode;
use rosu_pp::Beatmap;
pub use self::mode::{Catch, Mania, Mode, Osu, Taiko};
#[macro_export]
#[rustfmt::skip]
macro_rules! test_map {
($mode:ident) => {{
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
{ common::test_map::<$mode>() }
#[cfg(any(feature = "async_tokio", feature = "async_str"))]
{ common::test_map::<$mode>().await }
}};
}
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
pub fn test_map<M: Mode>() -> Beatmap {
let path = format!("./maps/{}.osu", M::TEST_MAP_ID);
Beatmap::from_path(path).unwrap()
}
#[cfg(any(feature = "async_tokio", feature = "async_str"))]
pub async fn test_map() -> Beatmap {
let path = format!("./maps/{}.osu", M::TEST_MAP_ID);
Beatmap::from_path(path).await.unwrap()
}
-68
View File
@@ -1,68 +0,0 @@
use rosu_pp::{
catch::CatchDifficultyAttributes, mania::ManiaDifficultyAttributes,
osu::OsuDifficultyAttributes, taiko::TaikoDifficultyAttributes,
};
pub struct Osu;
pub struct Taiko;
pub struct Catch;
pub struct Mania;
pub trait Mode {
type DifficultyAttributes;
const TEST_MAP_ID: u32;
const TEST_DIFF_ATTRS: Self::DifficultyAttributes;
}
macro_rules! impl_mode {
( $( $mode:ident: $map_id:literal, $diff_attrs:ident $fields:tt; )* ) => {
$(
impl Mode for $mode {
type DifficultyAttributes = $diff_attrs;
const TEST_MAP_ID: u32 = $map_id;
const TEST_DIFF_ATTRS: Self::DifficultyAttributes = $diff_attrs $fields;
}
)*
};
}
impl_mode! {
Osu: 2785319, OsuDifficultyAttributes {
aim: 2.8693628443424104,
speed: 2.533869745015772,
flashlight: 2.288770487900865,
slider_factor: 0.9803052946037858,
speed_note_count: 210.36373973116545,
ar: 9.300000190734863,
od: 8.800000190734863,
hp: 5.0,
n_circles: 307,
n_sliders: 293,
n_spinners: 1,
stars: 5.669858729379628,
max_combo: 909,
};
Taiko: 1028484, TaikoDifficultyAttributes {
stamina: 1.4528845068865617,
rhythm: 0.20130047251681948,
colour: 1.0487315549761433,
peak: 1.8881824429738323,
hit_window: 35.0,
stars: 2.9778030386845606,
max_combo: 289,
};
Catch: 2118524, CatchDifficultyAttributes {
stars: 3.2502669316166624,
ar: 8.0,
n_fruits: 728,
n_droplets: 2,
n_tiny_droplets: 291,
};
Mania: 1974394, ManiaDifficultyAttributes {
stars: 4.824631127426499,
hit_window: 40.0,
max_combo: 5064,
};
}
-40
View File
@@ -1,40 +0,0 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))]
use common::{test_map, Osu};
use rosu_pp::{CatchStars, ManiaStars, OsuStars, TaikoStars};
use crate::common::{Catch, Mania, Mode, Taiko};
mod common;
#[test]
fn difficulty_osu() {
let map = test_map::<Osu>();
let attrs = OsuStars::new(&map).calculate();
assert_eq!(Osu::TEST_DIFF_ATTRS, attrs);
}
#[test]
fn difficulty_taiko() {
let map = test_map::<Taiko>();
let attrs = TaikoStars::new(&map).calculate();
assert_eq!(Taiko::TEST_DIFF_ATTRS, attrs);
}
#[test]
fn difficulty_catch() {
let map = test_map::<Catch>();
let attrs = CatchStars::new(&map).calculate();
assert_eq!(Catch::TEST_DIFF_ATTRS, attrs);
}
#[test]
fn difficulty_mania() {
let map = test_map::<Mania>();
let attrs = ManiaStars::new(&map).calculate();
assert_eq!(Mania::TEST_DIFF_ATTRS, attrs);
}
-119
View File
@@ -1,119 +0,0 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))]
use rosu_pp::{
catch::{CatchGradualDifficultyAttributes, CatchGradualPerformanceAttributes, CatchScoreState},
Beatmap, CatchPP, CatchStars,
};
use crate::common::Catch;
mod common;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = CatchGradualDifficultyAttributes::new(&map, 0);
assert!(attributes.next().is_none());
}
#[test]
fn iter_end_eq_regular() {
let map = test_map!(Catch);
let regular = CatchStars::new(&map).calculate();
let iter_end = CatchGradualDifficultyAttributes::new(&map, 0)
.last()
.expect("empty iter");
assert_eq!(regular, iter_end);
}
#[test]
fn correct_empty() {
let map = test_map!(Catch);
let mut gradual = CatchGradualPerformanceAttributes::new(&map, 0);
let state = CatchScoreState::default();
let first_attrs = gradual.process_next_n_objects(state.clone(), usize::MAX);
assert!(first_attrs.is_some());
assert!(gradual.process_next_object(state).is_none());
}
#[test]
fn next_and_next_n() {
let map = test_map!(Catch);
let state = CatchScoreState::default();
let mut gradual1 = CatchGradualPerformanceAttributes::new(&map, 0);
let mut gradual2 = CatchGradualPerformanceAttributes::new(&map, 0);
for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
}
let n = 80;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
}
let state = CatchScoreState {
max_combo: 101,
n_fruits: 99,
n_droplets: 2,
n_tiny_droplets: 68,
n_tiny_droplet_misses: 0,
n_misses: 0,
};
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
assert_eq!(next_n, next);
}
#[test]
fn gradual_end_eq_regular() {
let map = test_map!(Catch);
let regular = CatchPP::new(&map).calculate();
let mut gradual = CatchGradualPerformanceAttributes::new(&map, 0);
let state = CatchScoreState {
max_combo: 730,
n_fruits: 728,
n_droplets: 2,
n_tiny_droplets: 291,
n_tiny_droplet_misses: 0,
n_misses: 0,
};
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end);
}
#[test]
fn gradual_eq_regular_passed() {
let map = test_map!(Catch);
let n = 100;
let regular = CatchPP::new(&map).passed_objects(n).calculate();
let mut gradual = CatchGradualPerformanceAttributes::new(&map, 0);
let state = CatchScoreState {
max_combo: 101,
n_fruits: 99,
n_droplets: 2,
n_tiny_droplets: 68,
n_tiny_droplet_misses: 0,
n_misses: 0,
};
let gradual = gradual.process_next_n_objects(state, n).unwrap();
assert_eq!(regular, gradual);
}
-131
View File
@@ -1,131 +0,0 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))]
use rosu_pp::{
mania::{ManiaGradualDifficultyAttributes, ManiaGradualPerformanceAttributes, ManiaScoreState},
Beatmap, ManiaPP, ManiaStars,
};
use crate::common::Mania;
mod common;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = ManiaGradualDifficultyAttributes::new(&map, 0);
assert!(attributes.next().is_none());
}
#[test]
fn iter_end_eq_regular() {
let map = test_map!(Mania);
let regular = ManiaStars::new(&map).calculate();
let iter_end = ManiaGradualDifficultyAttributes::new(&map, 0)
.last()
.expect("empty iter");
assert_eq!(regular, iter_end);
}
#[test]
fn correct_empty() {
let map = test_map!(Mania);
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, 0);
let state = ManiaScoreState {
n320: 0,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let first_attrs = gradual.process_next_n_objects(state.clone(), usize::MAX);
assert!(first_attrs.is_some());
assert!(gradual.process_next_object(state).is_none());
}
#[test]
fn next_and_next_n() {
let map = test_map!(Mania);
let mut state = ManiaScoreState {
n320: 0,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let mut gradual1 = ManiaGradualPerformanceAttributes::new(&map, 0);
let mut gradual2 = ManiaGradualPerformanceAttributes::new(&map, 0);
for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
state.n320 += 1;
}
let n = 80;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
state.n320 += 1;
}
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
assert_eq!(next_n, next);
}
#[test]
fn gradual_end_eq_regular() {
let map = test_map!(Mania);
let regular = ManiaPP::new(&map).calculate();
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, 0);
let state = ManiaScoreState {
n320: 3238,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end);
}
#[test]
fn gradual_eq_regular_passed() {
let map = test_map!(Mania);
let n = 100;
let state = ManiaScoreState {
n320: 100,
n300: 0,
n200: 0,
n100: 0,
n50: 0,
n_misses: 0,
};
let regular = ManiaPP::new(&map)
.passed_objects(n)
.state(state.clone())
.calculate();
let mut gradual = ManiaGradualPerformanceAttributes::new(&map, 0);
let gradual = gradual.process_next_n_objects(state, n).unwrap();
assert_eq!(regular, gradual);
}
-115
View File
@@ -1,115 +0,0 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))]
use rosu_pp::{
osu::{OsuGradualDifficultyAttributes, OsuGradualPerformanceAttributes, OsuScoreState},
Beatmap, OsuPP, OsuStars,
};
use crate::common::Osu;
mod common;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = OsuGradualDifficultyAttributes::new(&map, 0);
assert!(attributes.next().is_none());
}
#[test]
fn iter_end_eq_regular() {
let map = test_map!(Osu);
let regular = OsuStars::new(&map).calculate();
let iter_end = OsuGradualDifficultyAttributes::new(&map, 0)
.last()
.expect("empty iter");
assert_eq!(regular, iter_end);
}
#[test]
fn correct_empty() {
let map = test_map!(Osu);
let mut gradual = OsuGradualPerformanceAttributes::new(&map, 0);
let state = OsuScoreState::default();
let first_attrs = gradual.process_next_n_objects(state.clone(), usize::MAX);
assert!(first_attrs.is_some());
assert!(gradual.process_next_object(state).is_none());
}
#[test]
fn next_and_next_n() {
let map = test_map!(Osu);
let state = OsuScoreState::default();
let mut gradual1 = OsuGradualPerformanceAttributes::new(&map, 0);
let mut gradual2 = OsuGradualPerformanceAttributes::new(&map, 0);
for _ in 0..20 {
let _ = gradual1.process_next_object(state.clone());
let _ = gradual2.process_next_object(state.clone());
}
let n = 80;
for _ in 1..n {
let _ = gradual1.process_next_object(state.clone());
}
let state = OsuScoreState {
max_combo: 122,
n300: 88,
n100: 8,
n50: 2,
n_misses: 2,
};
let next = gradual1.process_next_object(state.clone());
let next_n = gradual2.process_next_n_objects(state, n);
assert_eq!(next_n, next);
}
#[test]
fn gradual_end_eq_regular() {
let map = test_map!(Osu);
let regular = OsuPP::new(&map).calculate();
let mut gradual = OsuGradualPerformanceAttributes::new(&map, 0);
let state = OsuScoreState {
max_combo: 909,
n300: 601,
n100: 0,
n50: 0,
n_misses: 0,
};
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end);
}
#[test]
fn gradual_eq_regular_passed() {
let map = test_map!(Osu);
let n = 100;
let regular = OsuPP::new(&map).passed_objects(n).calculate();
let mut gradual = OsuGradualPerformanceAttributes::new(&map, 0);
let state = OsuScoreState {
max_combo: 122,
n300: 100,
n100: 0,
n50: 0,
n_misses: 0,
};
let gradual = gradual.process_next_n_objects(state, n).unwrap();
assert_eq!(regular, gradual);
}
-113
View File
@@ -1,113 +0,0 @@
#![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);
}
-103
View File
@@ -1,103 +0,0 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))]
use rosu_pp::GameMode;
use crate::common::{Catch, Mania, Osu, Taiko};
mod common;
#[test]
fn parse_osu() {
let map = test_map!(Osu);
assert_eq!(map.mode, GameMode::Osu);
assert_eq!(map.version, 14);
assert_eq!(map.n_circles, 307);
assert_eq!(map.n_sliders, 293);
assert_eq!(map.n_spinners, 1);
assert!((map.ar - 9.3).abs() <= f32::EPSILON);
assert!((map.od - 8.8).abs() <= f32::EPSILON);
assert!((map.cs - 4.5).abs() <= f32::EPSILON);
assert!((map.hp - 5.0).abs() <= f32::EPSILON);
assert!((map.slider_mult - 1.7).abs() <= f64::EPSILON);
assert!((map.tick_rate - 1.0).abs() <= f64::EPSILON);
assert_eq!(map.hit_objects.len(), 601);
assert_eq!(map.sounds.len(), 601);
assert_eq!(map.timing_points.len(), 1);
assert_eq!(map.difficulty_points.len(), 50);
assert_eq!(map.effect_points.len(), 131);
assert!((map.stack_leniency - 0.5).abs() <= f32::EPSILON);
assert_eq!(map.breaks.len(), 1)
}
#[test]
fn parse_taiko() {
let map = test_map!(Taiko);
assert_eq!(map.mode, GameMode::Taiko);
assert_eq!(map.version, 14);
assert_eq!(map.n_circles, 289);
assert_eq!(map.n_sliders, 4);
assert_eq!(map.n_spinners, 2);
assert!((map.ar - 8.0).abs() <= f32::EPSILON);
assert!((map.od - 5.0).abs() <= f32::EPSILON);
assert!((map.cs - 2.0).abs() <= f32::EPSILON);
assert!((map.hp - 6.0).abs() <= f32::EPSILON);
assert!((map.slider_mult - 1.4).abs() <= f64::EPSILON);
assert!((map.tick_rate - 1.0).abs() <= f64::EPSILON);
assert_eq!(map.hit_objects.len(), 295);
assert_eq!(map.sounds.len(), 295);
assert_eq!(map.timing_points.len(), 1);
assert_eq!(map.difficulty_points.len(), 3);
assert_eq!(map.effect_points.len(), 8);
assert!((map.stack_leniency - 0.7).abs() <= f32::EPSILON);
assert_eq!(map.breaks.len(), 0)
}
#[test]
fn parse_catch() {
let map = test_map!(Catch);
assert_eq!(map.mode, GameMode::Catch);
assert_eq!(map.version, 14);
assert_eq!(map.n_circles, 249);
assert_eq!(map.n_sliders, 227);
assert_eq!(map.n_spinners, 1);
assert!((map.ar - 8.0).abs() <= f32::EPSILON);
assert!((map.od - 8.0).abs() <= f32::EPSILON);
assert!((map.cs - 3.5).abs() <= f32::EPSILON);
assert!((map.hp - 5.0).abs() <= f32::EPSILON);
assert!((map.slider_mult - 1.45).abs() <= f64::EPSILON);
assert!((map.tick_rate - 1.0).abs() <= f64::EPSILON);
assert_eq!(map.hit_objects.len(), 477);
assert_eq!(map.sounds.len(), 477);
assert_eq!(map.timing_points.len(), 1);
assert_eq!(map.difficulty_points.len(), 0);
assert_eq!(map.effect_points.len(), 57);
assert!((map.stack_leniency - 0.7).abs() <= f32::EPSILON);
assert_eq!(map.breaks.len(), 0)
}
#[test]
fn parse_mania() {
let map = test_map!(Mania);
assert_eq!(map.mode, GameMode::Mania);
assert_eq!(map.version, 14);
assert_eq!(map.n_circles, 2815);
assert_eq!(map.n_sliders, 423);
assert_eq!(map.n_spinners, 0);
assert!((map.ar - 5.0).abs() <= f32::EPSILON);
assert!((map.od - 8.0).abs() <= f32::EPSILON);
assert!((map.cs - 4.0).abs() <= f32::EPSILON);
assert!((map.hp - 9.0).abs() <= f32::EPSILON);
assert!((map.slider_mult - 1.4).abs() <= f64::EPSILON);
assert!((map.tick_rate - 1.0).abs() <= f64::EPSILON);
assert_eq!(map.hit_objects.len(), 3238);
assert_eq!(map.sounds.len(), 3238);
assert_eq!(map.timing_points.len(), 1);
assert_eq!(map.difficulty_points.len(), 1740);
assert_eq!(map.effect_points.len(), 1762);
assert!((map.stack_leniency - 0.7).abs() <= f32::EPSILON);
assert_eq!(map.breaks.len(), 0)
}