Skip to content

Commit

Permalink
Merge pull request #43 from MaxOhn/pp-update
Browse files Browse the repository at this point in the history
feat: port pp update
  • Loading branch information
MaxOhn authored Nov 18, 2024
2 parents daf718d + 1668bea commit 9f45d95
Show file tree
Hide file tree
Showing 67 changed files with 2,695 additions and 1,079 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ sync = []
tracing = ["rosu-map/tracing"]

[dependencies]
rosu-map = { version = "0.1.1" }
rosu-mods = { version = "0.1.0" }
rosu-map = { version = "0.2.0" }
rosu-mods = { version = "0.2.0" }

[dev-dependencies]
proptest = "1.4.0"
Expand Down
12 changes: 4 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,10 @@ with emphasis on a precise translation to Rust for the most [accurate results](#
while also providing a significant [boost in performance](#speed).

Last commits of the ported code:
- [osu!lazer] : `7342fb7f51b34533a42bffda89c3d6c569cc69ce` (2022-10-11)
- [osu!tools] : `146d5916937161ef65906aa97f85d367035f3712` (2022-10-08)

News posts of the latest gamemode updates:
- osu: <https://osu.ppy.sh/home/news/2022-09-30-changes-to-osu-sr-and-pp>
- taiko: <https://osu.ppy.sh/home/news/2022-09-28-changes-to-osu-taiko-sr-and-pp>
- catch: <https://osu.ppy.sh/home/news/2020-05-14-osucatch-scoring-updates>
- mania: <https://osu.ppy.sh/home/news/2022-10-09-changes-to-osu-mania-sr-and-pp>
- [osu!lazer] : `8bd65d9938a10fc42e6409501b0282f0fa4a25ef` (2024-11-08)
- [osu!tools] : `89b8f3b1c2e4e5674004eac4723120e7d3aef997` (2024-11-03)

News posts of the latest updates: <https://osu.ppy.sh/home/news/2024-10-28-performance-points-star-rating-updates>

### Usage

Expand Down
3 changes: 3 additions & 0 deletions proptest-regressions/osu/performance/mod.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ cc e5a861f6c665dd09e46423e71d7596edf98897d4130d3144aa6f5be580f31a8b # shrinks to
cc 2cd5c105bcca0b4255afccc15bee3894b06bd20ac3f5c5d3b785f7e0ef99df46 # shrinks to acc = 0.0, combo = None, n300 = Some(0), n100 = None, n50 = Some(479), n_misses = Some(123), best_case = false
cc 2cba8a76243aac7233e9207a3162aaa1f08f933c0cb3a2ac79580ece3a7329fc # shrinks to acc = 0.0, n300 = Some(0), n100 = Some(0), n50 = Some(0), n_misses = None, best_case = false
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
10 changes: 10 additions & 0 deletions src/any/difficulty/inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ pub struct InspectDifficulty {
///
/// Only relevant for osu!catch.
pub hardrock_offsets: Option<bool>,
/// Whether the calculated attributes belong to an osu!lazer or osu!stable
/// score.
///
/// Defaults to `true`.
pub lazer: Option<bool>,
}

impl InspectDifficulty {
Expand All @@ -41,6 +46,7 @@ impl InspectDifficulty {
hp,
od,
hardrock_offsets,
lazer,
} = self;

let mut difficulty = Difficulty::new().mods(mods);
Expand Down Expand Up @@ -73,6 +79,10 @@ impl InspectDifficulty {
difficulty = difficulty.hardrock_offsets(hardrock_offsets);
}

if let Some(lazer) = lazer {
difficulty = difficulty.lazer(lazer);
}

difficulty
}
}
Expand Down
47 changes: 31 additions & 16 deletions src/any/difficulty/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::{
borrow::Cow,
fmt::{Debug, Formatter, Result as FmtResult},
num::NonZeroU32,
num::NonZeroU64,
};

use rosu_map::section::general::GameMode;
Expand Down Expand Up @@ -51,17 +51,16 @@ pub struct Difficulty {
/// Clock rate will be clamped internally between 0.01 and 100.0.
///
/// Since its minimum value is 0.01, its bits are never zero.
/// Additionally, values between 0.01 and 100 are represented sufficiently
/// precise with 32 bits.
///
/// This allows for an optimization to reduce the struct size by storing its
/// bits as a [`NonZeroU32`].
clock_rate: Option<NonZeroU32>,
/// bits as a [`NonZeroU64`].
clock_rate: Option<NonZeroU64>,
ar: Option<ModsDependent>,
cs: Option<ModsDependent>,
hp: Option<ModsDependent>,
od: Option<ModsDependent>,
hardrock_offsets: Option<bool>,
lazer: Option<bool>,
}

/// Wrapper for beatmap attributes in [`Difficulty`].
Expand Down Expand Up @@ -97,6 +96,7 @@ impl Difficulty {
hp: None,
od: None,
hardrock_offsets: None,
lazer: None,
}
}

