feat!: adjust osu!standard hitresult generation
This commit is contained in:
@@ -15,6 +15,14 @@ pub struct ScoreState {
|
||||
///
|
||||
/// Irrelevant for osu!mania.
|
||||
pub max_combo: u32,
|
||||
/// Amount of successfully hit slider ticks and repeats.
|
||||
///
|
||||
/// Only relevant for osu!standard in lazer.
|
||||
pub slider_tick_hits: u32,
|
||||
/// Amount of successfully hit slider ends.
|
||||
///
|
||||
/// Only relevant for osu!standard in lazer.
|
||||
pub slider_end_hits: u32,
|
||||
/// Amount of current gekis (n320 for osu!mania).
|
||||
pub n_geki: u32,
|
||||
/// Amount of current katus (tiny droplet misses for osu!catch / n200 for
|
||||
@@ -35,6 +43,8 @@ impl ScoreState {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
max_combo: 0,
|
||||
slider_tick_hits: 0,
|
||||
slider_end_hits: 0,
|
||||
n_geki: 0,
|
||||
n_katu: 0,
|
||||
n300: 0,
|
||||
@@ -66,6 +76,8 @@ impl From<ScoreState> for OsuScoreState {
|
||||
fn from(state: ScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: state.max_combo,
|
||||
slider_tick_hits: state.slider_tick_hits,
|
||||
slider_end_hits: state.slider_end_hits,
|
||||
n300: state.n300,
|
||||
n100: state.n100,
|
||||
n50: state.n50,
|
||||
@@ -115,6 +127,8 @@ impl From<OsuScoreState> for ScoreState {
|
||||
fn from(state: OsuScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: state.max_combo,
|
||||
slider_tick_hits: state.slider_tick_hits,
|
||||
slider_end_hits: state.slider_end_hits,
|
||||
n_geki: 0,
|
||||
n_katu: 0,
|
||||
n300: state.n300,
|
||||
@@ -129,6 +143,8 @@ impl From<TaikoScoreState> for ScoreState {
|
||||
fn from(state: TaikoScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: state.max_combo,
|
||||
slider_tick_hits: 0,
|
||||
slider_end_hits: 0,
|
||||
n_geki: 0,
|
||||
n_katu: 0,
|
||||
n300: state.n300,
|
||||
@@ -143,6 +159,8 @@ impl From<CatchScoreState> for ScoreState {
|
||||
fn from(state: CatchScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: state.max_combo,
|
||||
slider_tick_hits: 0,
|
||||
slider_end_hits: 0,
|
||||
n_geki: 0,
|
||||
n_katu: state.tiny_droplet_misses,
|
||||
n300: state.fruits,
|
||||
@@ -157,6 +175,8 @@ impl From<ManiaScoreState> for ScoreState {
|
||||
fn from(state: ManiaScoreState) -> Self {
|
||||
Self {
|
||||
max_combo: 0,
|
||||
slider_tick_hits: 0,
|
||||
slider_end_hits: 0,
|
||||
n_geki: state.n320,
|
||||
n_katu: state.n200,
|
||||
n300: state.n300,
|
||||
|
||||
@@ -474,6 +474,8 @@ impl<'map> TryFrom<OsuPerformance<'map>> for CatchPerformance<'map> {
|
||||
difficulty,
|
||||
acc,
|
||||
combo,
|
||||
slider_tick_hits: _,
|
||||
slider_end_hits: _,
|
||||
n300,
|
||||
n100,
|
||||
n50,
|
||||
|
||||
@@ -827,6 +827,8 @@ impl<'map> TryFrom<OsuPerformance<'map>> for ManiaPerformance<'map> {
|
||||
difficulty,
|
||||
acc,
|
||||
combo: _,
|
||||
slider_tick_hits: _,
|
||||
slider_end_hits: _,
|
||||
n300,
|
||||
n100,
|
||||
n50,
|
||||
|
||||
@@ -27,6 +27,8 @@ pub struct OsuDifficultyAttributes {
|
||||
pub n_circles: u32,
|
||||
/// The amount of sliders.
|
||||
pub n_sliders: u32,
|
||||
/// The amount of slider ticks and repeat points.
|
||||
pub n_slider_ticks: u32,
|
||||
/// The amount of spinners.
|
||||
pub n_spinners: u32,
|
||||
/// The final star rating
|
||||
|
||||
@@ -56,6 +56,17 @@ pub fn convert_objects(
|
||||
OsuObjectKind::Slider(ref slider) => {
|
||||
attrs.n_sliders += 1;
|
||||
attrs.max_combo += slider.nested_objects.len() as u32;
|
||||
|
||||
attrs.n_slider_ticks += slider
|
||||
.nested_objects
|
||||
.iter()
|
||||
.filter(|nested| {
|
||||
matches!(
|
||||
nested.kind,
|
||||
NestedSliderObjectKind::Tick | NestedSliderObjectKind::Repeat
|
||||
)
|
||||
})
|
||||
.count() as u32;
|
||||
}
|
||||
OsuObjectKind::Spinner(_) => attrs.n_spinners += 1,
|
||||
}
|
||||
|
||||
+149
-18
@@ -28,6 +28,8 @@ 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) slider_end_hits: Option<u32>,
|
||||
pub(crate) n300: Option<u32>,
|
||||
pub(crate) n100: Option<u32>,
|
||||
pub(crate) n50: Option<u32>,
|
||||
@@ -166,6 +168,24 @@ impl<'map> OsuPerformance<'map> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify the amount of hit slider ticks.
|
||||
///
|
||||
/// 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);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify the amount of hit slider ends.
|
||||
///
|
||||
/// Only relevant for osu!lazer.
|
||||
pub const fn n_slider_ends(mut self, n_slider_ends: u32) -> Self {
|
||||
self.slider_end_hits = Some(n_slider_ends);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify the amount of 300s of a play.
|
||||
pub const fn n300(mut self, n300: u32) -> Self {
|
||||
self.n300 = Some(n300);
|
||||
@@ -293,6 +313,8 @@ impl<'map> OsuPerformance<'map> {
|
||||
pub const fn state(mut self, state: OsuScoreState) -> Self {
|
||||
let OsuScoreState {
|
||||
max_combo,
|
||||
slider_tick_hits,
|
||||
slider_end_hits,
|
||||
n300,
|
||||
n100,
|
||||
n50,
|
||||
@@ -300,6 +322,8 @@ impl<'map> OsuPerformance<'map> {
|
||||
} = state;
|
||||
|
||||
self.combo = Some(max_combo);
|
||||
self.slider_tick_hits = Some(slider_tick_hits);
|
||||
self.slider_end_hits = Some(slider_end_hits);
|
||||
self.n300 = Some(n300);
|
||||
self.n100 = Some(n100);
|
||||
self.n50 = Some(n50);
|
||||
@@ -342,8 +366,29 @@ 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 lazer = self.lazer.unwrap_or(true);
|
||||
|
||||
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));
|
||||
|
||||
(
|
||||
n_slider_ends,
|
||||
n_slider_ticks,
|
||||
attrs.n_sliders,
|
||||
attrs.n_slider_ticks,
|
||||
)
|
||||
} else {
|
||||
(0, 0, 0, 0)
|
||||
};
|
||||
|
||||
if let Some(acc) = self.acc {
|
||||
let target_total = acc * f64::from(6 * n_objects);
|
||||
let target_total =
|
||||
acc * f64::from(30 * n_objects + 15 * max_slider_ends + 3 * max_slider_ticks);
|
||||
|
||||
match (self.n300, self.n100, self.n50) {
|
||||
(Some(_), Some(_), Some(_)) => {
|
||||
@@ -363,13 +408,28 @@ impl<'map> OsuPerformance<'map> {
|
||||
n300 = cmp::min(n300, n_remaining);
|
||||
let n_remaining = n_remaining - n300;
|
||||
|
||||
let raw_n100 = target_total - f64::from(n_remaining + 6 * n300);
|
||||
let raw_n100 = (target_total
|
||||
- f64::from(
|
||||
5 * n_remaining + 30 * n300 + 15 * n_slider_ends + 3 * n_slider_ticks,
|
||||
))
|
||||
/ 5.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);
|
||||
|
||||
for new100 in min_n100..=max_n100 {
|
||||
let new50 = n_remaining - new100;
|
||||
let dist = (acc - accuracy(n300, new100, new50, misses)).abs();
|
||||
let dist = (acc
|
||||
- accuracy(
|
||||
n_slider_ticks,
|
||||
n_slider_ends,
|
||||
n300,
|
||||
new100,
|
||||
new50,
|
||||
misses,
|
||||
max_slider_ticks,
|
||||
max_slider_ends,
|
||||
))
|
||||
.abs();
|
||||
|
||||
if dist < best_dist {
|
||||
best_dist = dist;
|
||||
@@ -384,13 +444,28 @@ impl<'map> OsuPerformance<'map> {
|
||||
n100 = cmp::min(n100, n_remaining);
|
||||
let n_remaining = n_remaining - n100;
|
||||
|
||||
let raw_n300 = (target_total - f64::from(n_remaining + 2 * n100)) / 5.0;
|
||||
let raw_n300 = (target_total
|
||||
- f64::from(
|
||||
5 * n_remaining + 10 * n100 + 15 * n_slider_ends + 3 * n_slider_ticks,
|
||||
))
|
||||
/ 25.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 new50 = n_remaining - new300;
|
||||
let curr_dist = (acc - accuracy(new300, n100, new50, misses)).abs();
|
||||
let curr_dist = (acc
|
||||
- accuracy(
|
||||
n_slider_ticks,
|
||||
n_slider_ends,
|
||||
new300,
|
||||
n100,
|
||||
new50,
|
||||
misses,
|
||||
max_slider_ticks,
|
||||
max_slider_ends,
|
||||
))
|
||||
.abs();
|
||||
|
||||
if curr_dist < best_dist {
|
||||
best_dist = curr_dist;
|
||||
@@ -405,16 +480,27 @@ impl<'map> OsuPerformance<'map> {
|
||||
n50 = cmp::min(n50, n_remaining);
|
||||
let n_remaining = n_remaining - n50;
|
||||
|
||||
let raw_n300 = (target_total + f64::from(2 * misses + n50)
|
||||
- f64::from(2 * n_objects))
|
||||
/ 4.0;
|
||||
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 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 new100 = n_remaining - new300;
|
||||
let curr_dist = (acc - accuracy(new300, new100, n50, misses)).abs();
|
||||
let curr_dist = (acc
|
||||
- accuracy(
|
||||
n_slider_ticks,
|
||||
n_slider_ends,
|
||||
new300,
|
||||
new100,
|
||||
n50,
|
||||
misses,
|
||||
max_slider_ticks,
|
||||
max_slider_ends,
|
||||
))
|
||||
.abs();
|
||||
|
||||
if curr_dist < best_dist {
|
||||
best_dist = curr_dist;
|
||||
@@ -426,18 +512,38 @@ impl<'map> OsuPerformance<'map> {
|
||||
(None, None, None) => {
|
||||
let mut best_dist = f64::MAX;
|
||||
|
||||
let raw_n300 = (target_total - f64::from(n_remaining)) / 5.0;
|
||||
let raw_n300 = (target_total
|
||||
- f64::from(5 * n_remaining + 15 * n_slider_ends + 3 * n_slider_ticks))
|
||||
/ 25.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(n_remaining + 5 * new300);
|
||||
let raw_n100 = (target_total
|
||||
- f64::from(
|
||||
5 * n_remaining
|
||||
+ 25 * new300
|
||||
+ 15 * n_slider_ends
|
||||
+ 3 * n_slider_ticks,
|
||||
))
|
||||
/ 5.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);
|
||||
|
||||
for new100 in min_n100..=max_n100 {
|
||||
let new50 = n_remaining - new300 - new100;
|
||||
let curr_dist = (acc - accuracy(new300, new100, new50, misses)).abs();
|
||||
let curr_dist = (acc
|
||||
- accuracy(
|
||||
n_slider_ticks,
|
||||
n_slider_ends,
|
||||
new300,
|
||||
new100,
|
||||
new50,
|
||||
misses,
|
||||
max_slider_ticks,
|
||||
max_slider_ends,
|
||||
))
|
||||
.abs();
|
||||
|
||||
if curr_dist < best_dist {
|
||||
best_dist = curr_dist;
|
||||
@@ -492,6 +598,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.n300 = Some(n300);
|
||||
self.n100 = Some(n100);
|
||||
self.n50 = Some(n50);
|
||||
@@ -499,6 +607,8 @@ impl<'map> OsuPerformance<'map> {
|
||||
|
||||
OsuScoreState {
|
||||
max_combo,
|
||||
slider_tick_hits: n_slider_ticks,
|
||||
slider_end_hits: n_slider_ends,
|
||||
n300,
|
||||
n100,
|
||||
n50,
|
||||
@@ -517,13 +627,21 @@ impl<'map> OsuPerformance<'map> {
|
||||
|
||||
let effective_miss_count = calculate_effective_misses(&attrs, &state);
|
||||
|
||||
let lazer = self.lazer.unwrap_or(true);
|
||||
|
||||
let (n_slider_ends, n_slider_ticks) = if lazer {
|
||||
(attrs.n_sliders, attrs.n_slider_ticks)
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
|
||||
let inner = OsuPerformanceInner {
|
||||
attrs,
|
||||
mods: self.difficulty.get_mods(),
|
||||
acc: state.accuracy(),
|
||||
acc: state.accuracy(n_slider_ticks, n_slider_ends),
|
||||
state,
|
||||
effective_miss_count,
|
||||
lazer: self.lazer.unwrap_or(true),
|
||||
lazer,
|
||||
};
|
||||
|
||||
inner.calculate()
|
||||
@@ -535,6 +653,8 @@ impl<'map> OsuPerformance<'map> {
|
||||
difficulty: Difficulty::new(),
|
||||
acc: None,
|
||||
combo: None,
|
||||
slider_tick_hits: None,
|
||||
slider_end_hits: None,
|
||||
n300: None,
|
||||
n100: None,
|
||||
n50: None,
|
||||
@@ -900,13 +1020,24 @@ fn calculate_effective_misses(attrs: &OsuDifficultyAttributes, state: &OsuScoreS
|
||||
combo_based_miss_count.max(f64::from(state.misses))
|
||||
}
|
||||
|
||||
fn accuracy(n300: u32, n100: u32, n50: u32, misses: u32) -> f64 {
|
||||
if n300 + n100 + n50 + misses == 0 {
|
||||
fn accuracy(
|
||||
n_slider_ticks: u32,
|
||||
n_slider_ends: u32,
|
||||
n300: u32,
|
||||
n100: u32,
|
||||
n50: u32,
|
||||
misses: u32,
|
||||
max_slider_ticks: u32,
|
||||
max_slider_ends: u32,
|
||||
) -> f64 {
|
||||
if n_slider_ticks + n_slider_ends + n300 + n100 + n50 + misses == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let numerator = 6 * n300 + 2 * n100 + n50;
|
||||
let denominator = 6 * (n300 + n100 + n50 + misses);
|
||||
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)
|
||||
}
|
||||
|
||||
+38
-8
@@ -1,9 +1,17 @@
|
||||
/// Aggregation for a score's current state.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct OsuScoreState {
|
||||
/// Maximum combo that the score has had so far.
|
||||
/// **Not** the maximum possible combo of the map so far.
|
||||
/// 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.
|
||||
///
|
||||
/// Only relevant for osu!lazer.
|
||||
pub slider_tick_hits: u32,
|
||||
/// Amount of successfully hit slider ends.
|
||||
///
|
||||
/// Only relevant for osu!lazer.
|
||||
pub slider_end_hits: u32,
|
||||
/// Amount of current 300s.
|
||||
pub n300: u32,
|
||||
/// Amount of current 100s.
|
||||
@@ -19,6 +27,8 @@ impl OsuScoreState {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
max_combo: 0,
|
||||
slider_tick_hits: 0,
|
||||
slider_end_hits: 0,
|
||||
n300: 0,
|
||||
n100: 0,
|
||||
n50: 0,
|
||||
@@ -32,15 +42,35 @@ impl OsuScoreState {
|
||||
}
|
||||
|
||||
/// Calculate the accuracy between `0.0` and `1.0` for this state.
|
||||
pub fn accuracy(&self) -> f64 {
|
||||
let total_hits = self.total_hits();
|
||||
|
||||
if total_hits == 0 {
|
||||
///
|
||||
/// `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;
|
||||
}
|
||||
|
||||
let numerator = 6 * self.n300 + 2 * self.n100 + self.n50;
|
||||
let denominator = 6 * total_hits;
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -364,6 +364,8 @@ impl<'map> TryFrom<OsuPerformance<'map>> for TaikoPerformance<'map> {
|
||||
difficulty,
|
||||
acc,
|
||||
combo,
|
||||
slider_tick_hits: _,
|
||||
slider_end_hits: _,
|
||||
n300,
|
||||
n100,
|
||||
n50: _,
|
||||
|
||||
Reference in New Issue
Block a user