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.
///
/// Defaults to `true`.
///
/// Only relevant for osu!standard performance calculation.
pub const fn lazer(mut self, lazer: bool) -> Self {
self.lazer = Some(lazer);
+5 -5
View File
@@ -307,12 +307,12 @@ impl<'map> Performance<'map> {
/// This affects internal accuracy calculation because lazer considers
/// 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 {
if let Self::Osu(osu) = self {
Self::Osu(osu.lazer(lazer))
} else {
self
match self {
Self::Osu(o) => Self::Osu(o.lazer(lazer)),
Self::Taiko(_) | Self::Catch(_) => self,
Self::Mania(m) => Self::Mania(m.lazer(lazer)),
}
}
+2
View File
@@ -9,6 +9,8 @@ pub struct ManiaDifficultyAttributes {
pub hit_window: f64,
/// The amount of hitobjects in the map.
pub n_objects: u32,
/// The amount of hold notes in the map.
pub n_hold_notes: u32,
/// The maximum achievable combo.
pub max_combo: u32,
/// 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,
diff_objects: Box<[ManiaDifficultyObject]>,
hit_window: f64,
note_state: NoteState,
}
#[derive(Default)]
struct NoteState {
curr_combo: u32,
n_hold_notes: u32,
}
impl ManiaGradualDifficulty {
@@ -80,7 +86,7 @@ impl ManiaGradualDifficulty {
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
.hit_objects
@@ -95,7 +101,7 @@ impl ManiaGradualDifficulty {
objects_is_circle[0],
hit_object.start_time,
hit_object.end_time,
&mut curr_combo,
&mut note_state,
);
}
@@ -107,7 +113,7 @@ impl ManiaGradualDifficulty {
strain,
diff_objects,
hit_window,
curr_combo,
note_state,
}
}
}
@@ -128,7 +134,7 @@ impl Iterator for ManiaGradualDifficulty {
increment_combo(
is_circle,
curr,
&mut self.curr_combo,
&mut self.note_state,
self.difficulty.get_clock_rate(),
);
} else if self.objects_is_circle.is_empty() {
@@ -140,8 +146,9 @@ impl Iterator for ManiaGradualDifficulty {
Some(ManiaDifficultyAttributes {
stars: self.strain.as_difficulty_value() * DIFFICULTY_MULTIPLIER,
hit_window: self.hit_window,
max_combo: self.curr_combo,
max_combo: self.note_state.curr_combo,
n_objects: self.idx as u32,
n_hold_notes: self.note_state.n_hold_notes,
is_convert: self.is_convert,
})
}
@@ -171,7 +178,7 @@ impl Iterator for ManiaGradualDifficulty {
let clock_rate = self.difficulty.get_clock_rate();
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);
self.idx += 1;
}
@@ -189,22 +196,23 @@ impl ExactSizeIterator for ManiaGradualDifficulty {
fn increment_combo(
is_circle: bool,
diff_obj: &ManiaDifficultyObject,
curr_combo: &mut u32,
state: &mut NoteState,
clock_rate: f64,
) {
increment_combo_raw(
is_circle,
diff_obj.start_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 {
*curr_combo += 1;
state.curr_combo += 1;
} 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,
max_combo: values.max_combo,
n_objects,
n_hold_notes: values.n_hold_notes,
is_convert: converted.is_convert,
}
}
@@ -42,6 +43,7 @@ pub fn difficulty(
pub struct DifficultyValues {
pub strain: Strain,
pub max_combo: u32,
pub n_hold_notes: u32,
}
impl DifficultyValues {
@@ -71,7 +73,8 @@ impl DifficultyValues {
Self {
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;
params.max_combo += (duration / 100.0) as u32;
params.n_hold_notes += 1;
Self {
start_time: h.start_time,
@@ -57,6 +58,7 @@ impl ManiaObject {
HitObjectKind::Spinner(Spinner { duration })
| HitObjectKind::Hold(HoldNote { duration }) => {
params.max_combo += (duration / 100.0) as u32;
params.n_hold_notes += 1;
Self {
start_time: h.start_time,
@@ -77,6 +79,7 @@ impl ManiaObject {
pub struct ObjectParams<'a> {
map: &'a Beatmap,
max_combo: u32,
n_hold_notes: u32,
curve_bufs: CurveBuffers,
}
@@ -85,11 +88,16 @@ impl<'a> ObjectParams<'a> {
Self {
map,
max_combo: 0,
n_hold_notes: 0,
curve_bufs: CurveBuffers::default(),
}
}
pub fn into_max_combo(self) -> u32 {
pub fn max_combo(&self) -> u32 {
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.. {
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 {
assert_eq!(i, hit_objects_len + 1);
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
}
/// 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.
pub const fn n320(mut self, n320: u32) -> Self {
self.n320 = Some(n320);
@@ -245,11 +258,16 @@ impl<'map> ManiaPerformance<'map> {
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 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 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();
const N_OBJECTS: u32 = 594;
const N_HOLD_NOTES: u32 = 121;
fn beatmap() -> Beatmap {
Beatmap::from_path("./resources/1638954.osu").unwrap()
@@ -962,6 +981,14 @@ mod tests {
let attrs = Difficulty::new().with_mode().calculate(&converted);
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
})
@@ -975,6 +1002,7 @@ mod tests {
/// that it doesn't run unreasonably long.
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
fn brute_force_best(
lazer: bool,
acc: f64,
n320: Option<u32>,
n300: Option<u32>,
@@ -994,7 +1022,11 @@ mod tests {
let mut best_dist = f64::INFINITY;
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())
+ usize::from(n300.is_some())
@@ -1003,17 +1035,23 @@ mod tests {
+ usize::from(n50.is_some()))
> 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);
let min_n3x0 = cmp::min(
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(
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) {
@@ -1086,9 +1124,9 @@ mod tests {
let curr_dist = (acc - curr_acc).abs();
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 => {
best_dist = curr_dist;
best_custom_acc = curr_custom_acc;
@@ -1194,13 +1232,14 @@ mod tests {
#[test]
fn mania_hitresults(
acc in 0.0..=1.0,
n320 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + 10),
n300 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + 10),
n200 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + 10),
n100 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + 10),
n50 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + 10),
n_misses in prop::option::weighted(0.15, 0_u32..=N_OBJECTS + 10),
lazer in prop::bool::ANY,
acc in 0.0_f64..=1.0,
n320 in prop::option::weighted(0.10, 0_u32..=N_OBJECTS + N_HOLD_NOTES + 10),
n300 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 + N_HOLD_NOTES + 10),
n100 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 + 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,
) {
let priority = if best_case {
@@ -1211,6 +1250,7 @@ mod tests {
let mut state = ManiaPerformance::from(attrs())
.accuracy(acc * 100.0)
.lazer(lazer)
.hitresult_priority(priority);
if let Some(n320) = n320 {
@@ -1242,6 +1282,7 @@ mod tests {
assert_eq!(first, state);
let expected = brute_force_best(
lazer,
acc,
n320,
n300,
@@ -1259,6 +1300,7 @@ mod tests {
#[test]
fn hitresults_n320_misses_best() {
let state = ManiaPerformance::from(attrs())
.lazer(false)
.n320(500)
.misses(2)
.hitresult_priority(HitResultPriority::BestCase)
@@ -1279,6 +1321,7 @@ mod tests {
#[test]
fn hitresults_n100_n50_misses_worst() {
let state = ManiaPerformance::from(attrs())
.lazer(false)
.n100(200)
.n50(50)
.misses(2)
+6
View File
@@ -112,6 +112,7 @@ macro_rules! test_cases {
stars: $stars:literal,
hit_window: $hit_window:literal,
n_objects: $n_objects:literal,
n_hold_notes: $n_hold_notes:literal,
max_combo: $max_combo:literal,
is_convert: $is_convert:literal,
}) => {
@@ -119,6 +120,7 @@ macro_rules! test_cases {
stars: $stars,
hit_window: $hit_window,
n_objects: $n_objects,
n_hold_notes: $n_hold_notes,
max_combo: $max_combo,
is_convert: $is_convert,
}
@@ -531,6 +533,7 @@ fn basic_mania() {
stars: 3.358304846842773,
hit_window: 40.0,
n_objects: 594,
n_hold_notes: 121,
max_combo: 956,
is_convert: false,
};
@@ -538,6 +541,7 @@ fn basic_mania() {
stars: 4.6072892053157295,
hit_window: 40.0,
n_objects: 594,
n_hold_notes: 121,
max_combo: 956,
is_convert: false,
};
@@ -553,6 +557,7 @@ fn convert_mania() {
stars: 3.2033142085672255,
hit_window: 34.0,
n_objects: 1046,
n_hold_notes: 293,
max_combo: 1381,
is_convert: true,
};
@@ -560,6 +565,7 @@ fn convert_mania() {
stars: 4.2934063021960185,
hit_window: 34.0,
n_objects: 1046,
n_hold_notes: 293,
max_combo: 1381,
is_convert: true,
};
+6 -6
View File
@@ -359,9 +359,9 @@ fn convert_catch() {
fn basic_mania() {
test_cases! {
Mania: MANIA {
NM => { pp: 108.08430593303873, pp_difficulty: 108.08430593303873 };
EZ => { pp: 54.04215296651937, pp_difficulty: 108.08430593303873 };
DT => { pp: 222.79838979800365, pp_difficulty: 222.79838979800365 };
NM => { pp: 108.92297471705167, pp_difficulty: 108.92297471705167 };
EZ => { pp: 54.46148735852584, pp_difficulty: 108.92297471705167 };
DT => { pp: 224.52717042937203, pp_difficulty: 224.52717042937203 };
}
};
}
@@ -370,9 +370,9 @@ fn basic_mania() {
fn convert_mania() {
test_cases! {
Mania: OSU {
NM => { pp: 99.73849552661329, pp_difficulty: 99.73849552661329 };
EZ => { pp: 49.869247763306646, pp_difficulty: 99.73849552661329 };
DT => { pp: 195.23247718805612, pp_difficulty: 195.23247718805612 };
NM => { pp: 101.39189449271568, pp_difficulty: 101.39189449271568 };
EZ => { pp: 50.69594724635784, pp_difficulty: 101.39189449271568 };
DT => { pp: 198.46891237015896, pp_difficulty: 198.46891237015896 };
}
};
}