Expand All @@ -120,17 +120,19 @@ impl Difficulty {
hp,
od,
hardrock_offsets,
lazer,
} = self;

InspectDifficulty {
mods,
passed_objects,
clock_rate: clock_rate.map(non_zero_u32_to_f32).map(f64::from),
clock_rate: clock_rate.map(non_zero_u64_to_f64),
ar,
cs,
hp,
od,
hardrock_offsets,
lazer,
}
}

Expand Down Expand Up @@ -167,11 +169,11 @@ impl Difficulty {
/// | :-----: | :-----: |
/// | 0.01 | 100 |
pub fn clock_rate(self, clock_rate: f64) -> Self {
let clock_rate = (clock_rate as f32).clamp(0.01, 100.0).to_bits();
let clock_rate = clock_rate.clamp(0.01, 100.0).to_bits();

// SAFETY: The minimum value is 0.01 so its bits can never be fully
// zero.
let non_zero = unsafe { NonZeroU32::new_unchecked(clock_rate) };
let non_zero = unsafe { NonZeroU64::new_unchecked(clock_rate) };

Self {
clock_rate: Some(non_zero),
Expand Down Expand Up @@ -268,6 +270,16 @@ impl Difficulty {
self
}

/// Whether the calculated attributes belong to an osu!lazer or osu!stable
/// score.
///
/// Defaults to `true`.
pub const fn lazer(mut self, lazer: bool) -> Self {
self.lazer = Some(lazer);

self
}

/// Perform the difficulty calculation.
pub fn calculate(&self, map: &Beatmap) -> DifficultyAttributes {
let map = Cow::Borrowed(map);
Expand Down Expand Up @@ -316,11 +328,8 @@ impl Difficulty {
}

pub(crate) fn get_clock_rate(&self) -> f64 {
let clock_rate = self
.clock_rate
.map_or(self.mods.clock_rate(), non_zero_u32_to_f32);

f64::from(clock_rate)
self.clock_rate
.map_or(self.mods.clock_rate(), non_zero_u64_to_f64)
}

pub(crate) fn get_passed_objects(&self) -> usize {
Expand All @@ -347,10 +356,14 @@ impl Difficulty {
self.hardrock_offsets
.unwrap_or_else(|| self.mods.hardrock_offsets())
}

pub(crate) fn get_lazer(&self) -> bool {
self.lazer.unwrap_or(true)
}
}

fn non_zero_u32_to_f32(n: NonZeroU32) -> f32 {
f32::from_bits(n.get())
fn non_zero_u64_to_f64(n: NonZeroU64) -> f64 {
f64::from_bits(n.get())
}

impl Debug for Difficulty {
Expand All @@ -364,17 +377,19 @@ impl Debug for Difficulty {
hp,
od,
hardrock_offsets,
lazer,
} = self;

f.debug_struct("Difficulty")
.field("mods", mods)
.field("passed_objects", passed_objects)
.field("clock_rate", &clock_rate.map(non_zero_u32_to_f32))
.field("clock_rate", &clock_rate.map(non_zero_u64_to_f64))
.field("ar", ar)
.field("cs", cs)
.field("hp", hp)
.field("od", od)
.field("hardrock_offsets", hardrock_offsets)
.field("lazer", lazer)
.finish()
}
}
Expand Down
53 changes: 51 additions & 2 deletions src/any/performance/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ impl<'map> Performance<'map> {
///
/// The argument `map_or_attrs` must be either
/// - previously calculated attributes ([`DifficultyAttributes`],
/// [`PerformanceAttributes`], or mode-specific attributes like
/// [`TaikoDifficultyAttributes`], [`ManiaPerformanceAttributes`], ...)
/// [`PerformanceAttributes`], or mode-specific attributes like
/// [`TaikoDifficultyAttributes`], [`ManiaPerformanceAttributes`], ...)
/// - a beatmap ([`Beatmap`] or [`Converted<'_, M>`])
///
/// If a map is given, difficulty attributes will need to be calculated
Expand Down Expand Up @@ -299,6 +299,55 @@ impl<'map> Performance<'map> {
}
}

/// Whether the calculated attributes belong to an osu!lazer or osu!stable
/// score.
///
/// Defaults to `true`.
///
/// This affects internal accuracy calculation because lazer considers
/// slider heads for accuracy whereas stable does not.
///
/// Only relevant for osu!standard and osu!mania.
pub fn lazer(self, lazer: bool) -> Self {
match self {
Self::Osu(o) => Self::Osu(o.lazer(lazer)),
Self::Taiko(_) | Self::Catch(_) => self,
Self::Mania(m) => Self::Mania(m.lazer(lazer)),
}
}

/// Specify the amount of "large tick" hits.
///
/// Only relevant for osu!standard.
///
/// 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.large_tick_hits(large_tick_hits))
} else {
self
}
}

/// 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))
} else {
self
}
}

