initial rewrite

This commit is contained in:
MaxOhn
2024-02-09 17:54:14 +01:00
parent ecf1b2c7f4
commit a8bf9601b6
190 changed files with 10892 additions and 18142 deletions
-43
View File
@@ -1,43 +0,0 @@
use rosu_pp::{Beatmap, CatchPP, GameMode, ManiaPP, OsuPP, TaikoPP};
#[test]
fn osu() {
let map = Beatmap::default();
let _ = OsuPP::new(&map).calculate();
}
#[test]
fn taiko() {
let mut map = Beatmap::default();
// convert
let _ = TaikoPP::new(&map).calculate();
// regular
map.mode = GameMode::Taiko;
let _ = TaikoPP::new(&map).calculate();
}
#[test]
fn catch() {
let mut map = Beatmap::default();
// convert
let _ = CatchPP::new(&map).calculate();
// regular
map.mode = GameMode::Catch;
let _ = CatchPP::new(&map).calculate();
}
#[test]
fn mania() {
let mut map = Beatmap::default();
// convert
let _ = ManiaPP::new(&map).calculate();
// regular
map.mode = GameMode::Mania;
let _ = ManiaPP::new(&map).calculate();
}
+62
View File
@@ -0,0 +1,62 @@
// False positive
#[allow(unused)]
pub use self::{mods::*, paths::*};
/// Paths to .osu files
mod paths {
pub const OSU: &str = "./resources/2785319.osu";
pub const TAIKO: &str = "./resources/1028484.osu";
pub const CATCH: &str = "./resources/2118524.osu";
pub const MANIA: &str = "./resources/1638954.osu";
}
/// Bit values for mods
mod mods {
#![allow(unused)]
pub const NM: u32 = 0;
pub const NF: u32 = 1 << 0;
pub const EZ: u32 = 1 << 1;
pub const TD: u32 = 1 << 2;
pub const HD: u32 = 1 << 3;
pub const HR: u32 = 1 << 4;
pub const DT: u32 = 1 << 6;
pub const HT: u32 = 1 << 8;
pub const FL: u32 = 1 << 10;
pub const SO: u32 = 1 << 12;
}
#[track_caller]
pub fn assert_eq_float<F: Float>(a: F, b: F) {
assert!((a - b).abs() < F::EPSILON, "{a} != {b}")
}
/// Trait to provide flexibility in the `assert_eq_float` function.
pub trait Float:
Copy + std::fmt::Display + std::ops::Sub<Output = Self> + PartialOrd + Sized
{
const EPSILON: Self;
fn abs(self) -> Self;
}
macro_rules! impl_float {
( $( $ty:ty )* ) => {
$(
impl Float for $ty {
const EPSILON: Self = Self::EPSILON;
fn abs(self) -> Self {
self.abs()
}
}
)*
}
}
impl_float!(f32 f64);
/// Trait to compare two instances and panic if they are not equal.
pub trait AssertEq {
fn assert_eq(&self, expected: &Self);
}
-84
View File
@@ -1,84 +0,0 @@
mod mode;
use rosu_pp::{Beatmap, GameMode};
use std::sync::OnceLock;
pub use self::mode::{Catch, Mania, Mode, Osu, Taiko};
static MAPS: OnceLock<[Beatmap; 4]> = OnceLock::new();
#[macro_export]
#[rustfmt::skip]
macro_rules! test_map {
($mode:ident) => {{
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
{ common::test_map(rosu_pp::GameMode::$mode) }
#[cfg(any(feature = "async_tokio", feature = "async_std"))]
{ common::test_map(rosu_pp::GameMode::$mode).await }
}};
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
pub fn test_map(mode: GameMode) -> &'static Beatmap {
let maps = MAPS.get_or_init(|| {
let osu = Beatmap::from_path(path(GameMode::Osu)).unwrap();
let taiko = Beatmap::from_path(path(GameMode::Taiko)).unwrap();
let catch = Beatmap::from_path(path(GameMode::Catch)).unwrap();
let mania = Beatmap::from_path(path(GameMode::Mania)).unwrap();
[osu, taiko, catch, mania]
});
&maps[mode as usize]
}
#[cfg(feature = "async_tokio")]
pub async fn test_map(mode: GameMode) -> &'static Beatmap {
let maps = MAPS.get_or_init(|| {
let fut = async {
let osu = Beatmap::from_path(path(GameMode::Osu)).await.unwrap();
let taiko = Beatmap::from_path(path(GameMode::Taiko)).await.unwrap();
let catch = Beatmap::from_path(path(GameMode::Catch)).await.unwrap();
let mania = Beatmap::from_path(path(GameMode::Mania)).await.unwrap();
[osu, taiko, catch, mania]
};
match tokio::runtime::Handle::try_current() {
Ok(h) => std::thread::spawn(move || h.block_on(fut)).join().unwrap(),
Err(_) => tokio::runtime::Builder::new_current_thread()
.build()
.unwrap()
.block_on(fut),
}
});
&maps[mode as usize]
}
#[cfg(feature = "async_std")]
pub async fn test_map(mode: GameMode) -> &'static Beatmap {
let maps = MAPS.get_or_init(|| {
async_std::task::block_on(async {
let osu = Beatmap::from_path(path(GameMode::Osu)).await.unwrap();
let taiko = Beatmap::from_path(path(GameMode::Taiko)).await.unwrap();
let catch = Beatmap::from_path(path(GameMode::Catch)).await.unwrap();
let mania = Beatmap::from_path(path(GameMode::Mania)).await.unwrap();
[osu, taiko, catch, mania]
})
});
&maps[mode as usize]
}
fn path(mode: GameMode) -> String {
let map_id = match mode {
GameMode::Osu => <Osu as Mode>::TEST_MAP_ID,
GameMode::Taiko => <Taiko as Mode>::TEST_MAP_ID,
GameMode::Catch => <Catch as Mode>::TEST_MAP_ID,
GameMode::Mania => <Mania as Mode>::TEST_MAP_ID,
};
format!("./maps/{map_id}.osu")
}
-71
View File
@@ -1,71 +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,
is_convert: false,
};
Catch: 2118524, CatchDifficultyAttributes {
stars: 3.2502669316166624,
ar: 8.0,
n_fruits: 728,
n_droplets: 2,
n_tiny_droplets: 291,
};
Mania: 1638954, ManiaDifficultyAttributes {
stars: 3.441830819988125,
hit_window: 40.0,
max_combo: 956,
n_objects: 594,
is_convert: false,
};
}
+90
View File
@@ -0,0 +1,90 @@
use rosu_pp::{model::mode::GameMode, Beatmap};
use crate::common::assert_eq_float;
mod common;
#[test]
fn osu() {
let map = Beatmap::from_path(common::OSU).unwrap();
assert_eq!(map.mode, GameMode::Osu);
assert_eq!(map.version, 14);
assert_eq_float(map.ar, 9.3);
assert_eq_float(map.od, 8.8);
assert_eq_float(map.cs, 4.5);
assert_eq_float(map.hp, 5.0);
assert_eq_float(map.slider_multiplier, 1.7);
assert_eq_float(map.slider_tick_rate, 1.0);
assert_eq!(map.hit_objects.len(), 601);
assert_eq!(map.hit_sounds.len(), 601);
assert_eq!(map.timing_points.len(), 1);
assert_eq!(map.difficulty_points.len(), 50);
assert_eq!(map.effect_points.len(), 0);
assert_eq_float(map.stack_leniency, 0.5);
assert_eq!(map.breaks.len(), 1);
}
#[test]
fn taiko() {
let map = Beatmap::from_path(common::TAIKO).unwrap();
assert_eq!(map.mode, GameMode::Taiko);
assert_eq!(map.version, 14);
assert_eq_float(map.ar, 8.0);
assert_eq_float(map.od, 5.0);
assert_eq_float(map.cs, 2.0);
assert_eq_float(map.hp, 6.0);
assert_eq_float(map.slider_multiplier, 1.4);
assert_eq_float(map.slider_tick_rate, 1.0);
assert_eq!(map.hit_objects.len(), 295);
assert_eq!(map.hit_sounds.len(), 295);
assert_eq!(map.timing_points.len(), 1);
assert_eq!(map.difficulty_points.len(), 3);
// osu!lazer has 7 effect points because they store scroll speed
assert_eq!(map.effect_points.len(), 4);
assert_eq_float(map.stack_leniency, 0.7);
assert_eq!(map.breaks.len(), 0);
}
#[test]
fn catch() {
let map = Beatmap::from_path(common::CATCH).unwrap();
assert_eq!(map.mode, GameMode::Catch);
assert_eq!(map.version, 14);
assert_eq_float(map.ar, 8.0);
assert_eq_float(map.od, 8.0);
assert_eq_float(map.cs, 3.5);
assert_eq_float(map.hp, 5.0);
assert_eq_float(map.slider_multiplier, 1.45);
assert_eq_float(map.slider_tick_rate, 1.0);
assert_eq!(map.hit_objects.len(), 477);
assert_eq!(map.hit_sounds.len(), 477);
assert_eq!(map.timing_points.len(), 1);
assert_eq!(map.difficulty_points.len(), 0);
assert_eq!(map.effect_points.len(), 16);
assert_eq_float(map.stack_leniency, 0.7);
assert_eq!(map.breaks.len(), 0);
}
#[test]
fn mania() {
let map = Beatmap::from_path(common::MANIA).unwrap();
assert_eq!(map.mode, GameMode::Mania);
assert_eq!(map.version, 14);
assert_eq_float(map.ar, 5.0);
assert_eq_float(map.od, 8.0);
assert_eq_float(map.cs, 4.0);
assert_eq_float(map.hp, 8.0);
assert_eq_float(map.slider_multiplier, 1.4);
assert_eq_float(map.slider_tick_rate, 1.0);
assert_eq!(map.hit_objects.len(), 594);
assert_eq!(map.hit_sounds.len(), 594);
assert_eq!(map.timing_points.len(), 1);
assert_eq!(map.difficulty_points.len(), 0);
assert_eq!(map.effect_points.len(), 0);
assert_eq_float(map.stack_leniency, 0.7);
assert_eq!(map.breaks.len(), 0);
}
+475
View File
@@ -0,0 +1,475 @@
use std::panic::{self, UnwindSafe};
use rosu_pp::{
catch::{Catch, CatchDifficultyAttributes},
mania::{Mania, ManiaDifficultyAttributes},
osu::{Osu, OsuDifficultyAttributes},
taiko::{Taiko, TaikoDifficultyAttributes},
Beatmap, ModeDifficulty,
};
use self::common::*;
mod common;
macro_rules! test_cases {
( $mode:ident: $path:ident {
$( $( $mods:ident )+ => {
$( $key:ident: $value:literal $( , )? )*
} $( ; )? )*
} ) => {
let map = Beatmap::from_path(common::$path)
.unwrap()
.unchecked_into_converted::<$mode>();
$(
let mods = 0 $( + $mods )*;
let expected = test_cases!(@$mode { $( $key: $value, )* });
let actual = ModeDifficulty::new().mods(mods).calculate(&map);
run(&actual, &expected, mods);
)*
};
( @Osu {
aim: $aim:literal,
speed: $speed:literal,
flashlight: $flashlight:literal,
slider_factor: $slider_factor:literal,
speed_note_count: $speed_note_count:literal,
ar: $ar:literal,
od: $od:literal,
hp: $hp:literal,
n_circles: $n_circles:literal,
n_sliders: $n_sliders:literal,
n_spinners: $n_spinners:literal,
stars: $stars:literal,
max_combo: $max_combo:literal,
}) => {
OsuDifficultyAttributes {
aim: $aim,
speed: $speed,
flashlight: $flashlight,
slider_factor: $slider_factor,
speed_note_count: $speed_note_count,
ar: $ar,
od: $od,
hp: $hp,
n_circles: $n_circles,
n_sliders: $n_sliders,
n_spinners: $n_spinners,
stars: $stars,
max_combo: $max_combo,
}
};
( @Taiko {
stamina: $stamina:literal,
rhythm: $rhythm:literal,
color: $color:literal,
peak: $peak:literal,
hit_window: $hit_window:literal,
stars: $stars:literal,
max_combo: $max_combo:literal,
is_convert: $is_convert:literal,
}) => {
TaikoDifficultyAttributes {
stamina: $stamina,
rhythm: $rhythm,
color: $color,
peak: $peak,
hit_window: $hit_window,
stars: $stars,
max_combo: $max_combo,
is_convert: $is_convert,
}
};
( @Catch {
stars: $stars:literal,
ar: $ar:literal,
n_fruits: $n_fruits:literal,
n_droplets: $n_droplets:literal,
n_tiny_droplets: $n_tiny_droplets:literal,
is_convert: $is_convert:literal,
}) => {
CatchDifficultyAttributes {
stars: $stars,
ar: $ar,
n_fruits: $n_fruits,
n_droplets: $n_droplets,
n_tiny_droplets: $n_tiny_droplets,
is_convert: $is_convert,
}
};
( @Mania {
stars: $stars:literal,
hit_window: $hit_window:literal,
n_objects: $n_objects:literal,
max_combo: $max_combo:literal,
is_convert: $is_convert:literal,
}) => {
ManiaDifficultyAttributes {
stars: $stars,
hit_window: $hit_window,
n_objects: $n_objects,
max_combo: $max_combo,
is_convert: $is_convert,
}
}
}
#[test]
fn basic_osu() {
test_cases! {
Osu: OSU {
NM => {
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,
};
HD => {
aim: 2.8693628443424104,
speed: 2.533869745015772,
flashlight: 2.606877929965889,
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,
};
HR => {
aim: 3.2385394176190507,
speed: 2.7009854505234308,
flashlight: 2.8549217213059936,
slider_factor: 0.9690667605258665,
speed_note_count: 184.01205359079387,
ar: 10.0,
od: 10.0,
hp: 7.0,
n_circles: 307,
n_sliders: 293,
n_spinners: 1,
stars: 6.263576582906263,
max_combo: 909,
};
DT => {
aim: 4.041442573946681,
speed: 3.6784866216272474,
flashlight: 3.319522943625448,
slider_factor: 0.9776943279272041,
speed_note_count: 214.80421464205617,
ar: 10.53333346048991,
od: 10.311111238267687,
hp: 5.0,
n_circles: 307,
n_sliders: 293,
n_spinners: 1,
stars: 8.085307648397622,
max_combo: 909,
};
FL => {
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: 6.8667780753884236,
max_combo: 909,
};
HD FL => {
aim: 2.8693628443424104,
speed: 2.533869745015772,
flashlight: 2.606877929965889,
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: 7.17258038247615,
max_combo: 909,
};
}
};
}
#[test]
fn basic_taiko() {
test_cases! {
Taiko: TAIKO {
NM => {
stamina: 1.4528845068865617,
rhythm: 0.20130047251681948,
color: 1.0487315549761433,
peak: 1.8881824429738323,
hit_window: 35.0,
stars: 2.9778030386845606,
max_combo: 289,
is_convert: false,
};
HR => {
stamina: 1.4528845068865617,
rhythm: 0.20130047251681948,
color: 1.0487315549761433,
peak: 1.8881824429738323,
hit_window: 29.0,
stars: 2.9778030386845606,
max_combo: 289,
is_convert: false,
};
DT => {
stamina: 2.054617838297644,
rhythm: 0.4448175371191029,
color: 1.3637624960988888,
peak: 2.6393434317991886,
hit_window: 23.333333333333332,
stars: 3.9605501866340607,
max_combo: 289,
is_convert: false,
};
}
};
}
#[test]
fn convert_taiko() {
test_cases! {
Taiko: OSU {
NM => {
stamina: 2.947896529153566,
rhythm: 1.4696991260446617,
color: 2.3032281729649067,
peak: 4.130240422926277,
hit_window: 23.59999942779541,
stars: 5.247857660585606,
max_combo: 908,
is_convert: true,
};
HR => {
stamina: 2.947896529153566,
rhythm: 1.4696991260446617,
color: 2.3032281729649067,
peak: 4.130240422926277,
hit_window: 20.0,
stars: 5.247857660585606,
max_combo: 908,
is_convert: true,
};
DT => {
stamina: 4.412382708370478,
rhythm: 2.002843919169095,
color: 3.1864894777399986,
peak: 6.107962386775966,
hit_window: 15.733332951863607,
stars: 7.0140481946324815,
max_combo: 908,
is_convert: true,
};
}
};
}
#[test]
fn basic_catch() {
test_cases! {
Catch: CATCH {
NM => {
stars: 3.2502663133739844,
ar: 8.0,
n_fruits: 728,
n_droplets: 2,
n_tiny_droplets: 291,
is_convert: false,
};
HR => {
stars: 4.3148698646484265,
ar: 10.0,
n_fruits: 728,
n_droplets: 2,
n_tiny_droplets: 291,
is_convert: false,
};
EZ => {
stars: 4.021637906259923,
ar: 4.0,
n_fruits: 728,
n_droplets: 2,
n_tiny_droplets: 291,
is_convert: false,
};
DT => {
stars: 4.635262826575387,
ar: 9.666666666666668,
n_fruits: 728,
n_droplets: 2,
n_tiny_droplets: 291,
is_convert: false,
};
}
};
}
#[test]
fn convert_catch() {
test_cases! {
Catch: OSU {
NM => {
stars: 4.513884094512871,
ar: 9.300000190734863,
n_fruits: 908,
n_droplets: 0,
n_tiny_droplets: 159,
is_convert: true,
};
HR => {
stars: 5.082061773944862,
ar: 10.0,
n_fruits: 908,
n_droplets: 0,
n_tiny_droplets: 159,
is_convert: true,
};
EZ => {
stars: 3.598951063172104,
ar: 4.650000095367432,
n_fruits: 908,
n_droplets: 0,
n_tiny_droplets: 159,
is_convert: true,
};
DT => {
stars: 6.136837738350475,
ar: 10.53333346048991,
n_fruits: 908,
n_droplets: 0,
n_tiny_droplets: 159,
is_convert: true,
};
}
};
}
#[test]
fn basic_mania() {
test_cases! {
Mania: MANIA {
NM => {
stars: 3.441830819988125,
hit_window: 40.0,
n_objects: 594,
max_combo: 956,
is_convert: false,
};
DT => {
stars: 4.70051326060948,
hit_window: 40.0,
n_objects: 594,
max_combo: 956,
is_convert: false,
};
}
};
}
#[test]
fn convert_mania() {
test_cases! {
Mania: OSU {
NM => {
stars: 3.2033142085672255,
hit_window: 34.0,
n_objects: 1046,
max_combo: 1381,
is_convert: true,
};
DT => {
stars: 4.2934063021960185,
hit_window: 34.0,
n_objects: 1046,
max_combo: 1381,
is_convert: true,
};
}
};
}
fn run<A>(actual: &A, expected: &A, mods: u32)
where
A: AssertEq,
for<'a> &'a A: UnwindSafe,
{
if panic::catch_unwind(|| actual.assert_eq(expected)).is_err() {
panic!("Mods: {mods}");
}
}
impl AssertEq for OsuDifficultyAttributes {
fn assert_eq(&self, expected: &Self) {
assert_eq_float(self.aim, expected.aim);
assert_eq_float(self.speed, expected.speed);
assert_eq_float(self.flashlight, expected.flashlight);
assert_eq_float(self.slider_factor, expected.slider_factor);
assert_eq_float(self.speed_note_count, expected.speed_note_count);
assert_eq_float(self.ar, expected.ar);
assert_eq_float(self.od, expected.od);
assert_eq_float(self.hp, expected.hp);
assert_eq!(self.n_circles, expected.n_circles);
assert_eq!(self.n_sliders, expected.n_sliders);
assert_eq!(self.n_spinners, expected.n_spinners);
assert_eq_float(self.stars, expected.stars);
assert_eq!(self.max_combo, expected.max_combo);
}
}
impl AssertEq for TaikoDifficultyAttributes {
fn assert_eq(&self, expected: &Self) {
assert_eq_float(self.stamina, expected.stamina);
assert_eq_float(self.rhythm, expected.rhythm);
assert_eq_float(self.color, expected.color);
assert_eq_float(self.peak, expected.peak);
assert_eq_float(self.hit_window, expected.hit_window);
assert_eq_float(self.stars, expected.stars);
assert_eq!(self.max_combo, expected.max_combo);
assert_eq!(self.is_convert, expected.is_convert);
}
}
impl AssertEq for CatchDifficultyAttributes {
fn assert_eq(&self, expected: &Self) {
assert_eq_float(self.stars, expected.stars);
assert_eq_float(self.ar, expected.ar);
assert_eq!(self.n_fruits, expected.n_fruits);
assert_eq!(self.n_droplets, expected.n_droplets);
assert_eq!(self.n_tiny_droplets, expected.n_tiny_droplets);
}
}
impl AssertEq for ManiaDifficultyAttributes {
fn assert_eq(&self, expected: &Self) {
assert_eq_float(self.stars, expected.stars);
assert_eq_float(self.hit_window, expected.hit_window);
assert_eq!(self.n_objects, expected.n_objects);
assert_eq!(self.max_combo, expected.max_combo);
assert_eq!(self.is_convert, expected.is_convert);
}
}
-96
View File
@@ -1,96 +0,0 @@
#![cfg(all(
not(any(feature = "async_tokio", feature = "async_std")),
feature = "gradual"
))]
use rosu_pp::{
catch::{
CatchGradualDifficulty, CatchGradualPerformance, CatchOwnedGradualPerformance,
CatchScoreState,
},
Beatmap, BeatmapExt, CatchPP,
};
mod common;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = CatchGradualDifficulty::new(&map, 0);
assert!(attributes.next().is_none());
}
#[test]
fn gradual_end_eq_regular() {
let map = test_map!(Catch);
let regular = CatchPP::new(map).calculate();
let mut gradual = CatchGradualPerformance::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.nth(state, usize::MAX).unwrap();
assert_eq!(regular, gradual_end);
}
#[test]
fn gradual_complete_next() {
let map = test_map!(Catch);
let mods = 88; // HDHRDT
let n_objects = dbg!(map.catch_hitobjects(mods).len());
let mut gradual = CatchGradualPerformance::new(map, mods);
let mut gradual_2nd = CatchGradualPerformance::new(map, mods);
let mut gradual_3rd = CatchGradualPerformance::new(map, mods);
let mut gradual_owned = CatchOwnedGradualPerformance::new(map.to_owned(), mods);
let mut state = CatchScoreState::default();
for i in 1.. {
state.n_misses += 1;
let Some(next_gradual) = gradual.next(state.clone()) else {
assert_eq!(i, n_objects + 1);
assert!(gradual_2nd.last(state.clone()).is_some() || n_objects % 2 == 0);
assert!(gradual_3rd.last(state.clone()).is_some() || n_objects % 3 == 0);
assert!(gradual_owned.next(state.clone()).is_none());
break;
};
if i % 2 == 0 {
let next_gradual_2nd = gradual_2nd.nth(state.clone(), 1).unwrap();
assert_eq!(next_gradual, next_gradual_2nd, "i={i}");
}
if i % 3 == 0 {
let next_gradual_3rd = gradual_3rd.nth(state.clone(), 2).unwrap();
assert_eq!(next_gradual, next_gradual_3rd, "i={i}");
}
let next_gradual_owned = gradual_owned.next(state.clone()).unwrap();
let mut regular_calc = CatchPP::new(map)
.mods(mods)
.passed_objects(i)
.state(state.clone());
let _regular_state = regular_calc.generate_state();
// FIXME: juicestreams are currently added to the attributes in their entirety
// so the state won't change while objects of the same juicestream are processed
// assert_eq!(state, _regular_state);
let regular = regular_calc.calculate();
assert_eq!(next_gradual, next_gradual_owned, "i={i}");
assert_eq!(next_gradual, regular, "i={i}");
}
}
-87
View File
@@ -1,87 +0,0 @@
#![cfg(all(
not(any(feature = "async_tokio", feature = "async_std")),
feature = "gradual"
))]
use rosu_pp::{
mania::{
ManiaGradualDifficulty, ManiaGradualPerformance, ManiaOwnedGradualPerformance,
ManiaScoreState,
},
Beatmap, ManiaPP,
};
mod common;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = ManiaGradualDifficulty::new(&map, 0);
assert!(attributes.next().is_none());
}
#[test]
fn gradual_end_eq_regular() {
let map = test_map!(Mania);
let regular = ManiaPP::new(map).calculate();
let state = ManiaScoreState {
n320: map.hit_objects.len(),
..Default::default()
};
let gradual = ManiaGradualPerformance::new(map, 0).last(state).unwrap();
assert_eq!(regular, gradual);
}
#[test]
fn gradual_complete_next() {
let map = test_map!(Mania);
let mods = 67; // NFEZDT
let mut gradual = ManiaGradualPerformance::new(map, mods);
let mut gradual_2nd = ManiaGradualPerformance::new(map, mods);
let mut gradual_3rd = ManiaGradualPerformance::new(map, mods);
let mut gradual_owned = ManiaOwnedGradualPerformance::new(map.to_owned(), mods);
let mut state = ManiaScoreState::default();
for i in 1.. {
state.n320 += 1;
let Some(next_gradual) = gradual.next(state.clone()) else {
assert_eq!(i, map.hit_objects.len() + 1);
assert!(gradual_2nd.last(state.clone()).is_some() || map.hit_objects.len() % 2 == 0);
assert!(gradual_3rd.last(state.clone()).is_some() || map.hit_objects.len() % 3 == 0);
assert!(gradual_owned.next(state.clone()).is_none());
break;
};
if i % 2 == 0 {
let next_gradual_2nd = gradual_2nd.nth(state.clone(), 1).unwrap();
assert_eq!(next_gradual, next_gradual_2nd, "i={i}");
}
if i % 3 == 0 {
let next_gradual_3rd = gradual_3rd.nth(state.clone(), 2).unwrap();
assert_eq!(next_gradual, next_gradual_3rd, "i={i}");
}
let next_gradual_owned = gradual_owned.next(state.clone()).unwrap();
let mut regular_calc = ManiaPP::new(map)
.mods(mods)
.passed_objects(i)
.state(state.clone());
let regular_state = regular_calc.generate_state();
assert_eq!(state, regular_state);
let regular = regular_calc.calculate();
assert_eq!(next_gradual, next_gradual_owned, "i={i}");
assert_eq!(next_gradual, regular, "i={i}");
}
}
-90
View File
@@ -1,90 +0,0 @@
#![cfg(all(
not(any(feature = "async_tokio", feature = "async_std")),
feature = "gradual"
))]
use rosu_pp::osu::OsuOwnedGradualPerformance;
use rosu_pp::{
osu::{OsuGradualDifficulty, OsuGradualPerformance, OsuScoreState},
Beatmap, OsuPP,
};
mod common;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attributes = OsuGradualDifficulty::new(&map, 0);
assert!(attributes.next().is_none());
}
#[test]
fn gradual_end_eq_regular() {
let map = test_map!(Osu);
let regular = OsuPP::new(map).calculate();
let mut gradual = OsuGradualPerformance::new(map, 0);
let state = OsuScoreState {
max_combo: 909,
n300: 601,
n100: 0,
n50: 0,
n_misses: 0,
};
let gradual_end = gradual.last(state.clone()).unwrap();
assert_eq!(regular, gradual_end);
assert!(gradual.next(state).is_none());
}
#[test]
fn gradual_complete_next() {
let map = test_map!(Osu);
let mods = 88; // HDHRDT
let mut gradual = OsuGradualPerformance::new(map, mods);
let mut gradual_2nd = OsuGradualPerformance::new(map, mods);
let mut gradual_3rd = OsuGradualPerformance::new(map, mods);
let mut gradual_owned = OsuOwnedGradualPerformance::new(map.to_owned(), mods);
let mut state = OsuScoreState::default();
for i in 1.. {
state.n_misses += 1;
let Some(next_gradual) = gradual.next(state.clone()) else {
assert_eq!(i, map.hit_objects.len() + 1);
assert!(gradual_2nd.last(state.clone()).is_some() || map.hit_objects.len() % 2 == 0);
assert!(gradual_3rd.last(state.clone()).is_some() || map.hit_objects.len() % 3 == 0);
assert!(gradual_owned.next(state.clone()).is_none());
break;
};
if i % 2 == 0 {
let next_gradual_2nd = gradual_2nd.nth(state.clone(), 1).unwrap();
assert_eq!(next_gradual, next_gradual_2nd, "i={i}");
}
if i % 3 == 0 {
let next_gradual_3rd = gradual_3rd.nth(state.clone(), 2).unwrap();
assert_eq!(next_gradual, next_gradual_3rd, "i={i}");
}
let next_gradual_owned = gradual_owned.next(state.clone()).unwrap();
let mut regular_calc = OsuPP::new(map)
.mods(mods)
.passed_objects(i)
.state(state.clone());
let regular_state = regular_calc.generate_state();
assert_eq!(state, regular_state);
let regular = regular_calc.calculate();
assert_eq!(next_gradual, next_gradual_owned, "i={i}");
assert_eq!(next_gradual, regular, "i={i}");
}
}
-90
View File
@@ -1,90 +0,0 @@
#![cfg(all(
not(any(feature = "async_tokio", feature = "async_std")),
feature = "gradual"
))]
use rosu_pp::{
taiko::{
TaikoGradualDifficulty, TaikoGradualPerformance, TaikoOwnedGradualPerformance,
TaikoScoreState,
},
Beatmap, TaikoPP,
};
mod common;
#[test]
fn empty_map() {
let map = Beatmap::default();
let mut attrs = TaikoGradualDifficulty::new(&map, 0);
assert!(attrs.next().is_none());
}
#[test]
fn gradual_end_eq_regular() {
let map = test_map!(Taiko);
let regular = TaikoPP::new(map).calculate();
let mut gradual = TaikoGradualPerformance::new(map, 0);
let state = TaikoScoreState {
max_combo: 289,
n300: 289,
n100: 0,
n_misses: 0,
};
let gradual_end = gradual.last(state).unwrap();
assert_eq!(regular, gradual_end);
}
#[test]
fn gradual_complete_next() {
let map = test_map!(Taiko);
let mods = 88; // HDHRDT
let mut gradual = TaikoGradualPerformance::new(map, mods);
let mut gradual_2nd = TaikoGradualPerformance::new(map, mods);
let mut gradual_3rd = TaikoGradualPerformance::new(map, mods);
let mut gradual_owned = TaikoOwnedGradualPerformance::new(map.to_owned(), mods);
let mut state = TaikoScoreState::default();
for i in 1.. {
state.n_misses += 1;
let Some(next_gradual) = gradual.next(state.clone()) else {
assert_eq!(i, map.n_circles as usize + 1);
assert!(gradual_2nd.last(state.clone()).is_some() || map.hit_objects.len() % 2 == 0);
assert!(gradual_3rd.last(state.clone()).is_some() || map.hit_objects.len() % 3 == 0);
assert!(gradual_owned.next(state.clone()).is_none());
break;
};
if i % 2 == 0 {
let next_gradual_2nd = gradual_2nd.nth(state.clone(), 1).unwrap();
assert_eq!(next_gradual, next_gradual_2nd, "i={i}");
}
if i % 3 == 0 {
let next_gradual_3rd = gradual_3rd.nth(state.clone(), 2).unwrap();
assert_eq!(next_gradual, next_gradual_3rd, "i={i}");
}
let next_gradual_owned = gradual_owned.next(state.clone()).unwrap();
let mut regular_calc = TaikoPP::new(map)
.mods(mods)
.passed_objects(i)
.state(state.clone());
let regular_state = regular_calc.generate_state();
assert_eq!(state, regular_state);
let regular = regular_calc.calculate();
assert_eq!(next_gradual, next_gradual_owned, "i={i}");
assert_eq!(next_gradual, regular, "i={i}");
}
}
-176
View File
@@ -1,176 +0,0 @@
use rosu_pp::{Beatmap, GameMode};
mod common;
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
mod sync {
use super::*;
#[test]
fn parse_osu() {
assert_osu(test_map!(Osu));
}
#[test]
fn parse_taiko() {
assert_taiko(test_map!(Taiko));
}
#[test]
fn parse_catch() {
assert_catch(test_map!(Catch));
}
#[test]
fn parse_mania() {
assert_mania(test_map!(Mania));
}
}
#[cfg(feature = "async_tokio")]
mod async_tokio {
use tokio::runtime::Builder as RuntimeBuilder;
use super::*;
#[test]
fn parse_osu() {
RuntimeBuilder::new_current_thread()
.build()
.unwrap()
.block_on(async { assert_osu(test_map!(Osu)) });
}
#[test]
fn parse_taiko() {
RuntimeBuilder::new_current_thread()
.build()
.unwrap()
.block_on(async { assert_taiko(test_map!(Taiko)) });
}
#[test]
fn parse_catch() {
RuntimeBuilder::new_current_thread()
.build()
.unwrap()
.block_on(async { assert_catch(test_map!(Catch)) });
}
#[test]
fn parse_mania() {
RuntimeBuilder::new_current_thread()
.build()
.unwrap()
.block_on(async { assert_mania(test_map!(Mania)) });
}
}
#[cfg(feature = "async_std")]
mod async_tokio {
use super::*;
#[test]
fn parse_osu() {
async_std::task::block_on(async { assert_osu(test_map!(Osu)) });
}
#[test]
fn parse_taiko() {
async_std::task::block_on(async { assert_taiko(test_map!(Taiko)) });
}
#[test]
fn parse_catch() {
async_std::task::block_on(async { assert_catch(test_map!(Catch)) });
}
#[test]
fn parse_mania() {
async_std::task::block_on(async { assert_mania(test_map!(Mania)) });
}
}
fn assert_osu(map: &Beatmap) {
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)
}
fn assert_taiko(map: &Beatmap) {
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)
}
fn assert_catch(map: &Beatmap) {
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)
}
fn assert_mania(map: &Beatmap) {
assert_eq!(map.mode, GameMode::Mania);
assert_eq!(map.version, 14);
assert_eq!(map.n_circles, 473);
assert_eq!(map.n_sliders, 121);
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 - 8.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(), 594);
assert_eq!(map.sounds.len(), 594);
assert_eq!(map.timing_points.len(), 1);
assert_eq!(map.difficulty_points.len(), 0);
assert_eq!(map.effect_points.len(), 1);
assert!((map.stack_leniency - 0.7).abs() <= f32::EPSILON);
assert_eq!(map.breaks.len(), 0)
}
-73
View File
@@ -1,73 +0,0 @@
#![cfg(not(any(feature = "async_tokio", feature = "async_std")))]
use rosu_pp::{CatchPP, ManiaPP, OsuPP, TaikoPP};
mod common;
#[test]
fn osu() {
let map = test_map!(Osu);
let mods = 8 + 64;
let misses = 2;
let regular = OsuPP::new(map).mods(mods).n_misses(misses).calculate();
let via_diff = OsuPP::from(regular.difficulty.clone())
.mods(mods)
.n_misses(misses)
.calculate();
assert_eq!(regular, via_diff);
}
#[test]
fn taiko() {
let map = test_map!(Taiko);
let mods = 8 + 64;
let misses = 2;
let regular = TaikoPP::new(map).mods(mods).n_misses(misses).calculate();
let via_diff = TaikoPP::from(regular.difficulty.clone())
.mods(mods)
.n_misses(misses)
.calculate();
assert_eq!(regular, via_diff);
}
#[test]
fn catch() {
let map = test_map!(Catch);
let mods = 8 + 64;
let misses = 2;
let regular = CatchPP::new(map).mods(mods).misses(misses).calculate();
let via_diff = CatchPP::from(regular.difficulty.clone())
.mods(mods)
.misses(misses)
.calculate();
assert_eq!(regular, via_diff);
}
#[test]
fn mania() {
let map = test_map!(Mania);
let mods = 8 + 64;
let misses = 2;
let regular = ManiaPP::new(map).mods(mods).n_misses(misses).calculate();
let via_diff = ManiaPP::from(regular.difficulty.clone())
.mods(mods)
.n_misses(misses)
.calculate();
assert_eq!(regular, via_diff);
}
+315
View File
@@ -0,0 +1,315 @@
use std::panic::{self, UnwindSafe};
use rosu_pp::{
catch::{CatchPerformance, CatchPerformanceAttributes},
mania::{ManiaPerformance, ManiaPerformanceAttributes},
osu::{OsuPerformance, OsuPerformanceAttributes},
taiko::{TaikoPerformance, TaikoPerformanceAttributes},
Beatmap,
};
use self::common::*;
mod common;
macro_rules! test_cases {
( $mode:ident: $path:ident {
$( $( $mods:ident )+ => {
$( $key:ident: $value:literal $( , )? )*
} ;)*
} ) => {
let map = Beatmap::from_path(common::$path)
.unwrap()
.unchecked_into_converted();
$(
let mods = 0 $( + $mods )*;
let (calc, expected) = test_cases!(@$mode { map, $( $key: $value, )* });
let actual = calc.mods(mods).calculate();
run(&actual, &expected, mods);
)*
};
( @Osu {
$map:ident,
pp: $pp:literal,
pp_acc: $pp_acc:literal,
pp_aim: $pp_aim:literal,
pp_flashlight: $pp_flashlight:literal,
pp_speed: $pp_speed:literal,
effective_miss_count: $effective_miss_count:literal,
}) => {
(
OsuPerformance::from($map.as_owned()),
OsuPerformanceAttributes {
pp: $pp,
pp_acc: $pp_acc,
pp_aim: $pp_aim,
pp_flashlight: $pp_flashlight,
pp_speed: $pp_speed,
effective_miss_count: $effective_miss_count,
..Default::default()
},
)
};
( @Taiko {
$map: ident,
pp: $pp:literal,
pp_acc: $pp_acc:literal,
pp_difficulty: $pp_difficulty:literal,
effective_miss_count: $effective_miss_count:literal,
}) => {
(
TaikoPerformance::from($map.as_owned()),
TaikoPerformanceAttributes {
pp: $pp,
pp_acc: $pp_acc,
pp_difficulty: $pp_difficulty,
effective_miss_count: $effective_miss_count,
..Default::default()
},
)
};
( @Catch {
$map:ident,
pp: $pp:literal,
}) => {
(
CatchPerformance::from($map.as_owned()),
CatchPerformanceAttributes {
pp: $pp,
..Default::default()
},
)
};
( @Mania {
$map:ident,
pp: $pp:literal,
pp_difficulty: $pp_difficulty:literal,
}) => {
(
ManiaPerformance::from($map.as_owned()),
ManiaPerformanceAttributes {
pp: $pp,
pp_difficulty: $pp_difficulty,
..Default::default()
},
)
};
}
#[test]
fn basic_osu() {
test_cases! {
Osu: OSU {
NM => {
pp: 255.9419635475736,
pp_acc: 79.84500076626814,
pp_aim: 98.13131344235279,
pp_flashlight: 0.0,
pp_speed: 69.86876965478146,
effective_miss_count: 0.0,
};
HD => {
pp: 281.28736211196446,
pp_acc: 86.2326008275696,
pp_aim: 108.72949454544438,
pp_flashlight: 0.0,
pp_speed: 77.41459624444144,
effective_miss_count: 0.0,
};
EZ HD => {
pp: 185.64881287702838,
pp_acc: 13.59914468151693,
pp_aim: 96.88083530160195,
pp_flashlight: 0.0,
pp_speed: 65.96268917477774,
effective_miss_count: 0.0,
};
HR => {
pp: 375.0764291059058,
pp_acc: 132.13521300659738,
pp_aim: 143.28598037767793,
pp_flashlight: 0.0,
pp_speed: 87.39375701955078,
effective_miss_count: 0.0,
};
DT => {
pp: 716.3683237855254,
pp_acc: 150.5694857734174,
pp_aim: 300.39084638572484,
pp_flashlight: 0.0,
pp_speed: 240.8765306794618,
effective_miss_count: 0.0,
};
FL => {
pp: 384.8917879591265,
pp_acc: 81.4419007815935,
pp_aim: 98.13131344235279,
pp_flashlight: 132.3991950960219,
pp_speed: 69.86876965478146,
effective_miss_count: 0.0,
};
HD FL => {
pp: 450.3709760368082,
pp_acc: 87.95725284412099,
pp_aim: 108.72949454544438,
pp_flashlight: 171.7600847331662,
pp_speed: 77.41459624444144,
effective_miss_count: 0.0,
};
}
};
}
#[test]
fn basic_taiko() {
test_cases! {
Taiko: TAIKO {
NM => {
pp: 98.47602106219567,
pp_acc: 46.11642717726248,
pp_difficulty: 46.69844233558799,
effective_miss_count: 0.0,
};
HD => {
pp: 107.19493857245885,
pp_acc: 46.11642717726248,
pp_difficulty: 47.86590339397769,
effective_miss_count: 0.0,
};
HR => {
pp: 112.22705475791287,
pp_acc: 56.71431676265808,
pp_difficulty: 49.033364452367394,
effective_miss_count: 0.0,
};
DT => {
pp: 181.5021832786881,
pp_acc: 80.74206626516394,
pp_difficulty: 90.29961105452931,
effective_miss_count: 0.0,
};
}
};
}
#[test]
fn convert_taiko() {
test_cases! {
Taiko: OSU {
NM => {
pp: 324.23564627433217,
pp_acc: 125.81086361861148,
pp_difficulty: 179.31471072573842,
effective_miss_count: 0.0,
};
HD => {
pp: 353.7513933816713,
pp_acc: 125.81086361861148,
pp_difficulty: 183.79757849388187,
effective_miss_count: 0.0,
};
HR => {
pp: 360.12274556551137,
pp_acc: 150.9344373000759,
pp_difficulty: 188.28044626202535,
effective_miss_count: 0.0,
};
DT => {
pp: 604.8167434609272,
pp_acc: 220.7055264311451,
pp_difficulty: 347.90986552791844,
effective_miss_count: 0.0,
};
}
}
}
#[test]
fn basic_catch() {
test_cases! {
Catch: CATCH {
NM => { pp: 113.85903714373049 };
HD => { pp: 136.63084457247658 };
HD HR => { pp: 231.90266535529486 };
DT => { pp: 247.18402249125862 };
}
};
}
#[test]
fn convert_catch() {
test_cases! {
Catch: OSU {
NM => { pp: 230.99937552589745 };
HD => { pp: 254.6768082128294 };
HD HR => { pp: 328.41201070443725 };
DT => { pp: 500.4365349891725 };
}
};
}
#[test]
fn basic_mania() {
test_cases! {
Mania: MANIA {
NM => { pp: 114.37175184134917, pp_difficulty: 14.296468980168646 };
EZ => { pp: 57.18587592067458, pp_difficulty: 14.296468980168646 };
DT => { pp: 233.17882161546717, pp_difficulty: 29.147352701933396 };
}
};
}
#[test]
fn convert_mania() {
test_cases! {
Mania: OSU {
NM => { pp: 99.73849552661329, pp_difficulty: 12.467311940826661 };
EZ => { pp: 49.869247763306646, pp_difficulty: 12.467311940826661 };
DT => { pp: 195.23247718805612, pp_difficulty: 24.404059648507015 };
}
};
}
fn run<A>(actual: &A, expected: &A, mods: u32)
where
A: AssertEq,
for<'a> &'a A: UnwindSafe,
{
if panic::catch_unwind(|| actual.assert_eq(expected)).is_err() {
panic!("Mods: {mods}");
}
}
impl AssertEq for OsuPerformanceAttributes {
fn assert_eq(&self, expected: &Self) {
assert_eq_float(self.pp, expected.pp);
assert_eq_float(self.pp_acc, expected.pp_acc);
assert_eq_float(self.pp_aim, expected.pp_aim);
assert_eq_float(self.pp_flashlight, expected.pp_flashlight);
assert_eq_float(self.pp_speed, expected.pp_speed);
assert_eq_float(self.effective_miss_count, expected.effective_miss_count);
}
}
impl AssertEq for TaikoPerformanceAttributes {
fn assert_eq(&self, expected: &Self) {
assert_eq_float(self.pp, expected.pp);
assert_eq_float(self.pp_acc, expected.pp_acc);
assert_eq_float(self.pp_difficulty, expected.pp_difficulty);
assert_eq_float(self.effective_miss_count, expected.effective_miss_count);
}
}
impl AssertEq for CatchPerformanceAttributes {
fn assert_eq(&self, expected: &Self) {
assert_eq_float(self.pp, expected.pp);
}
}
impl AssertEq for ManiaPerformanceAttributes {
fn assert_eq(&self, expected: &Self) {
assert_eq_float(self.pp_difficulty, expected.pp_difficulty);
assert_eq_float(self.pp, expected.pp);
}
}