Merge branch 'rewrite' of https://github.com/MaxOhn/rosu-pp into rewrite

This commit is contained in:
MaxOhn
2024-02-14 15:28:02 +01:00
12 changed files with 185 additions and 206 deletions
+68 -182
View File
@@ -2,210 +2,96 @@
# rosu-pp
TODO: Rewrite readme
<!-- cargo-rdme start -->
A standalone crate to calculate star ratings and performance points for all [osu!](https://osu.ppy.sh/home) gamemodes.
Library to calculate difficulty and performance attributes for all [osu!] gamemodes.
Async is supported through features, see below.
### Description
A large part of `rosu-pp` is a port of [osu!lazer]'s difficulty and performance calculation
with emphasis on a precise translation to Rust for the most accurate results.
Another important factor is the calculation speed. Optimizations and an accurate translation
unfortunately don't always go hand-in-hand. Nonetheless, performance improvements are still
snuck in wherever possible, providing a significantly faster runtime than the native C# code.
TODO: values to compare
Additionally, `rosu-pp` allows previous values to be re-used so that they don't need to be
calculated again. For example, a beatmap needs to be decoded only once and can then be used
for any amount of attribute calculations. Similarly, previous attributes can be re-used for
later calculations (with some limitations, see the [example](#usage)).
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>
### Usage
```rust
use rosu_pp::{Beatmap, BeatmapExt};
// Decode the map
let map = rosu_pp::Beatmap::from_path("./resources/2785319.osu").unwrap();
// Parse the map yourself
let map = match Beatmap::from_path("/path/to/file.osu") {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
// Calculate difficulty attributes
let diff_attrs = map.difficulty()
.mods(8 + 16) // HDHR
.calculate();
// If `BeatmapExt` is included, you can make use of
// some methods on `Beatmap` to make your life simpler.
let result = map.pp()
.mods(24) // HDHR
.combo(1234)
let stars = diff_attrs.stars();
// Calculate performance attributes
let perf_attrs = map.performance()
// To speed up the calculation, we can use the previous attributes.
// **Note** that this should only be done if the map, mode, mods,
// clock rate, and amount of passed objects stay the same.
// Otherwise, the final attributes will be incorrect.
.attributes(diff_attrs)
.mods(24) // HDHR, must be the same as before
.combo(789)
.accuracy(99.2)
.misses(2)
.calculate();
println!("PP: {}", result.pp());
let pp = perf_attrs.pp();
// If you want to reuse the current map-mod combination, make use of the previous result!
// If attributes are given, then stars & co don't have to be recalculated.
let next_result = map.pp()
.mods(24) // HDHR
.attributes(result) // recycle
.combo(543)
.misses(5)
.n50(3)
.accuracy(96.5)
.calculate();
println!("Next PP: {}", next_result.pp());
let stars = map.stars()
.mods(16) // HR
// Again, we re-use the previous attributes for maximum efficiency.
// This time we do it directly instead of through the map.
let max_pp = perf_attrs.performance()
.mods(24) // Still the same
.calculate()
.stars();
.pp();
let max_pp = map.max_pp(16).pp();
println!("Stars: {} | Max PP: {}", stars, max_pp);
```
### With async
If either the `async_tokio` or `async_std` feature is enabled, beatmap parsing will be async.
```rust
use rosu_pp::{Beatmap, BeatmapExt};
// Parse the map asynchronously
let map = match Beatmap::from_path("/path/to/file.osu").await {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
// The rest stays the same
let result = map.pp()
.mods(24) // HDHR
.combo(1234)
.n_misses(2)
.accuracy(99.2)
.calculate();
println!("PP: {}", result.pp());
println!("Stars: {stars} | PP: {pp}/{max_pp}");
```
### Gradual calculation
Sometimes you might want to calculate the difficulty of a map or performance of a score after each hit object.
This could be done by using `passed_objects` as the amount of objects that were passed so far.
However, this requires to recalculate the beginning again and again, we can be more efficient than that.
Instead, you should enable the `gradual` feature and use `GradualDifficulty` and `GradualPerformance`:
```rust
use rosu_pp::{
Beatmap, BeatmapExt, GradualDifficulty, GradualPerformance, ScoreState,
taiko::TaikoScoreState,
};
let map = match Beatmap::from_path("/path/to/file.osu") {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
let mods = 8 + 64; // HDDT
// If you're only interested in the star rating or other difficulty values,
// use `GradualDifficulty`.
let gradual_difficulty = GradualDifficulty::new(&map, mods);
// Since `GradualDifficulty` implements `Iterator`, you can use
// any iterate function on it, use it in loops, collect them into a `Vec`, ...
for (i, difficulty) in gradual_difficulty.enumerate() {
println!("Stars after object {}: {}", i, difficulty.stars());
}
// Gradually calculating performance values does the same as calculating
// difficulty attributes but it goes the extra step and also evaluates
// the state of a score for these difficulty attributes.
let mut gradual_performance = GradualPerformance::new(&map, mods);
// The default score state is kinda chunky because it considers all modes.
let state = ScoreState {
max_combo: 1,
n_geki: 0, // only relevant for mania
n_katu: 0, // only relevant for mania and ctb
n300: 1,
n100: 0,
n50: 0,
n_misses: 0,
};
// Process the score state after the first object
let curr_performance = match gradual_performance.next(state) {
Some(perf) => perf,
None => panic!("the map has no hit objects"),
};
println!("PP after the first object: {}", curr_performance.pp());
// If you're only interested in maps of a specific mode, consider
// using the mode's gradual calculator instead of the general one.
// Let's assume it's a taiko map.
// Instead of starting off with `GradualPerformance` one could have
// used `TaikoGradualPerformance`.
let mut gradual_performance = match gradual_performance {
GradualPerformanceAttributes::Taiko(gradual) => gradual,
_ => panic!("the map was not taiko but {:?}", map.mode),
};
// A little simpler than the general score state.
let state = TaikoScoreState {
max_combo: 11,
n300: 9,
n100: 1,
n_misses: 1,
};
// Process the next 10 objects in one go (`nth` takes a zero-based value).
let curr_performance = match gradual_performance.nth(state, 9) {
Some(perf) => perf,
None => panic!("the previous `next` already processed the last object"),
};
println!("PP after the first 11 objects: {}", curr_performance.pp());
```
TODO
### Features
| Flag | Description |
| ------------- |------------------------------------------------------------------------------------------|
| `default` | Beatmap parsing will be non-async |
| `async_tokio` | Beatmap parsing will be async through [tokio](https://github.com/tokio-rs/tokio) |
| `async_std` | Beatmap parsing will be async through [async-std](https://github.com/async-rs/async-std) |
| `gradual` | Enable gradual difficulty and performance calculation |
### Version
A large portion of this repository is a port of [osu!lazer](https://github.com/ppy/osu)'s difficulty and performance calculation.
- osu!:
- osu!lazer: Commit `85adfc2df7d931164181e145377a6ced8db2bfb3` (Wed Sep 28 18:26:36 2022 +0300)
- osu!tools: Commit `146d5916937161ef65906aa97f85d367035f3712` (Sat Oct 8 14:28:49 2022 +0900)
- [Article](https://osu.ppy.sh/home/news/2022-09-30-changes-to-osu-sr-and-pp)
- taiko:
- osu!lazer: Commit `234c6ac7998fbc6742503e1a589536255554e56a` (Wed Oct 5 20:21:15 2022 +0900)
- osu!tools: Commit `146d5916937161ef65906aa97f85d367035f3712` (Sat Oct 8 14:28:49 2022 +0900)
- [Article](https://osu.ppy.sh/home/news/2022-09-28-changes-to-osu-taiko-sr-and-pp)
- catch: (will be updated on the next rework)
- osu!lazer: -
- osu!tools: -
- mania:
- osu!lazer: Commit `7342fb7f51b34533a42bffda89c3d6c569cc69ce` (Tue Oct 11 14:34:50 2022 +0900)
- osu!tools: Commit `146d5916937161ef65906aa97f85d367035f3712` (Sat Oct 8 14:28:49 2022 +0900)
- [Article](https://osu.ppy.sh/home/news/2022-10-09-changes-to-osu-mania-sr-and-pp)
### Accuracy
The difficulty and performance attributes generated by [osu-tools](https://github.com/ppy/osu-tools) itself were compared with rosu-pp's results when running on `130,000` different maps. Additionally, multiple mod combinations were tested depending on the mode:
- osu!: NM, EZ, HD, HR, DT
- taiko: NM, HD, HR, DT (+ all osu! converts)
- catch: -
- mania: NM, DT (+ all osu! converts)
For every (!) comparison of the star and pp values, the error margin was below `0.000000001`, ensuing a great accuracy.
### Benchmark
To be done
| Flag | Description | Dependencies
| --------- | ----------- | ------------
| `default` | No features |
| `tracing` | Any error encountered during beatmap decoding will be logged through `tracing::error`. If this feature is not enabled, errors will be ignored. | [`tracing`]
### Bindings
Using rosu-pp from other languages than Rust:
- JavaScript: [rosu-pp-js](https://github.com/MaxOhn/rosu-pp-js)
- Python: [rosu-pp-py](https://github.com/MaxOhn/rosu-pp-py)
Using `rosu-pp` from other languages than Rust:
- JavaScript: [rosu-pp-js]
- Python: [rosu-pp-py]
[osu!]: https://osu.ppy.sh/home
[osu!lazer]: https://github.com/ppy/osu
[osu!tools]: https://github.com/ppy/osu-tools
[`tracing`]: https://docs.rs/tracing
[rosu-pp-js]: https://github.com/MaxOhn/rosu-pp-js
[rosu-pp-py]: https://github.com/MaxOhn/rosu-pp-py
<!-- cargo-rdme end -->
+1 -1
View File
@@ -43,7 +43,7 @@ impl DifficultyAttributes {
}
/// Returns a builder for performance calculation.
pub fn pp<'a>(self) -> Performance<'a> {
pub fn performance<'a>(self) -> Performance<'a> {
self.into()
}
}
+4 -4
View File
@@ -275,10 +275,10 @@ impl<A: AttributeProvider> From<A> for Performance<'_> {
fn from(attrs: A) -> Self {
fn inner(attrs: DifficultyAttributes) -> Performance<'static> {
match attrs {
DifficultyAttributes::Osu(attrs) => Performance::Osu(attrs.pp()),
DifficultyAttributes::Taiko(attrs) => Performance::Taiko(attrs.pp()),
DifficultyAttributes::Catch(attrs) => Performance::Catch(attrs.pp()),
DifficultyAttributes::Mania(attrs) => Performance::Mania(attrs.pp()),
DifficultyAttributes::Osu(attrs) => Performance::Osu(attrs.performance()),
DifficultyAttributes::Taiko(attrs) => Performance::Taiko(attrs.performance()),
DifficultyAttributes::Catch(attrs) => Performance::Catch(attrs.performance()),
DifficultyAttributes::Mania(attrs) => Performance::Mania(attrs.performance()),
}
}
+1 -1
View File
@@ -33,7 +33,7 @@ impl CatchDifficultyAttributes {
}
/// Returns a builder for performance calculation.
pub fn pp<'a>(self) -> CatchPerformance<'a> {
pub fn performance<'a>(self) -> CatchPerformance<'a> {
self.into()
}
}
+91 -1
View File
@@ -1,4 +1,94 @@
#![deny(rustdoc::broken_intra_doc_links)]
//! Library to calculate difficulty and performance attributes for all [osu!] gamemodes.
//!
//! ## Description
//!
//! A large part of `rosu-pp` is a port of [osu!lazer]'s difficulty and performance calculation
//! with emphasis on a precise translation to Rust for the most accurate results.
//!
//! Another important factor is the calculation speed. Optimizations and an accurate translation
//! unfortunately don't always go hand-in-hand. Nonetheless, performance improvements are still
//! snuck in wherever possible, providing a significantly faster runtime than the native C# code.
//!
//! TODO: values to compare
//!
//! Additionally, `rosu-pp` allows previous values to be re-used so that they don't need to be
//! calculated again. For example, a beatmap needs to be decoded only once and can then be used
//! for any amount of attribute calculations. Similarly, previous attributes can be re-used for
//! later calculations (with some limitations, see the [example](#usage)).
//!
//! 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>
//!
//! ## Usage
//!
//! ```
//! // Decode the map
//! let map = rosu_pp::Beatmap::from_path("./resources/2785319.osu").unwrap();
//!
//! // Calculate difficulty attributes
//! let diff_attrs = map.difficulty()
//! .mods(8 + 16) // HDHR
//! .calculate();
//!
//! let stars = diff_attrs.stars();
//!
//! // Calculate performance attributes
//! let perf_attrs = map.performance()
//! // To speed up the calculation, we can use the previous attributes.
//! // **Note** that this should only be done if the map, mode, mods,
//! // clock rate, and amount of passed objects stay the same.
//! // Otherwise, the final attributes will be incorrect.
//! .attributes(diff_attrs)
//! .mods(24) // HDHR, must be the same as before
//! .combo(789)
//! .accuracy(99.2)
//! .misses(2)
//! .calculate();
//!
//! let pp = perf_attrs.pp();
//!
//! // Again, we re-use the previous attributes for maximum efficiency.
//! // This time we do it directly instead of through the map.
//! let max_pp = perf_attrs.performance()
//! .mods(24) // Still the same
//! .calculate()
//! .pp();
//!
//! println!("Stars: {stars} | PP: {pp}/{max_pp}");
//! ```
//!
//! ## Gradual calculation
//!
//! TODO
//!
//! ## Features
//!
//! | Flag | Description | Dependencies
//! | --------- | ----------- | ------------
//! | `default` | No features |
//! | `tracing` | Any error encountered during beatmap decoding will be logged through `tracing::error`. If this feature is not enabled, errors will be ignored. | [`tracing`]
//!
//! ## Bindings
//!
//! Using `rosu-pp` from other languages than Rust:
//! - JavaScript: [rosu-pp-js]
//! - Python: [rosu-pp-py]
//!
//! [osu!]: https://osu.ppy.sh/home
//! [osu!lazer]: https://github.com/ppy/osu
//! [osu!tools]: https://github.com/ppy/osu-tools
//! [`tracing`]: https://docs.rs/tracing
//! [rosu-pp-js]: https://github.com/MaxOhn/rosu-pp-js
//! [rosu-pp-py]: https://github.com/MaxOhn/rosu-pp-py
#![deny(rustdoc::broken_intra_doc_links, rustdoc::missing_crate_level_docs)]
#![warn(clippy::missing_const_for_fn, clippy::pedantic)]
#![allow(
clippy::missing_errors_doc,
+1 -1
View File
@@ -36,7 +36,7 @@ impl ManiaDifficultyAttributes {
}
/// Returns a builder for performance calculation.
pub fn pp<'a>(self) -> ManiaPerformance<'a> {
pub fn performance<'a>(self) -> ManiaPerformance<'a> {
self.into()
}
}
+2 -9
View File
@@ -1,10 +1,7 @@
use std::borrow::Cow;
use rosu_map::{
section::{
general::GameMode,
hit_objects::{BorrowedCurve, CurveBuffers},
},
section::{general::GameMode, hit_objects::CurveBuffers},
util::Pos,
};
@@ -107,11 +104,7 @@ fn convert(map: &mut Beatmap) {
last_values.pattern = new_pattern;
}
HitObjectKind::Slider(ref slider) => {
let curve = BorrowedCurve::new(
&slider.control_points,
slider.expected_dist,
&mut curve_bufs,
);
let curve = slider.curve(&mut curve_bufs);
let mut gen = DistanceObjectPatternGenerator::new(
&mut random,
+12
View File
@@ -5,6 +5,8 @@ use rosu_map::{
LATEST_FORMAT_VERSION,
};
use crate::{Performance, Difficulty};
pub use self::{
attributes::{BeatmapAttributes, BeatmapAttributesBuilder, HitWindows},
converted::Converted,
@@ -68,6 +70,16 @@ impl Beatmap {
rosu_map::from_bytes(bytes)
}
/// Create a new difficulty calculator for this [`Beatmap`].
pub fn difficulty(&self) -> Difficulty<'_> {
Difficulty::new(self)
}
/// Create a new performance calculator for this [`Beatmap`].
pub fn performance(&self) -> Performance<'_> {
Performance::new(self)
}
/// Finds the [`TimingPoint`] that is active at the given time.
pub(crate) fn timing_point_at(&self, time: f64) -> Option<&TimingPoint> {
timing_point_at(&self.timing_points, time)
+1 -1
View File
@@ -3,7 +3,7 @@ use std::cmp::Ordering;
use rosu_map::section::hit_objects::{BorrowedCurve, CurveBuffers};
pub use rosu_map::{
section::hit_objects::{hit_samples::HitSoundType, PathControlPoint},
section::hit_objects::{hit_samples::HitSoundType, PathControlPoint, PathType, SplineType},
util::Pos,
};
+1 -1
View File
@@ -43,7 +43,7 @@ impl OsuDifficultyAttributes {
}
/// Returns a builder for performance calculation.
pub fn pp<'a>(self) -> OsuPerformance<'a> {
pub fn performance<'a>(self) -> OsuPerformance<'a> {
self.into()
}
}
+2 -4
View File
@@ -1,7 +1,5 @@
use rosu_map::{
section::hit_objects::{
BorrowedCurve, CurveBuffers, SliderEvent, SliderEventType, SliderEventsIter,
},
section::hit_objects::{CurveBuffers, SliderEvent, SliderEventType, SliderEventsIter},
util::Pos,
};
@@ -183,7 +181,7 @@ impl OsuSlider {
|point| (point.slider_velocity, point.generate_ticks),
);
let path = BorrowedCurve::new(&slider.control_points, slider.expected_dist, curve_bufs);
let path = slider.curve(curve_bufs);
let span_count = slider.span_count() as f64;
+1 -1
View File
@@ -37,7 +37,7 @@ impl TaikoDifficultyAttributes {
}
/// Returns a builder for performance calculation.
pub fn pp<'a>(self) -> TaikoPerformance<'a> {
pub fn performance<'a>(self) -> TaikoPerformance<'a> {
self.into()
}
}