/// Specify the amount of 300s of a play.
pub fn n300(self, n300: u32) -> Self {
match self {
Expand Down
25 changes: 25 additions & 0 deletions src/any/score_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ pub struct ScoreState {
///
/// Irrelevant for osu!mania.
pub max_combo: u32,
/// "Large tick" hits for osu!standard.
///
/// 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.
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
Expand All @@ -35,6 +48,8 @@ impl ScoreState {
pub const fn new() -> Self {
Self {
max_combo: 0,
osu_large_tick_hits: 0,
slider_end_hits: 0,
n_geki: 0,
n_katu: 0,
n300: 0,
Expand Down Expand Up @@ -66,6 +81,8 @@ impl From<ScoreState> for OsuScoreState {
fn from(state: ScoreState) -> Self {
Self {
max_combo: state.max_combo,
large_tick_hits: state.osu_large_tick_hits,
slider_end_hits: state.slider_end_hits,
n300: state.n300,
n100: state.n100,
n50: state.n50,
Expand Down Expand Up @@ -115,6 +132,8 @@ impl From<OsuScoreState> for ScoreState {
fn from(state: OsuScoreState) -> Self {
Self {
max_combo: state.max_combo,
osu_large_tick_hits: state.large_tick_hits,
slider_end_hits: state.slider_end_hits,
n_geki: 0,
n_katu: 0,
n300: state.n300,
Expand All @@ -129,6 +148,8 @@ impl From<TaikoScoreState> for ScoreState {
fn from(state: TaikoScoreState) -> Self {
Self {
max_combo: state.max_combo,
osu_large_tick_hits: 0,
slider_end_hits: 0,
n_geki: 0,
n_katu: 0,
n300: state.n300,
Expand All @@ -143,6 +164,8 @@ impl From<CatchScoreState> for ScoreState {
fn from(state: CatchScoreState) -> Self {
Self {
max_combo: state.max_combo,
osu_large_tick_hits: 0,
slider_end_hits: 0,
n_geki: 0,
n_katu: state.tiny_droplet_misses,
n300: state.fruits,
Expand All @@ -157,6 +180,8 @@ impl From<ManiaScoreState> for ScoreState {
fn from(state: ManiaScoreState) -> Self {
Self {
max_combo: 0,
osu_large_tick_hits: 0,
slider_end_hits: 0,
n_geki: state.n320,
n_katu: state.n200,
n300: state.n300,
Expand Down
4 changes: 3 additions & 1 deletion src/catch/catcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ impl Catcher {
}

fn calculate_scale(cs: f32) -> f32 {
1.0 - 0.7 * (cs - 5.0) / 5.0
((f64::from(1.0_f32) - f64::from(0.7_f32) * ((f64::from(cs) - 5.0) / 5.0)) as f32 / 2.0
* 1.0)
* 2.0
}
}
Loading

0 comments on commit 9f45d95

Please sign in to comment.