fix: adjust hitresult gen for mania on lazer
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user