feat!: distinguish stable|lazer|lazer+classic
This commit is contained in:
@@ -14,3 +14,4 @@ cc 2cba8a76243aac7233e9207a3162aaa1f08f933c0cb3a2ac79580ece3a7329fc # shrinks to
|
||||
cc e93787ad8a849ec6d05750c8d09494b8f5a9fa785f843d9a8e2db986c0b32645 # shrinks to acc = 0.0, n300 = None, n100 = None, n50 = None, n_misses = Some(602), best_case = false
|
||||
cc a53cb48861126aa63be54606f9a770db5eae95242c9a9d75cf1fd101cfb21729 # shrinks to lazer = true, acc = 0.5679586776392227, n_slider_ticks = None, n_slider_ends = None, n300 = None, n100 = None, n50 = Some(0), n_misses = None, best_case = false
|
||||
cc cacb94cb2a61cf05e7083e332b378290a6267a499bf30821228bc0ae4dfe46f6 # shrinks to lazer = true, acc = 0.5270982297689498, n_slider_ticks = None, n_slider_ends = None, n300 = Some(70), n100 = None, n50 = None, n_misses = None, best_case = false
|
||||
cc 5679a686382f641f1fa3407a6e19e1caa0adff27e42c397778a2d178361719a3 # shrinks to lazer = true, classic = false, acc = 0.4911232243285752, large_tick_hits = None, slider_end_hits = Some(0), n300 = None, n100 = None, n50 = None, n_misses = None, best_case = false
|
||||
|
||||
@@ -316,12 +316,19 @@ impl<'map> Performance<'map> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify the amount of hit slider ticks.
|
||||
/// Specify the amount of "large tick" hits.
|
||||
///
|
||||
/// Only relevant for osu!standard.
|
||||
pub fn n_slider_ticks(self, n_slider_ticks: u32) -> Self {
|
||||
///
|
||||
/// The meaning depends on the kind of score:
|
||||
/// - if set on osu!stable, this value is irrelevant and can be `0`
|
||||
/// - if set on osu!lazer *without* `CL`, this value is the amount of hit
|
||||
/// slider ticks and repeats
|
||||
/// - if set on osu!lazer *with* `CL`, this value is the amount of hit
|
||||
/// slider heads, ticks, and repeats
|
||||
pub fn large_tick_hits(self, large_tick_hits: u32) -> Self {
|
||||
if let Self::Osu(osu) = self {
|
||||
Self::Osu(osu.n_slider_ticks(n_slider_ticks))
|
||||
Self::Osu(osu.large_tick_hits(large_tick_hits))
|
||||
} else {
|
||||
self
|
||||
}
|
||||
@@ -330,6 +337,9 @@ impl<'map> Performance<'map> {
|
||||
/// Specify the amount of hit slider ends.
|
||||
///
|
||||
/// Only relevant for osu!standard.
|
||||
///
|
||||
/// osu! calls this value "slider tail hits" without the classic
|
||||
/// mod and "small tick hits" with the classic mod.
|
||||
pub fn n_slider_ends(self, n_slider_ends: u32) -> Self {
|
||||
if let Self::Osu(osu) = self {
|
||||
Self::Osu(osu.n_slider_ends(n_slider_ends))
|
||||
|
||||
+14
-9
@@ -15,10 +15,15 @@ pub struct ScoreState {
|
||||
///
|
||||
/// Irrelevant for osu!mania.
|
||||
pub max_combo: u32,
|
||||
/// Amount of successfully hit slider ticks and repeats.
|
||||
/// "Large tick" hits for osu!standard.
|
||||
///
|
||||
/// Only relevant for osu!standard in lazer.
|
||||
pub slider_tick_hits: u32,
|
||||
/// The meaning depends on the kind of score:
|
||||
/// - if set on osu!stable, this field is irrelevant and can be `0`
|
||||
/// - if set on osu!lazer *without* `CL`, this field is the amount of hit
|
||||
/// slider ticks and repeats
|
||||
/// - if set on osu!lazer *with* `CL`, this field is the amount of hit
|
||||
/// slider heads, ticks, and repeats
|
||||
pub osu_large_tick_hits: u32,
|
||||
/// Amount of successfully hit slider ends.
|
||||
///
|
||||
/// Only relevant for osu!standard in lazer.
|
||||
@@ -43,7 +48,7 @@ impl ScoreState {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
max_combo: 0,
|
||||
slider_tick_hits: 0,
|
||||
osu_large_tick_hits: 0,
|
||||
slider_end_hits: 0,
|
||||
n_geki: 0,
|
||||
n_katu: 0,
|
||||
@@ -76,7 +81,7 @@ impl From<ScoreState> for OsuScoreState {
|
||||
fn from(state: ScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: state.max_combo,
|
||||
slider_tick_hits: state.slider_tick_hits,
|
||||
large_tick_hits: state.osu_large_tick_hits,
|
||||
slider_end_hits: state.slider_end_hits,
|
||||
n300: state.n300,
|
||||
n100: state.n100,
|
||||
@@ -127,7 +132,7 @@ impl From<OsuScoreState> for ScoreState {
|
||||
fn from(state: OsuScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: state.max_combo,
|
||||
slider_tick_hits: state.slider_tick_hits,
|
||||
osu_large_tick_hits: state.large_tick_hits,
|
||||
slider_end_hits: state.slider_end_hits,
|
||||
n_geki: 0,
|
||||
n_katu: 0,
|
||||
@@ -143,7 +148,7 @@ impl From<TaikoScoreState> for ScoreState {
|
||||
fn from(state: TaikoScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: state.max_combo,
|
||||
slider_tick_hits: 0,
|
||||
osu_large_tick_hits: 0,
|
||||
slider_end_hits: 0,
|
||||
n_geki: 0,
|
||||
n_katu: 0,
|
||||
@@ -159,7 +164,7 @@ impl From<CatchScoreState> for ScoreState {
|
||||
fn from(state: CatchScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: state.max_combo,
|
||||
slider_tick_hits: 0,
|
||||
osu_large_tick_hits: 0,
|
||||
slider_end_hits: 0,
|
||||
n_geki: 0,
|
||||
n_katu: state.tiny_droplet_misses,
|
||||
@@ -175,7 +180,7 @@ impl From<ManiaScoreState> for ScoreState {
|
||||
fn from(state: ManiaScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: 0,
|
||||
slider_tick_hits: 0,
|
||||
osu_large_tick_hits: 0,
|
||||
slider_end_hits: 0,
|
||||
n_geki: state.n320,
|
||||
n_katu: state.n200,
|
||||
|
||||
@@ -474,7 +474,7 @@ impl<'map> TryFrom<OsuPerformance<'map>> for CatchPerformance<'map> {
|
||||
difficulty,
|
||||
acc,
|
||||
combo,
|
||||
slider_tick_hits: _,
|
||||
large_tick_hits: _,
|
||||
slider_end_hits: _,
|
||||
n300,
|
||||
n100,
|
||||
|
||||
@@ -845,7 +845,7 @@ impl<'map> TryFrom<OsuPerformance<'map>> for ManiaPerformance<'map> {
|
||||
difficulty,
|
||||
acc,
|
||||
combo: _,
|
||||
slider_tick_hits: _,
|
||||
large_tick_hits: _,
|
||||
slider_end_hits: _,
|
||||
n300,
|
||||
n100,
|
||||
|
||||
+3
-4
@@ -102,12 +102,10 @@ impl GameMods {
|
||||
GameModsInner::Lazer(ref mods) => mods
|
||||
.iter()
|
||||
.find_map(|m| match m {
|
||||
GameMod::ClassicOsu(classic) => Some(classic),
|
||||
GameMod::ClassicOsu(cl) => Some(cl.no_slider_head_accuracy.unwrap_or(true)),
|
||||
_ => None,
|
||||
})
|
||||
.map_or(!lazer, |classic| {
|
||||
classic.no_slider_head_accuracy.unwrap_or(true)
|
||||
}),
|
||||
.unwrap_or(!lazer),
|
||||
GameModsInner::Intermode(ref mods) => {
|
||||
mods.contains(GameModIntermode::Classic) || !lazer
|
||||
}
|
||||
@@ -230,6 +228,7 @@ impl_has_mod! {
|
||||
fl: + Flashlight ["Flashlight"],
|
||||
so: + SpunOut ["SpunOut"],
|
||||
bl: - Blinds ["Blinds"],
|
||||
cl: - Classic ["Classic"],
|
||||
tc: - Traceable ["Traceable"],
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ pub use self::{
|
||||
convert::OsuBeatmap,
|
||||
difficulty::gradual::OsuGradualDifficulty,
|
||||
performance::{gradual::OsuGradualPerformance, OsuPerformance},
|
||||
score_state::OsuScoreState,
|
||||
score_state::{OsuScoreOrigin, OsuScoreState},
|
||||
strains::OsuStrains,
|
||||
};
|
||||
|
||||
|
||||
+205
-118
@@ -14,7 +14,7 @@ use crate::{
|
||||
use super::{
|
||||
attributes::{OsuDifficultyAttributes, OsuPerformanceAttributes},
|
||||
difficulty::skills::{flashlight::Flashlight, strain::OsuStrainSkill},
|
||||
score_state::OsuScoreState,
|
||||
score_state::{OsuScoreOrigin, OsuScoreState},
|
||||
Osu,
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ pub struct OsuPerformance<'map> {
|
||||
pub(crate) difficulty: Difficulty,
|
||||
pub(crate) acc: Option<f64>,
|
||||
pub(crate) combo: Option<u32>,
|
||||
pub(crate) slider_tick_hits: Option<u32>,
|
||||
pub(crate) large_tick_hits: Option<u32>,
|
||||
pub(crate) slider_end_hits: Option<u32>,
|
||||
pub(crate) n300: Option<u32>,
|
||||
pub(crate) n100: Option<u32>,
|
||||
@@ -167,11 +167,16 @@ impl<'map> OsuPerformance<'map> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify the amount of hit slider ticks.
|
||||
/// Specify the amount of "large tick" hits.
|
||||
///
|
||||
/// Only relevant for osu!lazer.
|
||||
pub const fn n_slider_ticks(mut self, n_slider_ticks: u32) -> Self {
|
||||
self.slider_tick_hits = Some(n_slider_ticks);
|
||||
/// The meaning depends on the kind of score:
|
||||
/// - if set on osu!stable, this value is irrelevant and can be `0`
|
||||
/// - if set on osu!lazer *without* `CL`, this value is the amount of hit
|
||||
/// slider ticks and repeats
|
||||
/// - if set on osu!lazer *with* `CL`, this value is the amount of hit
|
||||
/// slider heads, ticks, and repeats
|
||||
pub const fn large_tick_hits(mut self, large_tick_hits: u32) -> Self {
|
||||
self.large_tick_hits = Some(large_tick_hits);
|
||||
|
||||
self
|
||||
}
|
||||
@@ -179,6 +184,9 @@ impl<'map> OsuPerformance<'map> {
|
||||
/// Specify the amount of hit slider ends.
|
||||
///
|
||||
/// Only relevant for osu!lazer.
|
||||
///
|
||||
/// osu! calls this value "slider tail hits" without the classic
|
||||
/// mod and "small tick hits" with the classic mod.
|
||||
pub const fn n_slider_ends(mut self, n_slider_ends: u32) -> Self {
|
||||
self.slider_end_hits = Some(n_slider_ends);
|
||||
|
||||
@@ -312,7 +320,7 @@ impl<'map> OsuPerformance<'map> {
|
||||
pub const fn state(mut self, state: OsuScoreState) -> Self {
|
||||
let OsuScoreState {
|
||||
max_combo,
|
||||
slider_tick_hits,
|
||||
large_tick_hits,
|
||||
slider_end_hits,
|
||||
n300,
|
||||
n100,
|
||||
@@ -321,7 +329,7 @@ impl<'map> OsuPerformance<'map> {
|
||||
} = state;
|
||||
|
||||
self.combo = Some(max_combo);
|
||||
self.slider_tick_hits = Some(slider_tick_hits);
|
||||
self.large_tick_hits = Some(large_tick_hits);
|
||||
self.slider_end_hits = Some(slider_end_hits);
|
||||
self.n300 = Some(n300);
|
||||
self.n100 = Some(n100);
|
||||
@@ -365,29 +373,67 @@ impl<'map> OsuPerformance<'map> {
|
||||
let mut n100 = self.n100.map_or(0, |n| cmp::min(n, n_remaining));
|
||||
let mut n50 = self.n50.map_or(0, |n| cmp::min(n, n_remaining));
|
||||
|
||||
let classic = self.difficulty.get_mods().cl();
|
||||
let lazer = self.difficulty.get_lazer();
|
||||
|
||||
let (n_slider_ends, n_slider_ticks, max_slider_ends, max_slider_ticks) = if lazer {
|
||||
let n_slider_ends = self
|
||||
.slider_end_hits
|
||||
.map_or(attrs.n_sliders, |n| cmp::min(n, attrs.n_sliders));
|
||||
let n_slider_ticks = self
|
||||
.slider_tick_hits
|
||||
.map_or(attrs.n_slider_ticks, |n| cmp::min(n, attrs.n_slider_ticks));
|
||||
let (origin, slider_end_hits, large_tick_hits) = match (lazer, classic) {
|
||||
(false, _) => (OsuScoreOrigin::Stable, 0, 0),
|
||||
(true, false) => {
|
||||
let origin = OsuScoreOrigin::LazerWithoutClassic {
|
||||
max_large_ticks: attrs.n_slider_ticks,
|
||||
max_slider_ends: attrs.n_sliders,
|
||||
};
|
||||
|
||||
(
|
||||
n_slider_ends,
|
||||
n_slider_ticks,
|
||||
attrs.n_sliders,
|
||||
attrs.n_slider_ticks,
|
||||
)
|
||||
} else {
|
||||
(0, 0, 0, 0)
|
||||
let slider_end_hits = self
|
||||
.slider_end_hits
|
||||
.map_or(attrs.n_sliders, |n| cmp::min(n, attrs.n_sliders));
|
||||
|
||||
let large_tick_hits = self
|
||||
.large_tick_hits
|
||||
.map_or(attrs.n_slider_ticks, |n| cmp::min(n, attrs.n_slider_ticks));
|
||||
|
||||
(origin, slider_end_hits, large_tick_hits)
|
||||
}
|
||||
(true, true) => {
|
||||
let origin = OsuScoreOrigin::LazerWithClassic {
|
||||
max_large_ticks: attrs.n_sliders + attrs.n_slider_ticks,
|
||||
max_slider_ends: attrs.n_sliders,
|
||||
};
|
||||
|
||||
let slider_end_hits = self
|
||||
.slider_end_hits
|
||||
.map_or(attrs.n_sliders, |n| cmp::min(n, attrs.n_sliders));
|
||||
|
||||
let large_tick_hits = self
|
||||
.large_tick_hits
|
||||
.map_or(attrs.n_sliders + attrs.n_slider_ticks, |n| {
|
||||
cmp::min(n, attrs.n_sliders + attrs.n_slider_ticks)
|
||||
});
|
||||
|
||||
(origin, slider_end_hits, large_tick_hits)
|
||||
}
|
||||
};
|
||||
|
||||
let (slider_acc_value, max_slider_acc_value) = match origin {
|
||||
OsuScoreOrigin::Stable => (0, 0),
|
||||
OsuScoreOrigin::LazerWithoutClassic {
|
||||
max_large_ticks,
|
||||
max_slider_ends,
|
||||
} => (
|
||||
150 * slider_end_hits + 30 * large_tick_hits,
|
||||
150 * max_slider_ends + 30 * max_large_ticks,
|
||||
),
|
||||
OsuScoreOrigin::LazerWithClassic {
|
||||
max_large_ticks,
|
||||
max_slider_ends,
|
||||
} => (
|
||||
30 * large_tick_hits + 10 * slider_end_hits,
|
||||
30 * max_large_ticks + 10 * max_slider_ends,
|
||||
),
|
||||
};
|
||||
|
||||
if let Some(acc) = self.acc {
|
||||
let target_total =
|
||||
acc * f64::from(30 * n_objects + 15 * max_slider_ends + 3 * max_slider_ticks);
|
||||
let target_total = acc * f64::from(300 * n_objects + max_slider_acc_value);
|
||||
|
||||
match (self.n300, self.n100, self.n50) {
|
||||
(Some(_), Some(_), Some(_)) => {
|
||||
@@ -408,10 +454,8 @@ impl<'map> OsuPerformance<'map> {
|
||||
let n_remaining = n_remaining - n300;
|
||||
|
||||
let raw_n100 = (target_total
|
||||
- f64::from(
|
||||
5 * n_remaining + 30 * n300 + 15 * n_slider_ends + 3 * n_slider_ticks,
|
||||
))
|
||||
/ 5.0;
|
||||
- f64::from(50 * n_remaining + 300 * n300 + slider_acc_value))
|
||||
/ 50.0;
|
||||
let min_n100 = cmp::min(n_remaining, raw_n100.floor() as u32);
|
||||
let max_n100 = cmp::min(n_remaining, raw_n100.ceil() as u32);
|
||||
|
||||
@@ -423,11 +467,11 @@ impl<'map> OsuPerformance<'map> {
|
||||
n100: new100,
|
||||
n50: new50,
|
||||
misses,
|
||||
n_slider_ticks,
|
||||
n_slider_ends,
|
||||
large_tick_hits,
|
||||
slider_end_hits,
|
||||
};
|
||||
|
||||
let dist = (acc - accuracy(state, max_slider_ticks, max_slider_ends)).abs();
|
||||
let dist = (acc - state.accuracy(origin)).abs();
|
||||
|
||||
if dist < best_dist {
|
||||
best_dist = dist;
|
||||
@@ -443,10 +487,8 @@ impl<'map> OsuPerformance<'map> {
|
||||
let n_remaining = n_remaining - n100;
|
||||
|
||||
let raw_n300 = (target_total
|
||||
- f64::from(
|
||||
5 * n_remaining + 10 * n100 + 15 * n_slider_ends + 3 * n_slider_ticks,
|
||||
))
|
||||
/ 25.0;
|
||||
- f64::from(50 * n_remaining + 100 * n100 + slider_acc_value))
|
||||
/ 250.0;
|
||||
let min_n300 = cmp::min(n_remaining, raw_n300.floor() as u32);
|
||||
let max_n300 = cmp::min(n_remaining, raw_n300.ceil() as u32);
|
||||
|
||||
@@ -458,12 +500,11 @@ impl<'map> OsuPerformance<'map> {
|
||||
n100,
|
||||
n50: new50,
|
||||
misses,
|
||||
n_slider_ticks,
|
||||
n_slider_ends,
|
||||
large_tick_hits,
|
||||
slider_end_hits,
|
||||
};
|
||||
|
||||
let curr_dist =
|
||||
(acc - accuracy(state, max_slider_ticks, max_slider_ends)).abs();
|
||||
let curr_dist = (acc - state.accuracy(origin)).abs();
|
||||
|
||||
if curr_dist < best_dist {
|
||||
best_dist = curr_dist;
|
||||
@@ -478,9 +519,9 @@ impl<'map> OsuPerformance<'map> {
|
||||
n50 = cmp::min(n50, n_remaining);
|
||||
let n_remaining = n_remaining - n50;
|
||||
|
||||
let raw_n300 = (target_total + f64::from(10 * misses + 5 * n50)
|
||||
- f64::from(10 * n_objects + 15 * n_slider_ends + 3 * n_slider_ticks))
|
||||
/ 20.0;
|
||||
let raw_n300 = (target_total + f64::from(100 * misses + 50 * n50)
|
||||
- f64::from(100 * n_objects + slider_acc_value))
|
||||
/ 200.0;
|
||||
|
||||
let min_n300 = cmp::min(n_remaining, raw_n300.floor() as u32);
|
||||
let max_n300 = cmp::min(n_remaining, raw_n300.ceil() as u32);
|
||||
@@ -493,12 +534,11 @@ impl<'map> OsuPerformance<'map> {
|
||||
n100: new100,
|
||||
n50,
|
||||
misses,
|
||||
n_slider_ticks,
|
||||
n_slider_ends,
|
||||
large_tick_hits,
|
||||
slider_end_hits,
|
||||
};
|
||||
|
||||
let curr_dist =
|
||||
(acc - accuracy(state, max_slider_ticks, max_slider_ends)).abs();
|
||||
let curr_dist = (acc - state.accuracy(origin)).abs();
|
||||
|
||||
if curr_dist < best_dist {
|
||||
best_dist = curr_dist;
|
||||
@@ -510,21 +550,15 @@ impl<'map> OsuPerformance<'map> {
|
||||
(None, None, None) => {
|
||||
let mut best_dist = f64::MAX;
|
||||
|
||||
let raw_n300 = (target_total
|
||||
- f64::from(5 * n_remaining + 15 * n_slider_ends + 3 * n_slider_ticks))
|
||||
/ 25.0;
|
||||
let raw_n300 =
|
||||
(target_total - f64::from(50 * n_remaining + slider_acc_value)) / 250.0;
|
||||
let min_n300 = cmp::min(n_remaining, raw_n300.floor() as u32);
|
||||
let max_n300 = cmp::min(n_remaining, raw_n300.ceil() as u32);
|
||||
|
||||
for new300 in min_n300..=max_n300 {
|
||||
let raw_n100 = (target_total
|
||||
- f64::from(
|
||||
5 * n_remaining
|
||||
+ 25 * new300
|
||||
+ 15 * n_slider_ends
|
||||
+ 3 * n_slider_ticks,
|
||||
))
|
||||
/ 5.0;
|
||||
- f64::from(50 * n_remaining + 250 * new300 + slider_acc_value))
|
||||
/ 50.0;
|
||||
let min_n100 = cmp::min(raw_n100.floor() as u32, n_remaining - new300);
|
||||
let max_n100 = cmp::min(raw_n100.ceil() as u32, n_remaining - new300);
|
||||
|
||||
@@ -536,12 +570,11 @@ impl<'map> OsuPerformance<'map> {
|
||||
n100: new100,
|
||||
n50: new50,
|
||||
misses,
|
||||
n_slider_ticks,
|
||||
n_slider_ends,
|
||||
large_tick_hits,
|
||||
slider_end_hits,
|
||||
};
|
||||
|
||||
let curr_dist =
|
||||
(acc - accuracy(state, max_slider_ticks, max_slider_ends)).abs();
|
||||
let curr_dist = (acc - state.accuracy(origin)).abs();
|
||||
|
||||
if curr_dist < best_dist {
|
||||
best_dist = curr_dist;
|
||||
@@ -596,8 +629,8 @@ impl<'map> OsuPerformance<'map> {
|
||||
});
|
||||
|
||||
self.combo = Some(max_combo);
|
||||
self.slider_end_hits = Some(n_slider_ends);
|
||||
self.slider_tick_hits = Some(n_slider_ticks);
|
||||
self.slider_end_hits = Some(slider_end_hits);
|
||||
self.large_tick_hits = Some(large_tick_hits);
|
||||
self.n300 = Some(n300);
|
||||
self.n100 = Some(n100);
|
||||
self.n50 = Some(n50);
|
||||
@@ -605,8 +638,8 @@ impl<'map> OsuPerformance<'map> {
|
||||
|
||||
OsuScoreState {
|
||||
max_combo,
|
||||
slider_tick_hits: n_slider_ticks,
|
||||
slider_end_hits: n_slider_ends,
|
||||
large_tick_hits,
|
||||
slider_end_hits,
|
||||
n300,
|
||||
n100,
|
||||
n50,
|
||||
@@ -625,6 +658,7 @@ impl<'map> OsuPerformance<'map> {
|
||||
|
||||
let mods = self.difficulty.get_mods();
|
||||
let lazer = self.difficulty.get_lazer();
|
||||
let classic = mods.cl();
|
||||
let using_classic_slider_acc = mods.no_slider_head_acc(lazer);
|
||||
|
||||
let mut effective_miss_count = f64::from(state.misses);
|
||||
@@ -661,16 +695,24 @@ impl<'map> OsuPerformance<'map> {
|
||||
effective_miss_count = effective_miss_count.max(f64::from(state.misses));
|
||||
effective_miss_count = effective_miss_count.min(f64::from(state.total_hits()));
|
||||
|
||||
let (n_slider_ends, n_slider_ticks) = if lazer {
|
||||
(attrs.n_sliders, attrs.n_slider_ticks)
|
||||
} else {
|
||||
(0, 0)
|
||||
let origin = match (lazer, classic) {
|
||||
(false, _) => OsuScoreOrigin::Stable,
|
||||
(true, false) => OsuScoreOrigin::LazerWithoutClassic {
|
||||
max_large_ticks: attrs.n_slider_ticks,
|
||||
max_slider_ends: attrs.n_sliders,
|
||||
},
|
||||
(true, true) => OsuScoreOrigin::LazerWithClassic {
|
||||
max_large_ticks: attrs.n_sliders + attrs.n_slider_ticks,
|
||||
max_slider_ends: attrs.n_sliders,
|
||||
},
|
||||
};
|
||||
|
||||
let acc = state.accuracy(origin);
|
||||
|
||||
let inner = OsuPerformanceInner {
|
||||
attrs,
|
||||
mods,
|
||||
acc: state.accuracy(n_slider_ticks, n_slider_ends),
|
||||
acc,
|
||||
state,
|
||||
effective_miss_count,
|
||||
using_classic_slider_acc,
|
||||
@@ -685,7 +727,7 @@ impl<'map> OsuPerformance<'map> {
|
||||
difficulty: Difficulty::new(),
|
||||
acc: None,
|
||||
combo: None,
|
||||
slider_tick_hits: None,
|
||||
large_tick_hits: None,
|
||||
slider_end_hits: None,
|
||||
n300: None,
|
||||
n100: None,
|
||||
@@ -1047,7 +1089,7 @@ const fn n_slider_ends_dropped(attrs: &OsuDifficultyAttributes, state: &OsuScore
|
||||
}
|
||||
|
||||
const fn n_slider_tick_miss(attrs: &OsuDifficultyAttributes, state: &OsuScoreState) -> u32 {
|
||||
attrs.n_slider_ticks - state.slider_tick_hits
|
||||
attrs.n_slider_ticks - state.large_tick_hits
|
||||
}
|
||||
|
||||
struct NoComboState {
|
||||
@@ -1055,31 +1097,45 @@ struct NoComboState {
|
||||
n100: u32,
|
||||
n50: u32,
|
||||
misses: u32,
|
||||
n_slider_ticks: u32,
|
||||
n_slider_ends: u32,
|
||||
large_tick_hits: u32,
|
||||
slider_end_hits: u32,
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn accuracy(state: NoComboState, max_slider_ticks: u32, max_slider_ends: u32) -> f64 {
|
||||
let NoComboState {
|
||||
n300,
|
||||
n100,
|
||||
n50,
|
||||
misses,
|
||||
n_slider_ticks,
|
||||
n_slider_ends,
|
||||
} = state;
|
||||
impl NoComboState {
|
||||
fn accuracy(&self, origin: OsuScoreOrigin) -> f64 {
|
||||
let mut numerator = 300 * self.n300 + 100 * self.n100 + 50 * self.n50;
|
||||
let mut denominator = 300 * (self.n300 + self.n100 + self.n50 + self.misses);
|
||||
|
||||
if n_slider_ticks + n_slider_ends + n300 + n100 + n50 + misses == 0 {
|
||||
return 0.0;
|
||||
match origin {
|
||||
OsuScoreOrigin::Stable => {}
|
||||
OsuScoreOrigin::LazerWithoutClassic {
|
||||
max_large_ticks,
|
||||
max_slider_ends,
|
||||
} => {
|
||||
let slider_end_hits = self.slider_end_hits.min(max_slider_ends);
|
||||
let large_tick_hits = self.large_tick_hits.min(max_large_ticks);
|
||||
|
||||
numerator += 150 * slider_end_hits + 30 * large_tick_hits;
|
||||
denominator += 150 * max_slider_ends + 30 * max_large_ticks;
|
||||
}
|
||||
OsuScoreOrigin::LazerWithClassic {
|
||||
max_large_ticks,
|
||||
max_slider_ends,
|
||||
} => {
|
||||
let large_tick_hits = self.large_tick_hits.min(max_large_ticks);
|
||||
let slider_end_hits = self.slider_end_hits.min(max_slider_ends);
|
||||
|
||||
numerator += 30 * large_tick_hits + 10 * slider_end_hits;
|
||||
denominator += 30 * max_large_ticks + 10 * max_slider_ends;
|
||||
}
|
||||
}
|
||||
|
||||
if denominator == 0 {
|
||||
0.0
|
||||
} else {
|
||||
f64::from(numerator) / f64::from(denominator)
|
||||
}
|
||||
}
|
||||
|
||||
let numerator = 300 * n300 + 100 * n100 + 50 * n50 + 150 * n_slider_ends + 30 * n_slider_ticks;
|
||||
|
||||
let denominator =
|
||||
300 * (n300 + n100 + n50 + misses) + 150 * max_slider_ends + 30 * max_slider_ticks;
|
||||
|
||||
f64::from(numerator) / f64::from(denominator)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1087,6 +1143,7 @@ mod test {
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use proptest::prelude::*;
|
||||
use rosu_mods::{GameModIntermode, GameModsIntermode};
|
||||
|
||||
use crate::{
|
||||
any::{DifficultyAttributes, PerformanceAttributes},
|
||||
@@ -1135,9 +1192,10 @@ mod test {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn brute_force_best(
|
||||
lazer: bool,
|
||||
classic: bool,
|
||||
acc: f64,
|
||||
n_slider_ticks: Option<u32>,
|
||||
n_slider_ends: Option<u32>,
|
||||
large_tick_hits: Option<u32>,
|
||||
slider_end_hits: Option<u32>,
|
||||
n300: Option<u32>,
|
||||
n100: Option<u32>,
|
||||
n50: Option<u32>,
|
||||
@@ -1146,20 +1204,41 @@ mod test {
|
||||
) -> OsuScoreState {
|
||||
let misses = cmp::min(misses, N_OBJECTS);
|
||||
|
||||
let (n_slider_ends, n_slider_ticks, max_slider_ends, max_slider_ticks) = if lazer {
|
||||
let n_slider_ends = n_slider_ends.map_or(N_SLIDERS, |n| cmp::min(n, N_SLIDERS));
|
||||
let n_slider_ticks =
|
||||
n_slider_ticks.map_or(N_SLIDER_TICKS, |n| cmp::min(n, N_SLIDER_TICKS));
|
||||
let (origin, slider_end_hits, large_tick_hits) = match (lazer, classic) {
|
||||
(false, _) => (OsuScoreOrigin::Stable, 0, 0),
|
||||
(true, false) => {
|
||||
let origin = OsuScoreOrigin::LazerWithoutClassic {
|
||||
max_large_ticks: N_SLIDER_TICKS,
|
||||
max_slider_ends: N_SLIDERS,
|
||||
};
|
||||
|
||||
(n_slider_ends, n_slider_ticks, N_SLIDERS, N_SLIDER_TICKS)
|
||||
} else {
|
||||
(0, 0, 0, 0)
|
||||
let slider_end_hits = slider_end_hits.map_or(N_SLIDERS, |n| cmp::min(n, N_SLIDERS));
|
||||
|
||||
let large_tick_hits =
|
||||
large_tick_hits.map_or(N_SLIDER_TICKS, |n| cmp::min(n, N_SLIDER_TICKS));
|
||||
|
||||
(origin, slider_end_hits, large_tick_hits)
|
||||
}
|
||||
(true, true) => {
|
||||
let origin = OsuScoreOrigin::LazerWithClassic {
|
||||
max_large_ticks: N_SLIDERS + N_SLIDER_TICKS,
|
||||
max_slider_ends: N_SLIDERS,
|
||||
};
|
||||
|
||||
let slider_end_hits = slider_end_hits.map_or(N_SLIDERS, |n| cmp::min(n, N_SLIDERS));
|
||||
|
||||
let large_tick_hits = large_tick_hits.map_or(N_SLIDERS + N_SLIDER_TICKS, |n| {
|
||||
cmp::min(n, N_SLIDERS + N_SLIDER_TICKS)
|
||||
});
|
||||
|
||||
(origin, slider_end_hits, large_tick_hits)
|
||||
}
|
||||
};
|
||||
|
||||
let mut best_state = OsuScoreState {
|
||||
misses,
|
||||
slider_end_hits: n_slider_ends,
|
||||
slider_tick_hits: n_slider_ticks,
|
||||
slider_end_hits,
|
||||
large_tick_hits,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -1200,11 +1279,11 @@ mod test {
|
||||
n100: new100,
|
||||
n50: new50,
|
||||
misses,
|
||||
n_slider_ticks,
|
||||
n_slider_ends,
|
||||
large_tick_hits,
|
||||
slider_end_hits,
|
||||
};
|
||||
|
||||
let curr_acc = accuracy(state, max_slider_ticks, max_slider_ends);
|
||||
let curr_acc = state.accuracy(origin);
|
||||
let curr_dist = (acc - curr_acc).abs();
|
||||
|
||||
if curr_dist < best_dist {
|
||||
@@ -1249,9 +1328,10 @@ mod test {
|
||||
#[test]
|
||||
fn hitresults(
|
||||
lazer in prop::bool::ANY,
|
||||
classic in prop::bool::ANY,
|
||||
acc in 0.0_f64..=1.0,
|
||||
n_slider_ticks in prop::option::weighted(0.1, 0_u32..=N_SLIDER_TICKS + 10),
|
||||
n_slider_ends in prop::option::weighted(0.1, 0_u32..=N_SLIDERS + 10),
|
||||
large_tick_hits in prop::option::weighted(0.1, 0_u32..=N_SLIDERS + N_SLIDER_TICKS + 10),
|
||||
slider_end_hits in prop::option::weighted(0.1, 0_u32..=N_SLIDERS + 10),
|
||||
n300 in prop::option::weighted(0.1, 0_u32..=N_OBJECTS + 10),
|
||||
n100 in prop::option::weighted(0.1, 0_u32..=N_OBJECTS + 10),
|
||||
n50 in prop::option::weighted(0.1, 0_u32..=N_OBJECTS + 10),
|
||||
@@ -1272,11 +1352,17 @@ mod test {
|
||||
.lazer(lazer)
|
||||
.hitresult_priority(priority);
|
||||
|
||||
if let Some(n_slider_ticks) = n_slider_ticks {
|
||||
state = state.n_slider_ticks(n_slider_ticks);
|
||||
if lazer && classic {
|
||||
let mut mods = GameModsIntermode::new();
|
||||
mods.insert(GameModIntermode::Classic);
|
||||
state = state.mods(mods);
|
||||
}
|
||||
|
||||
if let Some(n_slider_ends) = n_slider_ends {
|
||||
if let Some(large_tick_hits) = large_tick_hits {
|
||||
state = state.large_tick_hits(large_tick_hits);
|
||||
}
|
||||
|
||||
if let Some(n_slider_ends) = slider_end_hits {
|
||||
state = state.n_slider_ends(n_slider_ends);
|
||||
}
|
||||
|
||||
@@ -1302,9 +1388,10 @@ mod test {
|
||||
|
||||
let mut expected = brute_force_best(
|
||||
lazer,
|
||||
classic,
|
||||
acc,
|
||||
n_slider_ticks,
|
||||
n_slider_ends,
|
||||
large_tick_hits,
|
||||
slider_end_hits,
|
||||
n300,
|
||||
n100,
|
||||
n50,
|
||||
@@ -1330,7 +1417,7 @@ mod test {
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
slider_tick_hits: N_SLIDER_TICKS,
|
||||
large_tick_hits: N_SLIDER_TICKS,
|
||||
slider_end_hits: N_SLIDERS,
|
||||
n300: 300,
|
||||
n100: 20,
|
||||
@@ -1354,7 +1441,7 @@ mod test {
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
slider_tick_hits: 0,
|
||||
large_tick_hits: 0,
|
||||
slider_end_hits: 0,
|
||||
n300: 300,
|
||||
n100: 289,
|
||||
@@ -1377,7 +1464,7 @@ mod test {
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
slider_tick_hits: N_SLIDER_TICKS,
|
||||
large_tick_hits: N_SLIDER_TICKS,
|
||||
slider_end_hits: N_SLIDERS,
|
||||
n300: 0,
|
||||
n100: 589,
|
||||
@@ -1402,7 +1489,7 @@ mod test {
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
slider_tick_hits: 0,
|
||||
large_tick_hits: 0,
|
||||
slider_end_hits: 0,
|
||||
n300: 300,
|
||||
n100: 50,
|
||||
|
||||
+56
-33
@@ -4,10 +4,15 @@ pub struct OsuScoreState {
|
||||
/// Maximum combo that the score has had so far. **Not** the maximum
|
||||
/// possible combo of the map so far.
|
||||
pub max_combo: u32,
|
||||
/// Amount of successfully hit slider ticks and repeat.
|
||||
/// "Large tick" hits.
|
||||
///
|
||||
/// Only relevant for osu!lazer.
|
||||
pub slider_tick_hits: u32,
|
||||
/// The meaning depends on the kind of score:
|
||||
/// - if set on osu!stable, this field is irrelevant and can be `0`
|
||||
/// - if set on osu!lazer *without* `CL`, this field is the amount of hit
|
||||
/// slider ticks and repeats
|
||||
/// - if set on osu!lazer *with* `CL`, this field is the amount of hit
|
||||
/// slider heads, ticks, and repeats
|
||||
pub large_tick_hits: u32,
|
||||
/// Amount of successfully hit slider ends.
|
||||
///
|
||||
/// Only relevant for osu!lazer.
|
||||
@@ -27,7 +32,7 @@ impl OsuScoreState {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
max_combo: 0,
|
||||
slider_tick_hits: 0,
|
||||
large_tick_hits: 0,
|
||||
slider_end_hits: 0,
|
||||
n300: 0,
|
||||
n100: 0,
|
||||
@@ -42,37 +47,39 @@ impl OsuScoreState {
|
||||
}
|
||||
|
||||
/// Calculate the accuracy between `0.0` and `1.0` for this state.
|
||||
///
|
||||
/// `max_slider_ticks` and `max_slider_ends` are only relevant for
|
||||
/// `osu!lazer` scores. Otherwise, they may be `0`.
|
||||
pub fn accuracy(&self, max_slider_ticks: u32, max_slider_ends: u32) -> f64 {
|
||||
if self.total_hits() + self.slider_tick_hits + self.slider_end_hits == 0 {
|
||||
return 0.0;
|
||||
pub fn accuracy(&self, origin: OsuScoreOrigin) -> f64 {
|
||||
let mut numerator = 300 * self.n300 + 100 * self.n100 + 50 * self.n50;
|
||||
let mut denominator = 300 * (self.n300 + self.n100 + self.n50 + self.misses);
|
||||
|
||||
match origin {
|
||||
OsuScoreOrigin::Stable => {}
|
||||
OsuScoreOrigin::LazerWithoutClassic {
|
||||
max_large_ticks,
|
||||
max_slider_ends,
|
||||
} => {
|
||||
let slider_end_hits = self.slider_end_hits.min(max_slider_ends);
|
||||
let large_tick_hits = self.large_tick_hits.min(max_large_ticks);
|
||||
|
||||
numerator += 150 * slider_end_hits + 30 * large_tick_hits;
|
||||
denominator += 150 * max_slider_ends + 30 * max_large_ticks;
|
||||
}
|
||||
OsuScoreOrigin::LazerWithClassic {
|
||||
max_large_ticks,
|
||||
max_slider_ends,
|
||||
} => {
|
||||
let large_tick_hits = self.large_tick_hits.min(max_large_ticks);
|
||||
let slider_end_hits = self.slider_end_hits.min(max_slider_ends);
|
||||
|
||||
numerator += 30 * large_tick_hits + 10 * slider_end_hits;
|
||||
denominator += 30 * max_large_ticks + 10 * max_slider_ends;
|
||||
}
|
||||
}
|
||||
|
||||
debug_assert!(
|
||||
self.slider_end_hits <= max_slider_ends,
|
||||
"`self.slider_end_hits` must not be greater than `max_slider_ends`"
|
||||
);
|
||||
debug_assert!(
|
||||
self.slider_tick_hits <= max_slider_ticks,
|
||||
"`self.slider_tick_hits` must not be greater than `max_slider_ticks`"
|
||||
);
|
||||
|
||||
let numerator = 300 * self.n300
|
||||
+ 100 * self.n100
|
||||
+ 50 * self.n50
|
||||
+ 150 * self.slider_end_hits
|
||||
+ 30 * self.slider_tick_hits;
|
||||
|
||||
let denominator = 300 * self.n300
|
||||
+ 300 * self.n100
|
||||
+ 300 * self.n50
|
||||
+ 300 * self.misses
|
||||
+ 150 * max_slider_ends
|
||||
+ 30 * max_slider_ticks;
|
||||
|
||||
f64::from(numerator) / f64::from(denominator)
|
||||
if denominator == 0 {
|
||||
0.0
|
||||
} else {
|
||||
f64::from(numerator) / f64::from(denominator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,3 +88,19 @@ impl Default for OsuScoreState {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum OsuScoreOrigin {
|
||||
/// For scores set on osu!stable
|
||||
Stable,
|
||||
/// For scores set on osu!lazer without the `Classic` mod
|
||||
LazerWithoutClassic {
|
||||
max_large_ticks: u32,
|
||||
max_slider_ends: u32,
|
||||
},
|
||||
/// For scores set on osu!lazer with the `Classic` mod
|
||||
LazerWithClassic {
|
||||
max_large_ticks: u32,
|
||||
max_slider_ends: u32,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -364,7 +364,7 @@ impl<'map> TryFrom<OsuPerformance<'map>> for TaikoPerformance<'map> {
|
||||
difficulty,
|
||||
acc,
|
||||
combo,
|
||||
slider_tick_hits: _,
|
||||
large_tick_hits: _,
|
||||
slider_end_hits: _,
|
||||
n300,
|
||||
n100,
|
||||
|
||||
Reference in New Issue
Block a user