fixed & extended test cases
This commit is contained in:
@@ -49,7 +49,9 @@ use super::{
|
||||
pub struct OsuGradualDifficultyAttributes {
|
||||
pub(crate) idx: usize,
|
||||
mods: u32,
|
||||
attributes: OsuDifficultyAttributes,
|
||||
attrs: OsuDifficultyAttributes,
|
||||
// Unused but `diff_objects`' lifetimes secretly depend on it
|
||||
#[allow(unused)]
|
||||
hit_objects: Vec<OsuObject>,
|
||||
diff_objects: Vec<OsuDifficultyObject<'static>>,
|
||||
skills: [Box<dyn Skill>; 4],
|
||||
@@ -60,8 +62,8 @@ impl Debug for OsuGradualDifficultyAttributes {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
f.debug_struct("OsuGradualDifficultyAttributes")
|
||||
.field("idx", &self.idx)
|
||||
.field("attributes", &self.attributes)
|
||||
.field("hit_objects", &self.hit_objects)
|
||||
.field("attrs", &self.attrs)
|
||||
.field("diff_objects", &self.diff_objects)
|
||||
.field("skills", &"<cannot be displayed>")
|
||||
.finish()
|
||||
}
|
||||
@@ -75,7 +77,7 @@ impl OsuGradualDifficultyAttributes {
|
||||
let scaling_factor = ScalingFactor::new(map_attrs.cs);
|
||||
let hr = mods.hr();
|
||||
let hit_window = 2.0 * map_attrs.hit_windows.od;
|
||||
let time_preempt = map_attrs.hit_windows.ar;
|
||||
let time_preempt = (map_attrs.hit_windows.ar * clock_rate) as f32 as f64;
|
||||
|
||||
// * Preempt time can go below 450ms. Normally, this is achieved via the DT mod
|
||||
// * which uniformly speeds up all animations game wide regardless of AR.
|
||||
@@ -100,7 +102,7 @@ impl OsuGradualDifficultyAttributes {
|
||||
|
||||
let mut params = ObjectParameters {
|
||||
map,
|
||||
attributes: &mut attrs,
|
||||
attrs: &mut attrs,
|
||||
ticks: Vec::new(),
|
||||
curve_bufs: CurveBuffers::default(),
|
||||
};
|
||||
@@ -140,7 +142,7 @@ impl OsuGradualDifficultyAttributes {
|
||||
return Self {
|
||||
idx: 0,
|
||||
mods,
|
||||
attributes: attrs,
|
||||
attrs,
|
||||
hit_objects: Vec::new(),
|
||||
diff_objects: Vec::new(),
|
||||
skills,
|
||||
@@ -149,6 +151,8 @@ impl OsuGradualDifficultyAttributes {
|
||||
}
|
||||
};
|
||||
|
||||
Self::increment_combo(last, &mut attrs);
|
||||
|
||||
let mut last_last = None;
|
||||
|
||||
// Prepare `lazy_travel_dist` and `lazy_end_pos` for `last` manually
|
||||
@@ -182,13 +186,26 @@ impl OsuGradualDifficultyAttributes {
|
||||
Self {
|
||||
idx: 0,
|
||||
mods,
|
||||
attributes: attrs,
|
||||
attrs,
|
||||
diff_objects: extend_lifetime(diff_objects),
|
||||
hit_objects,
|
||||
skills,
|
||||
hit_window,
|
||||
}
|
||||
}
|
||||
|
||||
fn increment_combo(h: &OsuObject, attrs: &mut OsuDifficultyAttributes) {
|
||||
attrs.max_combo += 1;
|
||||
|
||||
match &h.kind {
|
||||
OsuObjectKind::Circle => attrs.n_circles += 1,
|
||||
OsuObjectKind::Slider(slider) => {
|
||||
attrs.n_sliders += 1;
|
||||
attrs.max_combo += slider.nested_len();
|
||||
}
|
||||
OsuObjectKind::Spinner { .. } => attrs.n_spinners += 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_lifetime(
|
||||
@@ -210,18 +227,7 @@ impl Iterator for OsuGradualDifficultyAttributes {
|
||||
skill.process(curr, &self.diff_objects, self.hit_window);
|
||||
}
|
||||
|
||||
let mut attrs = self.attributes.clone();
|
||||
|
||||
attrs.max_combo += 1;
|
||||
|
||||
match &curr.base.kind {
|
||||
OsuObjectKind::Circle => attrs.n_circles += 1,
|
||||
OsuObjectKind::Slider(slider) => {
|
||||
attrs.n_sliders += 1;
|
||||
attrs.max_combo += slider.nested_len();
|
||||
}
|
||||
OsuObjectKind::Spinner { .. } => attrs.n_spinners += 1,
|
||||
}
|
||||
Self::increment_combo(curr.base, &mut self.attrs);
|
||||
|
||||
let [aim, aim_no_sliders, speed, flashlight] = &self.skills;
|
||||
|
||||
@@ -289,6 +295,7 @@ impl Iterator for OsuGradualDifficultyAttributes {
|
||||
0.0
|
||||
};
|
||||
|
||||
let mut attrs = self.attrs.clone();
|
||||
attrs.aim = aim_rating;
|
||||
attrs.speed = speed_rating;
|
||||
attrs.flashlight = flashlight_rating;
|
||||
@@ -301,41 +308,32 @@ impl Iterator for OsuGradualDifficultyAttributes {
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let len = self.hit_objects.len() - self.idx;
|
||||
let len = self.len();
|
||||
|
||||
(len, Some(len))
|
||||
}
|
||||
|
||||
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
||||
let skip = n.min(self.len()).saturating_sub(1);
|
||||
|
||||
for _ in 0..skip {
|
||||
let curr = self.diff_objects.get(self.idx)?;
|
||||
self.idx += 1;
|
||||
|
||||
for skill in self.skills.iter_mut() {
|
||||
skill.process(curr, &self.diff_objects, self.hit_window);
|
||||
}
|
||||
|
||||
Self::increment_combo(curr.base, &mut self.attrs);
|
||||
}
|
||||
|
||||
self.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for OsuGradualDifficultyAttributes {
|
||||
#[inline]
|
||||
fn len(&self) -> usize {
|
||||
self.hit_objects.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty_map() {
|
||||
let map = Beatmap::default();
|
||||
let mut attributes = OsuGradualDifficultyAttributes::new(&map, 0);
|
||||
assert!(attributes.next().is_none());
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||
#[test]
|
||||
fn iter_end_eq_regular() {
|
||||
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
|
||||
let mods = 64;
|
||||
let regular = crate::OsuStars::new(&map).mods(mods).calculate();
|
||||
|
||||
let iter_end = OsuGradualDifficultyAttributes::new(&map, mods)
|
||||
.last()
|
||||
.expect("empty iter");
|
||||
|
||||
assert_eq!(regular, iter_end);
|
||||
self.diff_objects.len() - self.idx
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ impl OsuScoreState {
|
||||
/// // Then comes a miss.
|
||||
/// // Note that state's max combo won't be incremented for
|
||||
/// // the next few objects because the combo is reset.
|
||||
/// state.misses += 1;
|
||||
/// state.n_misses += 1;
|
||||
/// # /*
|
||||
/// let performance = gradual_perf.process_next_object(state.clone()).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
@@ -126,7 +126,7 @@ impl OsuScoreState {
|
||||
/// state.n300 = ...
|
||||
/// state.n100 = ...
|
||||
/// state.n50 = ...
|
||||
/// state.misses = ...
|
||||
/// state.n_misses = ...
|
||||
/// let final_performance = gradual_perf.process_next_n_objects(state.clone(), usize::MAX).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
/// # */
|
||||
@@ -174,118 +174,17 @@ impl<'map> OsuGradualPerformanceAttributes<'map> {
|
||||
state: OsuScoreState,
|
||||
n: usize,
|
||||
) -> Option<OsuPerformanceAttributes> {
|
||||
let n = n.min(self.difficulty.len()).saturating_sub(1);
|
||||
let difficulty = self.difficulty.nth(n)?;
|
||||
let sub = (self.difficulty.idx == 0) as usize;
|
||||
let difficulty = self.difficulty.nth(n.saturating_sub(sub))?;
|
||||
|
||||
let performance = self
|
||||
.performance
|
||||
.clone()
|
||||
.attributes(difficulty)
|
||||
.state(state)
|
||||
.passed_objects(self.difficulty.idx)
|
||||
.passed_objects(self.difficulty.idx + 1)
|
||||
.calculate();
|
||||
|
||||
Some(performance)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[allow(unused_imports)]
|
||||
use super::*;
|
||||
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||
#[test]
|
||||
fn correct_empty() {
|
||||
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
|
||||
let mods = 64;
|
||||
|
||||
let mut gradual = OsuGradualPerformanceAttributes::new(&map, mods);
|
||||
let state = OsuScoreState::default();
|
||||
|
||||
assert!(gradual
|
||||
.process_next_n_objects(state.clone(), usize::MAX)
|
||||
.is_some());
|
||||
assert!(gradual.process_next_object(state).is_none());
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||
#[test]
|
||||
fn next_and_next_n() {
|
||||
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
|
||||
let mods = 64;
|
||||
let state = OsuScoreState::default();
|
||||
|
||||
let mut gradual1 = OsuGradualPerformanceAttributes::new(&map, mods);
|
||||
let mut gradual2 = OsuGradualPerformanceAttributes::new(&map, mods);
|
||||
|
||||
for _ in 0..20 {
|
||||
let _ = gradual1.process_next_object(state.clone());
|
||||
let _ = gradual2.process_next_object(state.clone());
|
||||
}
|
||||
|
||||
let n = 80;
|
||||
|
||||
for _ in 1..n {
|
||||
let _ = gradual1.process_next_object(state.clone());
|
||||
}
|
||||
|
||||
let state = OsuScoreState {
|
||||
max_combo: 122,
|
||||
n300: 88,
|
||||
n100: 8,
|
||||
n50: 2,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
let next = gradual1.process_next_object(state.clone());
|
||||
let next_n = gradual2.process_next_n_objects(state, n);
|
||||
|
||||
assert_eq!(next_n, next);
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||
#[test]
|
||||
fn gradual_end_eq_regular() {
|
||||
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
|
||||
let mods = 64;
|
||||
let regular = OsuPP::new(&map).mods(mods).calculate();
|
||||
|
||||
let mut gradual = OsuGradualPerformanceAttributes::new(&map, mods);
|
||||
|
||||
let state = OsuScoreState {
|
||||
max_combo: 909,
|
||||
n300: 601,
|
||||
n100: 0,
|
||||
n50: 0,
|
||||
n_misses: 0,
|
||||
};
|
||||
|
||||
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
|
||||
|
||||
assert_eq!(regular, gradual_end);
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||
#[test]
|
||||
fn gradual_eq_regular_passed() {
|
||||
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
|
||||
let mods = 64;
|
||||
let n = 100;
|
||||
let regular = OsuPP::new(&map).mods(mods).passed_objects(n).calculate();
|
||||
|
||||
let mut gradual = OsuGradualPerformanceAttributes::new(&map, mods);
|
||||
|
||||
let state = OsuScoreState {
|
||||
max_combo: 122,
|
||||
n300: 100,
|
||||
n100: 0,
|
||||
n50: 0,
|
||||
n_misses: 0,
|
||||
};
|
||||
|
||||
let gradual = gradual.process_next_n_objects(state, n).unwrap();
|
||||
|
||||
assert_eq!(regular, gradual);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -260,7 +260,7 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
|
||||
400.0 * (time_preempt / PREEMPT_MIN).min(1.0)
|
||||
};
|
||||
|
||||
let mut attributes = OsuDifficultyAttributes {
|
||||
let mut attrs = OsuDifficultyAttributes {
|
||||
ar: map_attrs.ar,
|
||||
hp: map_attrs.hp,
|
||||
od: map_attrs.od,
|
||||
@@ -269,7 +269,7 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
|
||||
|
||||
let mut params = ObjectParameters {
|
||||
map,
|
||||
attributes: &mut attributes,
|
||||
attrs: &mut attrs,
|
||||
ticks: Vec::new(),
|
||||
curve_bufs: CurveBuffers::default(),
|
||||
};
|
||||
@@ -301,7 +301,7 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
|
||||
|
||||
let last = match hit_objects.next() {
|
||||
Some(prev) => prev,
|
||||
None => return (skills, attributes),
|
||||
None => return (skills, attrs),
|
||||
};
|
||||
|
||||
let mut last_last = None;
|
||||
@@ -340,7 +340,7 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
|
||||
}
|
||||
}
|
||||
|
||||
(skills, attributes)
|
||||
(skills, attrs)
|
||||
}
|
||||
|
||||
fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
||||
|
||||
@@ -86,7 +86,7 @@ pub(crate) enum NestedObjectKind {
|
||||
|
||||
pub(crate) struct ObjectParameters<'a> {
|
||||
pub(crate) map: &'a Beatmap,
|
||||
pub(crate) attributes: &'a mut OsuDifficultyAttributes,
|
||||
pub(crate) attrs: &'a mut OsuDifficultyAttributes,
|
||||
pub(crate) ticks: Vec<(Pos2, f64)>,
|
||||
pub(crate) curve_bufs: CurveBuffers,
|
||||
}
|
||||
@@ -95,17 +95,17 @@ impl OsuObject {
|
||||
pub(crate) fn new(h: &HitObject, params: &mut ObjectParameters<'_>) -> Option<Self> {
|
||||
let ObjectParameters {
|
||||
map,
|
||||
attributes,
|
||||
attrs,
|
||||
ticks,
|
||||
curve_bufs,
|
||||
} = params;
|
||||
|
||||
attributes.max_combo += 1; // hitcircle, slider head, or spinner
|
||||
attrs.max_combo += 1; // hitcircle, slider head, or spinner
|
||||
let pos = h.pos;
|
||||
|
||||
let obj = match &h.kind {
|
||||
HitObjectKind::Circle => {
|
||||
attributes.n_circles += 1;
|
||||
attrs.n_circles += 1;
|
||||
|
||||
Self {
|
||||
start_time: h.start_time,
|
||||
@@ -121,7 +121,7 @@ impl OsuObject {
|
||||
control_points,
|
||||
..
|
||||
} => {
|
||||
attributes.n_sliders += 1;
|
||||
attrs.n_sliders += 1;
|
||||
|
||||
let timing_point = map.timing_point_at(h.start_time);
|
||||
let difficulty_point = map.difficulty_point_at(h.start_time).unwrap_or_default();
|
||||
@@ -283,7 +283,7 @@ impl OsuObject {
|
||||
_ => nested_objects.push(legacy_last_tick),
|
||||
};
|
||||
|
||||
attributes.max_combo += nested_objects.len();
|
||||
attrs.max_combo += nested_objects.len();
|
||||
|
||||
let last_time = nested_objects
|
||||
.last()
|
||||
@@ -318,7 +318,7 @@ impl OsuObject {
|
||||
}
|
||||
}
|
||||
HitObjectKind::Spinner { end_time } => {
|
||||
attributes.n_spinners += 1;
|
||||
attrs.n_spinners += 1;
|
||||
|
||||
Self {
|
||||
start_time: h.start_time,
|
||||
|
||||
+29
-49
@@ -21,15 +21,15 @@ use crate::{
|
||||
/// let pp_result = OsuPP::new(&map)
|
||||
/// .mods(8 + 64) // HDDT
|
||||
/// .combo(1234)
|
||||
/// .misses(1)
|
||||
/// .n_misses(1)
|
||||
/// .accuracy(98.5) // should be set last
|
||||
/// .calculate();
|
||||
///
|
||||
/// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars());
|
||||
///
|
||||
/// let next_result = OsuPP::new(&map)
|
||||
/// .attributes(pp_result) // reusing previous results for performance
|
||||
/// .mods(8 + 64) // has to be the same to reuse attributes
|
||||
/// .attributes(pp_result) // reusing previous results for performance
|
||||
/// .mods(8 + 64) // has to be the same to reuse attributes
|
||||
/// .accuracy(99.5)
|
||||
/// .calculate();
|
||||
///
|
||||
@@ -739,13 +739,17 @@ impl OsuAttributeProvider for PerformanceAttributes {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::Beatmap;
|
||||
|
||||
fn test_attrs() -> OsuDifficultyAttributes {
|
||||
OsuDifficultyAttributes {
|
||||
fn test_data() -> (Beatmap, OsuDifficultyAttributes) {
|
||||
let path = "./maps/2785319.osu";
|
||||
let map = Beatmap::from_path(path).unwrap();
|
||||
|
||||
let attrs = OsuDifficultyAttributes {
|
||||
aim: 2.8693628443424104,
|
||||
speed: 2.533869745015772,
|
||||
flashlight: 2.288770487900865,
|
||||
@@ -759,38 +763,14 @@ mod test {
|
||||
n_spinners: 1,
|
||||
stars: 5.669858729379631,
|
||||
max_combo: 909,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
|
||||
fn test_map() -> (Beatmap, OsuDifficultyAttributes) {
|
||||
let path = "./maps/2785319.osu";
|
||||
let map = Beatmap::from_path(path).unwrap();
|
||||
|
||||
(map, test_attrs())
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async_tokio", feature = "async_str"))]
|
||||
async fn test_map() -> (Beatmap, OsuDifficultyAttributes) {
|
||||
let path = "./maps/2785319.osu";
|
||||
let map = Beatmap::from_path(path).await.unwrap();
|
||||
|
||||
(map, test_attrs())
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
macro_rules! test_data {
|
||||
() => {{
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
|
||||
{ test_map() }
|
||||
#[cfg(any(feature = "async_tokio", feature = "async_str"))]
|
||||
{ test_map().await }
|
||||
}};
|
||||
(map, attrs)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osu_hitresults_n300_n100_n_misses_best() {
|
||||
let (map, attrs) = test_data!();
|
||||
fn hitresults_n300_n100_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
@@ -814,8 +794,8 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osu_hitresults_n300_n50_n_misses_best() {
|
||||
let (map, attrs) = test_data!();
|
||||
fn hitresults_n300_n50_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
@@ -839,8 +819,8 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osu_hitresults_n50_n_misses_worst() {
|
||||
let (map, attrs) = test_data!();
|
||||
fn hitresults_n50_n_misses_worst() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
@@ -863,8 +843,8 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osu_hitresults_n300_n100_n50_n_misses_worst() {
|
||||
let (map, attrs) = test_data!();
|
||||
fn hitresults_n300_n100_n50_n_misses_worst() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
@@ -889,8 +869,8 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osu_hitresults_acc_n_misses_best() {
|
||||
let (map, attrs) = test_data!();
|
||||
fn hitresults_acc_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
@@ -919,8 +899,8 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osu_hitresults_acc_n100_n_misses_best() {
|
||||
let (map, attrs) = test_data!();
|
||||
fn hitresults_acc_n100_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
@@ -950,8 +930,8 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osu_hitresults_acc_n50_n_misses_best() {
|
||||
let (map, attrs) = test_data!();
|
||||
fn hitresults_acc_n50_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
@@ -981,8 +961,8 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osu_hitresults_acc_best() {
|
||||
let (map, attrs) = test_data!();
|
||||
fn hitresults_acc_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
@@ -1010,8 +990,8 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osu_hitresults_acc_worst() {
|
||||
let (map, attrs) = test_data!();
|
||||
fn hitresults_acc_worst() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
|
||||
Reference in New Issue
Block a user