Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: port pp update #43

Merged
merged 48 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
3c57b9b
feat: updated osu!standard difficulate calculation
MaxOhn Sep 27, 2024
64001a3
feat: updated taiko calc
MaxOhn Sep 28, 2024
a02b8a8
refactor: cleanup taiko skill eval
MaxOhn Sep 28, 2024
c7b5414
feat: update mania calc
MaxOhn Sep 28, 2024
cc0a041
feat: update catch calc
MaxOhn Sep 29, 2024
092cf34
fix: adjust remaining catch pieces
MaxOhn Sep 30, 2024
f9dfa5e
chore: made clippy happy
MaxOhn Sep 30, 2024
5bdb2ba
Port osu!std updates since f08134f
MaxOhn Oct 10, 2024
b33e259
Port osu!taiko updates since f08134f
MaxOhn Oct 10, 2024
ec52aaa
test: update test case values
MaxOhn Oct 11, 2024
5f634d8
fix: adjust mania converts
MaxOhn Oct 11, 2024
315d4b3
fix: ensure f32 usage
MaxOhn Oct 11, 2024
14c646d
fix: refactor `get_precision_adjusted_beat_len`
MaxOhn Oct 11, 2024
b641f60
fix: dont sort unstably for catch
MaxOhn Oct 11, 2024
b683200
fix: more catch fixes
MaxOhn Oct 11, 2024
915d07e
test: fix test values
MaxOhn Oct 11, 2024
25d8d29
chore: made clippy happy
MaxOhn Oct 11, 2024
f646cba
fix: negate classic mod check
MaxOhn Oct 13, 2024
f72ff79
feat!: adjust osu!standard hitresult generation
MaxOhn Oct 13, 2024
2c77ddf
refactor!: store `lazer` in `Difficulty`
MaxOhn Oct 18, 2024
391eb55
chore: made clippy happy
MaxOhn Oct 18, 2024
1ee664a
test: remove target_os check
MaxOhn Oct 18, 2024
ad26097
feat: avoid using missed slider estimates
MaxOhn Oct 30, 2024
4053623
test: split target_os tests again for std
MaxOhn Oct 30, 2024
19609cb
feat: add slider hitresult methods to Performance
MaxOhn Oct 31, 2024
98ca341
fix: cap effective miss count
MaxOhn Nov 1, 2024
674cbec
chore: satisfy clippy
MaxOhn Nov 1, 2024
c504511
fix: properly divide by combo
MaxOhn Nov 3, 2024
45cd1ed
refactor: start effective miss count with miss count
MaxOhn Nov 7, 2024
9e4db40
feat: include latest taiko changes
MaxOhn Nov 8, 2024
0448980
fix: clamp effective x
MaxOhn Nov 8, 2024
9edad8a
dep: use f64 for clock rate
MaxOhn Nov 9, 2024
757b57b
feat: support horizontal flipping
MaxOhn Nov 11, 2024
4f1c63e
chore: made clippy happy
MaxOhn Nov 11, 2024
2743ea3
fix: rx perf calc
MaxOhn Nov 11, 2024
636209e
dep: replace local with upstream rosu-mods
MaxOhn Nov 11, 2024
4326ec4
test: split target_os for taiko convert test
MaxOhn Nov 11, 2024
78d7bd3
fix: handle acc pp calc for niche case
MaxOhn Nov 11, 2024
e0c53af
fix: adjust hitresult gen for mania on lazer
MaxOhn Nov 11, 2024
d6d1916
fix: dont overwrite mods & clock_rate
MaxOhn Nov 12, 2024
11095ad
fix: clamp map attributes
MaxOhn Nov 12, 2024
baeaeb7
fix: mirror reflection handling
MaxOhn Nov 13, 2024
f37b917
feat: support MR for catch
MaxOhn Nov 13, 2024
93f3756
chore: made clippy happy
MaxOhn Nov 13, 2024
30054fe
feat!: distinguish stable|lazer|lazer+classic
MaxOhn Nov 14, 2024
e2bfaea
fix: handle hitsounds with filenames differently
MaxOhn Nov 15, 2024
c615e54
dep: use published dependencies
MaxOhn Nov 18, 2024
1668bea
doc: update readme
MaxOhn Nov 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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