fix: adjust hitresult gen for mania on lazer

This commit is contained in:
MaxOhn
2024-11-11 20:15:18 +01:00
parent 78d7bd3873
commit e0c53af27a
10 changed files with 115 additions and 40 deletions
-2
View File
@@ -274,8 +274,6 @@ impl Difficulty {
/// score. /// score.
/// ///
/// Defaults to `true`. /// Defaults to `true`.
///
/// Only relevant for osu!standard performance calculation.
pub const fn lazer(mut self, lazer: bool) -> Self { pub const fn lazer(mut self, lazer: bool) -> Self {
self.lazer = Some(lazer); self.lazer = Some(lazer);
+5 -5
View File
@@ -307,12 +307,12 @@ impl<'map> Performance<'map> {
/// This affects internal accuracy calculation because lazer considers /// This affects internal accuracy calculation because lazer considers
/// slider heads for accuracy whereas stable does not. /// slider heads for accuracy whereas stable does not.
/// ///
/// Only relevant for osu!standard. /// Only relevant for osu!standard and osu!mania.
pub fn lazer(self, lazer: bool) -> Self { pub fn lazer(self, lazer: bool) -> Self {
if let Self::Osu(osu) = self { match self {
Self::Osu(osu.lazer(lazer)) Self::Osu(o) => Self::Osu(o.lazer(lazer)),
} else { Self::Taiko(_) | Self::Catch(_) => self,
self Self::Mania(m) => Self::Mania(m.lazer(lazer)),
} }
} }
+2
View File
@@ -9,6 +9,8 @@ pub struct ManiaDifficultyAttributes {
pub hit_window: f64, pub hit_window: f64,
/// The amount of hitobjects in the map. /// The amount of hitobjects in the map.
pub n_objects: u32, pub n_objects: u32,
/// The amount of hold notes in the map.
pub n_hold_notes: u32,
/// The maximum achievable combo. /// The maximum achievable combo.
pub max_combo: u32, pub max_combo: u32,
/// Whether the [`Beatmap`] was a convert i.e. an osu!standard map. /// Whether the [`Beatmap`] was a convert i.e. an osu!standard map.
+19 -11
View File
@@ -54,7 +54,13 @@ pub struct ManiaGradualDifficulty {
strain: Strain, strain: Strain,
diff_objects: Box<[ManiaDifficultyObject]>, diff_objects: Box<[ManiaDifficultyObject]>,
hit_window: f64, hit_window: f64,
note_state: NoteState,
}
#[derive(Default)]
struct NoteState {
curr_combo: u32, curr_combo: u32,
n_hold_notes: u32,
} }
impl ManiaGradualDifficulty { impl ManiaGradualDifficulty {
@@ -80,7 +86,7 @@ impl ManiaGradualDifficulty {
let strain = Strain::new(total_columns as usize); let strain = Strain::new(total_columns as usize);
let mut curr_combo = 0; let mut note_state = NoteState::default();
let objects_is_circle: Box<[_]> = converted let objects_is_circle: Box<[_]> = converted
.hit_objects .hit_objects
@@ -95,7 +101,7 @@ impl ManiaGradualDifficulty {
objects_is_circle[0], objects_is_circle[0],
hit_object.start_time, hit_object.start_time,
hit_object.end_time, hit_object.end_time,
&mut curr_combo, &mut note_state,
); );
} }
@@ -107,7 +113,7 @@ impl ManiaGradualDifficulty {
strain, strain,
diff_objects, diff_objects,
hit_window, hit_window,
curr_combo, note_state,
} }
} }
} }
@@ -128,7 +134,7 @@ impl Iterator for ManiaGradualDifficulty {
increment_combo( increment_combo(
is_circle, is_circle,
curr, curr,
&mut self.curr_combo, &mut self.note_state,
self.difficulty.get_clock_rate(), self.difficulty.get_clock_rate(),
); );
} else if self.objects_is_circle.is_empty() { } else if self.objects_is_circle.is_empty() {
@@ -140,8 +146,9 @@ impl Iterator for ManiaGradualDifficulty {
Some(ManiaDifficultyAttributes { Some(ManiaDifficultyAttributes {
stars: self.strain.as_difficulty_value() * DIFFICULTY_MULTIPLIER, stars: self.strain.as_difficulty_value() * DIFFICULTY_MULTIPLIER,
hit_window: self.hit_window, hit_window: self.hit_window,
max_combo: self.curr_combo, max_combo: self.note_state.curr_combo,
n_objects: self.idx as u32, n_objects: self.idx as u32,
n_hold_notes: self.note_state.n_hold_notes,
is_convert: self.is_convert, is_convert: self.is_convert,
}) })
} }
@@ -171,7 +178,7 @@ impl Iterator for ManiaGradualDifficulty {
let clock_rate = self.difficulty.get_clock_rate(); let clock_rate = self.difficulty.get_clock_rate();
for (curr, is_circle) in skip_iter.take(take) { for (curr, is_circle) in skip_iter.take(take) {
increment_combo(*is_circle, curr, &mut self.curr_combo, clock_rate); increment_combo(*is_circle, curr, &mut self.note_state, clock_rate);
strain.process(curr); strain.process(curr);
self.idx += 1; self.idx += 1;
} }
@@ -189,22 +196,23 @@ impl ExactSizeIterator for ManiaGradualDifficulty {
fn increment_combo( fn increment_combo(
is_circle: bool, is_circle: bool,
diff_obj: &ManiaDifficultyObject, diff_obj: &ManiaDifficultyObject,
curr_combo: &mut u32, state: &mut NoteState,
clock_rate: f64, clock_rate: f64,
) { ) {
increment_combo_raw( increment_combo_raw(
is_circle, is_circle,
diff_obj.start_time * clock_rate, diff_obj.start_time * clock_rate,
diff_obj.end_time * clock_rate, diff_obj.end_time * clock_rate,
curr_combo, state,
); );
} }
fn increment_combo_raw(is_circle: bool, start_time: f64, end_time: f64, curr_combo: &mut u32) { fn increment_combo_raw(is_circle: bool, start_time: f64, end_time: f64, state: &mut NoteState) {
if is_circle { if is_circle {
*curr_combo += 1; state.curr_combo += 1;
} else { } else {
*curr_combo += 1 + ((end_time - start_time) / 100.0) as u32; state.curr_combo += 1 + ((end_time - start_time) / 100.0) as u32;
state.n_hold_notes += 1;
} }
} }
+4 -1
View File
@@ -35,6 +35,7 @@ pub fn difficulty(
hit_window, hit_window,
max_combo: values.max_combo, max_combo: values.max_combo,
n_objects, n_objects,
n_hold_notes: values.n_hold_notes,
is_convert: converted.is_convert, is_convert: converted.is_convert,
} }
} }
@@ -42,6 +43,7 @@ pub fn difficulty(
pub struct DifficultyValues { pub struct DifficultyValues {
pub strain: Strain, pub strain: Strain,
pub max_combo: u32, pub max_combo: u32,
pub n_hold_notes: u32,
} }
impl DifficultyValues { impl DifficultyValues {
@@ -71,7 +73,8 @@ impl DifficultyValues {
Self { Self {
strain, strain,
max_combo: params.into_max_combo(), max_combo: params.max_combo(),
n_hold_notes: params.n_hold_notes(),
} }
} }
+9 -1
View File
@@ -47,6 +47,7 @@ impl ManiaObject {
let duration = (slider.span_count() as f64) * dist / velocity; let duration = (slider.span_count() as f64) * dist / velocity;
params.max_combo += (duration / 100.0) as u32; params.max_combo += (duration / 100.0) as u32;
params.n_hold_notes += 1;
Self { Self {
start_time: h.start_time, start_time: h.start_time,
@@ -57,6 +58,7 @@ impl ManiaObject {
HitObjectKind::Spinner(Spinner { duration }) HitObjectKind::Spinner(Spinner { duration })
| HitObjectKind::Hold(HoldNote { duration }) => { | HitObjectKind::Hold(HoldNote { duration }) => {
params.max_combo += (duration / 100.0) as u32; params.max_combo += (duration / 100.0) as u32;
params.n_hold_notes += 1;
Self { Self {
start_time: h.start_time, start_time: h.start_time,
@@ -77,6 +79,7 @@ impl ManiaObject {
pub struct ObjectParams<'a> { pub struct ObjectParams<'a> {
map: &'a Beatmap, map: &'a Beatmap,
max_combo: u32, max_combo: u32,
n_hold_notes: u32,
curve_bufs: CurveBuffers, curve_bufs: CurveBuffers,
} }
@@ -85,11 +88,16 @@ impl<'a> ObjectParams<'a> {
Self { Self {
map, map,
max_combo: 0, max_combo: 0,
n_hold_notes: 0,
curve_bufs: CurveBuffers::default(), curve_bufs: CurveBuffers::default(),
} }
} }
pub fn into_max_combo(self) -> u32 { pub fn max_combo(&self) -> u32 {
self.max_combo self.max_combo
} }
pub fn n_hold_notes(&self) -> u32 {
self.n_hold_notes
}
} }
+7
View File
@@ -146,6 +146,13 @@ mod tests {
for i in 1.. { for i in 1.. {
state.misses += 1; state.misses += 1;
// Hold notes award two hitresults in lazer
if let Some(h) = converted.hit_objects.get(i - 1) {
if !h.is_circle() {
state.n320 += 1;
}
}
let Some(next_gradual) = gradual.next(state.clone()) else { let Some(next_gradual) = gradual.next(state.clone()) else {
assert_eq!(i, hit_objects_len + 1); assert_eq!(i, hit_objects_len + 1);
assert!(gradual_2nd.last(state.clone()).is_some() || hit_objects_len % 2 == 0); assert!(gradual_2nd.last(state.clone()).is_some() || hit_objects_len % 2 == 0);
+57 -14
View File
@@ -169,6 +169,19 @@ impl<'map> ManiaPerformance<'map> {
self self
} }
/// Whether the calculated attributes belong to an osu!lazer or osu!stable
/// score.
///
/// Defaults to `true`.
///
/// This affects internal hitresult generation because lazer gives two
/// hitresults per hold note whereas stable only gives one.
pub fn lazer(mut self, lazer: bool) -> Self {
self.difficulty = self.difficulty.lazer(lazer);
self
}
/// Specify the amount of 320s of a play. /// Specify the amount of 320s of a play.
pub const fn n320(mut self, n320: u32) -> Self { pub const fn n320(mut self, n320: u32) -> Self {
self.n320 = Some(n320); self.n320 = Some(n320);
@@ -245,11 +258,16 @@ impl<'map> ManiaPerformance<'map> {
MapOrAttrs::Attrs(ref attrs) => attrs, MapOrAttrs::Attrs(ref attrs) => attrs,
}; };
let n_objects = cmp::min(self.difficulty.get_passed_objects() as u32, attrs.n_objects); let mut n_objects = cmp::min(self.difficulty.get_passed_objects() as u32, attrs.n_objects);
let priority = self.hitresult_priority; let priority = self.hitresult_priority;
let misses = self.misses.map_or(0, |n| cmp::min(n, n_objects)); let misses = self.misses.map_or(0, |n| cmp::min(n, n_objects));
if self.difficulty.get_lazer() {
n_objects += attrs.n_hold_notes;
}
let n_remaining = n_objects - misses; let n_remaining = n_objects - misses;
let mut n320 = self.n320.map_or(0, |n| cmp::min(n, n_remaining)); let mut n320 = self.n320.map_or(0, |n| cmp::min(n, n_remaining));
@@ -950,6 +968,7 @@ mod tests {
static ATTRS: OnceLock<ManiaDifficultyAttributes> = OnceLock::new(); static ATTRS: OnceLock<ManiaDifficultyAttributes> = OnceLock::new();
const N_OBJECTS: u32 = 594; const N_OBJECTS: u32 = 594;
const N_HOLD_NOTES: u32 = 121;
fn beatmap() -> Beatmap { fn beatmap() -> Beatmap {
Beatmap::from_path("./resources/1638954.osu").unwrap() Beatmap::from_path("./resources/1638954.osu").unwrap()
@@ -962,6 +981,14 @@ mod tests {
let attrs = Difficulty::new().with_mode().calculate(&converted); let attrs = Difficulty::new().with_mode().calculate(&converted);
assert_eq!(N_OBJECTS, converted.hit_objects.len() as u32); assert_eq!(N_OBJECTS, converted.hit_objects.len() as u32);
assert_eq!(
N_HOLD_NOTES,
converted
.hit_objects
.iter()
.filter(|h| !h.is_circle())
.count() as u32
);
attrs attrs
}) })
@@ -975,6 +1002,7 @@ mod tests {
/// that it doesn't run unreasonably long. /// that it doesn't run unreasonably long.
#[allow(clippy::too_many_arguments, clippy::too_many_lines)] #[allow(clippy::too_many_arguments, clippy::too_many_lines)]
fn brute_force_best( fn brute_force_best(
lazer: bool,
acc: f64, acc: f64,
n320: Option<u32>, n320: Option<u32>,
n300: Option<u32>, n300: Option<u32>,
@@ -994,7 +1022,11 @@ mod tests {
let mut best_dist = f64::INFINITY; let mut best_dist = f64::INFINITY;
let mut best_custom_acc = 0.0; let mut best_custom_acc = 0.0;
let n_remaining = N_OBJECTS - misses; let mut n_remaining = N_OBJECTS - misses;
if lazer {
n_remaining += N_HOLD_NOTES;
}
let multiple_given = (usize::from(n320.is_some()) let multiple_given = (usize::from(n320.is_some())
+ usize::from(n300.is_some()) + usize::from(n300.is_some())
@@ -1003,17 +1035,23 @@ mod tests {
+ usize::from(n50.is_some())) + usize::from(n50.is_some()))
> 1; > 1;
let max_left = N_OBJECTS let mut n_objects = N_OBJECTS;
if lazer {
n_objects += N_HOLD_NOTES;
}
let max_left = n_objects
.saturating_sub(n200.unwrap_or(0) + n100.unwrap_or(0) + n50.unwrap_or(0) + misses); .saturating_sub(n200.unwrap_or(0) + n100.unwrap_or(0) + n50.unwrap_or(0) + misses);
let min_n3x0 = cmp::min( let min_n3x0 = cmp::min(
max_left, max_left,
(acc * f64::from(3 * N_OBJECTS) - f64::from(2 * n_remaining)).floor() as u32, (acc * f64::from(3 * n_objects) - f64::from(2 * n_remaining)).floor() as u32,
); );
let max_n3x0 = cmp::min( let max_n3x0 = cmp::min(
max_left, max_left,
((acc * f64::from(6 * N_OBJECTS) - f64::from(n_remaining)) / 5.0).ceil() as u32, ((acc * f64::from(6 * n_objects) - f64::from(n_remaining)) / 5.0).ceil() as u32,
); );
let (min_n3x0, max_n3x0) = match (n320, n300) { let (min_n3x0, max_n3x0) = match (n320, n300) {
@@ -1086,9 +1124,9 @@ mod tests {
let curr_dist = (acc - curr_acc).abs(); let curr_dist = (acc - curr_acc).abs();
let curr_custom_acc = let curr_custom_acc =
custom_accuracy(new320, new300, new200, new100, new50, N_OBJECTS); custom_accuracy(new320, new300, new200, new100, new50, n_objects);
match curr_dist.partial_cmp(&best_dist).expect("non-NaN") { match curr_dist.total_cmp(&best_dist) {
Ordering::Less => { Ordering::Less => {
best_dist = curr_dist; best_dist = curr_dist;
best_custom_acc = curr_custom_acc; best_custom_acc = curr_custom_acc;
@@ -1194,13 +1232,14 @@ mod tests {
#[test] #[test]
fn mania_hitresults( fn mania_hitresults(
acc in 0.0..=1.0, lazer in prop::bool::ANY,
n320 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + 10), acc in 0.0_f64..=1.0,
n300 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + 10), n320 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + N_HOLD_NOTES + 10),
n200 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + 10), n300 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + N_HOLD_NOTES + 10),
n100 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + 10), n200 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + N_HOLD_NOTES + 10),
n50 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + 10), n100 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + N_HOLD_NOTES + 10),
n_misses in prop::option::weighted(0.15, 0_u32..=N_OBJECTS + 10), n50 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + N_HOLD_NOTES + 10),
n_misses in prop::option::weighted(0.15, 0_u32..=N_OBJECTS + N_HOLD_NOTES + 10),
best_case in prop::bool::ANY, best_case in prop::bool::ANY,
) { ) {
let priority = if best_case { let priority = if best_case {
@@ -1211,6 +1250,7 @@ mod tests {
let mut state = ManiaPerformance::from(attrs()) let mut state = ManiaPerformance::from(attrs())
.accuracy(acc * 100.0) .accuracy(acc * 100.0)
.lazer(lazer)
.hitresult_priority(priority); .hitresult_priority(priority);
if let Some(n320) = n320 { if let Some(n320) = n320 {
@@ -1242,6 +1282,7 @@ mod tests {
assert_eq!(first, state); assert_eq!(first, state);
let expected = brute_force_best( let expected = brute_force_best(
lazer,
acc, acc,
n320, n320,
n300, n300,
@@ -1259,6 +1300,7 @@ mod tests {
#[test] #[test]
fn hitresults_n320_misses_best() { fn hitresults_n320_misses_best() {
let state = ManiaPerformance::from(attrs()) let state = ManiaPerformance::from(attrs())
.lazer(false)
.n320(500) .n320(500)
.misses(2) .misses(2)
.hitresult_priority(HitResultPriority::BestCase) .hitresult_priority(HitResultPriority::BestCase)
@@ -1279,6 +1321,7 @@ mod tests {
#[test] #[test]
fn hitresults_n100_n50_misses_worst() { fn hitresults_n100_n50_misses_worst() {
let state = ManiaPerformance::from(attrs()) let state = ManiaPerformance::from(attrs())
.lazer(false)
.n100(200) .n100(200)
.n50(50) .n50(50)
.misses(2) .misses(2)
+6
View File
@@ -112,6 +112,7 @@ macro_rules! test_cases {
stars: $stars:literal, stars: $stars:literal,
hit_window: $hit_window:literal, hit_window: $hit_window:literal,
n_objects: $n_objects:literal, n_objects: $n_objects:literal,
n_hold_notes: $n_hold_notes:literal,
max_combo: $max_combo:literal, max_combo: $max_combo:literal,
is_convert: $is_convert:literal, is_convert: $is_convert:literal,
}) => { }) => {
@@ -119,6 +120,7 @@ macro_rules! test_cases {
stars: $stars, stars: $stars,
hit_window: $hit_window, hit_window: $hit_window,
n_objects: $n_objects, n_objects: $n_objects,
n_hold_notes: $n_hold_notes,
max_combo: $max_combo, max_combo: $max_combo,
is_convert: $is_convert, is_convert: $is_convert,
} }
@@ -531,6 +533,7 @@ fn basic_mania() {
stars: 3.358304846842773, stars: 3.358304846842773,
hit_window: 40.0, hit_window: 40.0,
n_objects: 594, n_objects: 594,
n_hold_notes: 121,
max_combo: 956, max_combo: 956,
is_convert: false, is_convert: false,
}; };
@@ -538,6 +541,7 @@ fn basic_mania() {
stars: 4.6072892053157295, stars: 4.6072892053157295,
hit_window: 40.0, hit_window: 40.0,
n_objects: 594, n_objects: 594,
n_hold_notes: 121,
max_combo: 956, max_combo: 956,
is_convert: false, is_convert: false,
}; };
@@ -553,6 +557,7 @@ fn convert_mania() {
stars: 3.2033142085672255, stars: 3.2033142085672255,
hit_window: 34.0, hit_window: 34.0,
n_objects: 1046, n_objects: 1046,
n_hold_notes: 293,
max_combo: 1381, max_combo: 1381,
is_convert: true, is_convert: true,
}; };
@@ -560,6 +565,7 @@ fn convert_mania() {
stars: 4.2934063021960185, stars: 4.2934063021960185,
hit_window: 34.0, hit_window: 34.0,
n_objects: 1046, n_objects: 1046,
n_hold_notes: 293,
max_combo: 1381, max_combo: 1381,
is_convert: true, is_convert: true,
}; };
+6 -6
View File
@@ -359,9 +359,9 @@ fn convert_catch() {
fn basic_mania() { fn basic_mania() {
test_cases! { test_cases! {
Mania: MANIA { Mania: MANIA {
NM => { pp: 108.08430593303873, pp_difficulty: 108.08430593303873 }; NM => { pp: 108.92297471705167, pp_difficulty: 108.92297471705167 };
EZ => { pp: 54.04215296651937, pp_difficulty: 108.08430593303873 }; EZ => { pp: 54.46148735852584, pp_difficulty: 108.92297471705167 };
DT => { pp: 222.79838979800365, pp_difficulty: 222.79838979800365 }; DT => { pp: 224.52717042937203, pp_difficulty: 224.52717042937203 };
} }
}; };
} }
@@ -370,9 +370,9 @@ fn basic_mania() {
fn convert_mania() { fn convert_mania() {
test_cases! { test_cases! {
Mania: OSU { Mania: OSU {
NM => { pp: 99.73849552661329, pp_difficulty: 99.73849552661329 }; NM => { pp: 101.39189449271568, pp_difficulty: 101.39189449271568 };
EZ => { pp: 49.869247763306646, pp_difficulty: 99.73849552661329 }; EZ => { pp: 50.69594724635784, pp_difficulty: 101.39189449271568 };
DT => { pp: 195.23247718805612, pp_difficulty: 195.23247718805612 }; DT => { pp: 198.46891237015896, pp_difficulty: 198.46891237015896 };
} }
}; };
} }