feature-gated modes and osu!standard versions
This commit is contained in:
+12
@@ -3,3 +3,15 @@ name = "rosu-pp"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["MaxOhn <ohn.m@hotmail.de>"]
|
authors = ["MaxOhn <ohn.m@hotmail.de>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["osu", "taiko", "fruits", "mania", "no_leniency"]
|
||||||
|
|
||||||
|
osu = []
|
||||||
|
taiko = []
|
||||||
|
fruits = []
|
||||||
|
mania = []
|
||||||
|
|
||||||
|
all_included = []
|
||||||
|
no_leniency = []
|
||||||
|
no_sliders_no_leniency = []
|
||||||
@@ -4,7 +4,7 @@ A standalone crate to calculate star ratings and performance points for all [osu
|
|||||||
|
|
||||||
Conversions are generally not supported.
|
Conversions are generally not supported.
|
||||||
|
|
||||||
#### Usage
|
### Usage
|
||||||
```rust
|
```rust
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use rosu_pp::{Beatmap, BeatmapExt, GameMode, OsuPP, TaikoPP};
|
use rosu_pp::{Beatmap, BeatmapExt, GameMode, OsuPP, TaikoPP};
|
||||||
@@ -31,8 +31,7 @@ match map.mode {
|
|||||||
.combo(1234)
|
.combo(1234)
|
||||||
.misses(2)
|
.misses(2)
|
||||||
.accuracy(99.2)
|
.accuracy(99.2)
|
||||||
// `no_leniency::stars` is the suggested default
|
.calculate();
|
||||||
.calculate(rosu_pp::osu::no_leniency::stars);
|
|
||||||
|
|
||||||
println!("PP: {}", result.pp());
|
println!("PP: {}", result.pp());
|
||||||
|
|
||||||
@@ -46,7 +45,7 @@ match map.mode {
|
|||||||
.misses(5)
|
.misses(5)
|
||||||
.n50(3)
|
.n50(3)
|
||||||
.accuracy(97.5)
|
.accuracy(97.5)
|
||||||
.calculate(rosu_pp::osu::no_leniency::stars);
|
.calculate();
|
||||||
|
|
||||||
println!("Next PP: {}", next_result.pp());
|
println!("Next PP: {}", next_result.pp());
|
||||||
},
|
},
|
||||||
@@ -66,22 +65,35 @@ match map.mode {
|
|||||||
|
|
||||||
// If all you want is the map's stars or max pp,
|
// If all you want is the map's stars or max pp,
|
||||||
// you can make use of the BeatmapExt trait.
|
// you can make use of the BeatmapExt trait.
|
||||||
let stars = map.stars(16, None); // HR
|
let stars = map.stars(16, None).stars(); // HR
|
||||||
let max_pp = map.max_pp(16);
|
let max_pp = map.max_pp(16).pp();
|
||||||
|
|
||||||
println!("Stars: {} | Max PP: {}", stars, max_pp);
|
println!("Stars: {} | Max PP: {}", stars, max_pp);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### osu!standard versions
|
### osu!standard versions
|
||||||
- `all_included`: WIP
|
- `all_included`: WIP
|
||||||
- `no_leniency`: The positional offset of notes created by stack leniency is not considered. This means the jump distance inbetween notes might be slightly off, resulting in small inaccuracies. Since calculating these offsets is relatively expensive though, this version is considerably faster than `all_included`.
|
- `no_leniency`: The positional offset of notes created by stack leniency is not considered. This means the jump distance inbetween notes might be slightly off, resulting in small inaccuracies. Since calculating these offsets is relatively expensive though, this version is considerably faster than `all_included`.
|
||||||
- `no_slider_no_leniency` (i.e. oppai): In addtion to not considering the positional offset caused by stack leniency, slider paths are also ignored. This means the travel distance of notes is completely omitted which may cause further inaccuracies. Since the slider paths don't have to be computed though, it should generally be faster than `no_leniency`.
|
- `no_slider_no_leniency` (i.e. [oppai](https://github.com/Francesco149/oppai-ng)): In addtion to not considering the positional offset caused by stack leniency, slider paths are also ignored. This means the travel distance of notes is completely omitted which may cause further inaccuracies. Since the slider paths don't have to be computed though, it should generally be faster than `no_leniency`.
|
||||||
|
|
||||||
#### Roadmap
|
### Features
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|-----|-----|
|
||||||
|
| `default` | Enable all modes and choose the `no_leniency` version for osu!standard. |
|
||||||
|
| `taiko` | Enable osu!taiko. |
|
||||||
|
| `fruits` | Enable osu!ctb. |
|
||||||
|
| `mania` | Enable osu!mania. |
|
||||||
|
| `osu` | Enable osu!standard. Requires to also enable exactly one of the features `no_leniency`, `no_sliders_no_leniency`, or `all_included`. |
|
||||||
|
| `no_leniency` | When calculating difficulty attributes in osu!standard, ignore stack leniency but consider sliders. Solid middleground between performance and precision, suggested default version. |
|
||||||
|
| `no_sliders_no_leniency` | When calculating difficulty attributes in osu!standard, ignore stack leniency and sliders. Best performance but slightly less precision than `no_leniency`. |
|
||||||
|
| `all_included` | When calculating difficulty attributes in osu!standard, consider both stack leniency and sliders. Best precision but significantly worse performance than `no_leniency`. |
|
||||||
|
|
||||||
|
### Roadmap
|
||||||
- osu sr versions
|
- osu sr versions
|
||||||
- [ ] all included
|
- [ ] all included
|
||||||
- [x] no_leniency
|
- [x] no_leniency
|
||||||
- [x] no_sliders_no_leniency (i.e. oppai)
|
- [x] no_sliders_no_leniency (i.e. [oppai](https://github.com/Francesco149/oppai-ng))
|
||||||
- [x] taiko sr
|
- [x] taiko sr
|
||||||
- [x] ctb sr
|
- [x] ctb sr
|
||||||
- [x] mania sr
|
- [x] mania sr
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
#![cfg(any(
|
||||||
|
feature = "fruits",
|
||||||
|
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||||
|
))]
|
||||||
|
|
||||||
use crate::{math_util, Pos2};
|
use crate::{math_util, Pos2};
|
||||||
|
|
||||||
const SLIDER_QUALITY: f32 = 50.0;
|
const SLIDER_QUALITY: f32 = 50.0;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![cfg(feature = "fruits")]
|
||||||
|
|
||||||
mod catch_object;
|
mod catch_object;
|
||||||
mod control_point_iter;
|
mod control_point_iter;
|
||||||
mod difficulty_object;
|
mod difficulty_object;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ impl FruitsAttributeProvider for DifficultyAttributes {
|
|||||||
|
|
||||||
impl FruitsAttributeProvider for StarResult {
|
impl FruitsAttributeProvider for StarResult {
|
||||||
fn attributes(self) -> Option<DifficultyAttributes> {
|
fn attributes(self) -> Option<DifficultyAttributes> {
|
||||||
|
#[allow(irrefutable_let_patterns)]
|
||||||
if let Self::Fruits(attributes) = self {
|
if let Self::Fruits(attributes) = self {
|
||||||
Some(attributes)
|
Some(attributes)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+207
-19
@@ -29,8 +29,7 @@
|
|||||||
//! .combo(1234)
|
//! .combo(1234)
|
||||||
//! .misses(2)
|
//! .misses(2)
|
||||||
//! .accuracy(99.2)
|
//! .accuracy(99.2)
|
||||||
//! // `no_leniency::stars` is the suggested default
|
//! .calculate();
|
||||||
//! .calculate(rosu_pp::osu::no_leniency::stars);
|
|
||||||
//!
|
//!
|
||||||
//! println!("PP: {}", result.pp());
|
//! println!("PP: {}", result.pp());
|
||||||
//!
|
//!
|
||||||
@@ -44,7 +43,7 @@
|
|||||||
//! .misses(5)
|
//! .misses(5)
|
||||||
//! .n50(3)
|
//! .n50(3)
|
||||||
//! .accuracy(97.5)
|
//! .accuracy(97.5)
|
||||||
//! .calculate(rosu_pp::osu::no_leniency::stars);
|
//! .calculate();
|
||||||
//!
|
//!
|
||||||
//! println!("Next PP: {}", next_result.pp());
|
//! println!("Next PP: {}", next_result.pp());
|
||||||
//! },
|
//! },
|
||||||
@@ -73,13 +72,26 @@
|
|||||||
//! ### osu!standard versions
|
//! ### osu!standard versions
|
||||||
//! - `all_included`: WIP
|
//! - `all_included`: WIP
|
||||||
//! - `no_leniency`: The positional offset of notes created by stack leniency is not considered. This means the jump distance inbetween notes might be slightly off, resulting in small inaccuracies. Since calculating these offsets is relatively expensive though, this version is considerably faster than `all_included`.
|
//! - `no_leniency`: The positional offset of notes created by stack leniency is not considered. This means the jump distance inbetween notes might be slightly off, resulting in small inaccuracies. Since calculating these offsets is relatively expensive though, this version is considerably faster than `all_included`.
|
||||||
//! - `no_slider_no_leniency` (i.e. oppai): In addtion to not considering the positional offset caused by stack leniency, slider paths are also ignored. This means the travel distance of notes is completely omitted which may cause further inaccuracies. Since the slider paths don't have to be computed though, it should generally be faster than `no_leniency`.
|
//! - `no_slider_no_leniency` (i.e. [oppai](https://github.com/Francesco149/oppai-ng)): In addtion to not considering the positional offset caused by stack leniency, slider paths are also ignored. This means the travel distance of notes is completely omitted which may cause further inaccuracies. Since the slider paths don't have to be computed though, it should generally be faster than `no_leniency`.
|
||||||
|
//!
|
||||||
|
//! ### Features
|
||||||
|
//!
|
||||||
|
//! | Flag | Description |
|
||||||
|
//! |-----|-----|
|
||||||
|
//! | `default` | Enable all modes and choose the `no_leniency` version for osu!standard. |
|
||||||
|
//! | `taiko` | Enable osu!taiko. |
|
||||||
|
//! | `fruits` | Enable osu!ctb. |
|
||||||
|
//! | `mania` | Enable osu!mania. |
|
||||||
|
//! | `osu` | Enable osu!standard. Requires to also enable exactly one of the features `no_leniency`, `no_sliders_no_leniency`, or `all_included`. |
|
||||||
|
//! | `no_leniency` | When calculating difficulty attributes in osu!standard, ignore stack leniency but consider sliders. Solid middleground between performance and precision, suggested default version. |
|
||||||
|
//! | `no_sliders_no_leniency` | When calculating difficulty attributes in osu!standard, ignore stack leniency and sliders. Best performance but slightly less precision than `no_leniency`. |
|
||||||
|
//! | `all_included` | When calculating difficulty attributes in osu!standard, consider both stack leniency and sliders. Best precision but significantly worse performance than `no_leniency`. |
|
||||||
//!
|
//!
|
||||||
//! ### Roadmap
|
//! ### Roadmap
|
||||||
//! - osu sr versions
|
//! - osu sr versions
|
||||||
//! - [ ] all included
|
//! - [ ] all included
|
||||||
//! - [x] no_leniency
|
//! - [x] no_leniency
|
||||||
//! - [x] no_sliders_no_leniency (i.e. oppai)
|
//! - [x] no_sliders_no_leniency (i.e. [oppai](https://github.com/Francesco149/oppai-ng))
|
||||||
//! - [x] taiko sr
|
//! - [x] taiko sr
|
||||||
//! - [x] ctb sr
|
//! - [x] ctb sr
|
||||||
//! - [x] mania sr
|
//! - [x] mania sr
|
||||||
@@ -102,9 +114,16 @@ mod curve;
|
|||||||
mod math_util;
|
mod math_util;
|
||||||
mod mods;
|
mod mods;
|
||||||
|
|
||||||
|
#[cfg(feature = "fruits")]
|
||||||
pub use fruits::FruitsPP;
|
pub use fruits::FruitsPP;
|
||||||
|
|
||||||
|
#[cfg(feature = "mania")]
|
||||||
pub use mania::ManiaPP;
|
pub use mania::ManiaPP;
|
||||||
|
|
||||||
|
#[cfg(feature = "osu")]
|
||||||
pub use osu::OsuPP;
|
pub use osu::OsuPP;
|
||||||
|
|
||||||
|
#[cfg(feature = "taiko")]
|
||||||
pub use taiko::TaikoPP;
|
pub use taiko::TaikoPP;
|
||||||
|
|
||||||
pub use mods::Mods;
|
pub use mods::Mods;
|
||||||
@@ -140,28 +159,150 @@ pub trait BeatmapExt {
|
|||||||
impl BeatmapExt for Beatmap {
|
impl BeatmapExt for Beatmap {
|
||||||
fn stars(&self, mods: impl Mods, passed_objects: Option<usize>) -> StarResult {
|
fn stars(&self, mods: impl Mods, passed_objects: Option<usize>) -> StarResult {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
GameMode::STD => osu::no_leniency::stars(self, mods, passed_objects),
|
GameMode::STD => {
|
||||||
GameMode::MNA => mania::stars(self, mods, passed_objects),
|
#[cfg(not(feature = "osu"))]
|
||||||
GameMode::TKO => taiko::stars(self, mods, passed_objects),
|
panic!("`osu` feature is not enabled");
|
||||||
GameMode::CTB => fruits::stars(self, mods, passed_objects),
|
|
||||||
|
#[cfg(feature = "osu")]
|
||||||
|
{
|
||||||
|
#[cfg(feature = "no_leniency")]
|
||||||
|
{
|
||||||
|
osu::no_leniency::stars(self, mods, passed_objects)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(not(feature = "no_leniency"), feature = "no_sliders_no_leniency"))]
|
||||||
|
{
|
||||||
|
osu::no_sliders_no_leniency::stars(self, mods, passed_objects)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
not(feature = "no_leniency"),
|
||||||
|
not(feature = "no_sliders_no_leniency"),
|
||||||
|
feature = "all_included"
|
||||||
|
))]
|
||||||
|
{
|
||||||
|
osu::all_included::stars(self, mods, passed_objects)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(
|
||||||
|
feature = "no_leniency",
|
||||||
|
feature = "no_sliders_no_leniency",
|
||||||
|
feature = "all_included"
|
||||||
|
)))]
|
||||||
|
panic!("either of the features `no_leniency`, `no_sliders_no_leniency`, or `all_included` must be enabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GameMode::MNA => {
|
||||||
|
#[cfg(not(feature = "mania"))]
|
||||||
|
panic!("`mania` feature is not enabled");
|
||||||
|
|
||||||
|
#[cfg(feature = "mania")]
|
||||||
|
mania::stars(self, mods, passed_objects)
|
||||||
|
}
|
||||||
|
GameMode::TKO => {
|
||||||
|
#[cfg(not(feature = "taiko"))]
|
||||||
|
panic!("`osu` feature is not enabled");
|
||||||
|
|
||||||
|
#[cfg(feature = "taiko")]
|
||||||
|
taiko::stars(self, mods, passed_objects)
|
||||||
|
}
|
||||||
|
GameMode::CTB => {
|
||||||
|
#[cfg(not(feature = "fruits"))]
|
||||||
|
panic!("`fruits` feature is not enabled");
|
||||||
|
|
||||||
|
#[cfg(feature = "fruits")]
|
||||||
|
fruits::stars(self, mods, passed_objects)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn max_pp(&self, mods: u32) -> PpResult {
|
fn max_pp(&self, mods: u32) -> PpResult {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
GameMode::STD => OsuPP::new(self)
|
GameMode::STD => {
|
||||||
.mods(mods)
|
#[cfg(not(feature = "osu"))]
|
||||||
.calculate(osu::no_leniency::stars),
|
panic!("`osu` feature is not enabled");
|
||||||
GameMode::MNA => ManiaPP::new(self).mods(mods).calculate(),
|
|
||||||
GameMode::TKO => TaikoPP::new(self).mods(mods).calculate(),
|
#[cfg(feature = "osu")]
|
||||||
GameMode::CTB => FruitsPP::new(self).mods(mods).calculate(),
|
OsuPP::new(self).mods(mods).calculate()
|
||||||
|
}
|
||||||
|
GameMode::MNA => {
|
||||||
|
#[cfg(not(feature = "mania"))]
|
||||||
|
panic!("`mania` feature is not enabled");
|
||||||
|
|
||||||
|
#[cfg(feature = "mania")]
|
||||||
|
ManiaPP::new(self).mods(mods).calculate()
|
||||||
|
}
|
||||||
|
GameMode::TKO => {
|
||||||
|
#[cfg(not(feature = "taiko"))]
|
||||||
|
panic!("`osu` feature is not enabled");
|
||||||
|
|
||||||
|
#[cfg(feature = "taiko")]
|
||||||
|
TaikoPP::new(self).mods(mods).calculate()
|
||||||
|
}
|
||||||
|
GameMode::CTB => {
|
||||||
|
#[cfg(not(feature = "fruits"))]
|
||||||
|
panic!("`fruits` feature is not enabled");
|
||||||
|
|
||||||
|
#[cfg(feature = "fruits")]
|
||||||
|
FruitsPP::new(self).mods(mods).calculate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn strains(&self, mods: impl Mods) -> Strains {
|
fn strains(&self, mods: impl Mods) -> Strains {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
GameMode::STD => osu::no_leniency::strains(self, mods),
|
GameMode::STD => {
|
||||||
GameMode::MNA => mania::strains(self, mods),
|
#[cfg(not(feature = "osu"))]
|
||||||
GameMode::TKO => taiko::strains(self, mods),
|
panic!("`osu` feature is not enabled");
|
||||||
GameMode::CTB => fruits::strains(self, mods),
|
|
||||||
|
#[cfg(feature = "osu")]
|
||||||
|
{
|
||||||
|
#[cfg(feature = "no_leniency")]
|
||||||
|
{
|
||||||
|
osu::no_leniency::strains(self, mods)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(not(feature = "no_leniency"), feature = "no_sliders_no_leniency"))]
|
||||||
|
{
|
||||||
|
osu::no_sliders_no_leniency::strains(self, mods)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
not(feature = "no_leniency"),
|
||||||
|
not(feature = "no_sliders_no_leniency"),
|
||||||
|
feature = "all_included"
|
||||||
|
))]
|
||||||
|
{
|
||||||
|
osu::all_included::strains(self, mods)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(
|
||||||
|
feature = "no_leniency",
|
||||||
|
feature = "no_sliders_no_leniency",
|
||||||
|
feature = "all_included"
|
||||||
|
)))]
|
||||||
|
panic!("either of the features `no_leniency`, `no_sliders_no_leniency`, or `all_included` must be enabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GameMode::MNA => {
|
||||||
|
#[cfg(not(feature = "mania"))]
|
||||||
|
panic!("`mania` feature is not enabled");
|
||||||
|
|
||||||
|
#[cfg(feature = "mania")]
|
||||||
|
mania::strains(self, mods)
|
||||||
|
}
|
||||||
|
GameMode::TKO => {
|
||||||
|
#[cfg(not(feature = "taiko"))]
|
||||||
|
panic!("`osu` feature is not enabled");
|
||||||
|
|
||||||
|
#[cfg(feature = "taiko")]
|
||||||
|
taiko::strains(self, mods)
|
||||||
|
}
|
||||||
|
GameMode::CTB => {
|
||||||
|
#[cfg(not(feature = "fruits"))]
|
||||||
|
panic!("`fruits` feature is not enabled");
|
||||||
|
|
||||||
|
#[cfg(feature = "fruits")]
|
||||||
|
fruits::strains(self, mods)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,9 +322,13 @@ pub struct Strains {
|
|||||||
/// Basic enum containing the result of a star calculation based on the mode.
|
/// Basic enum containing the result of a star calculation based on the mode.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum StarResult {
|
pub enum StarResult {
|
||||||
|
#[cfg(feature = "fruits")]
|
||||||
Fruits(fruits::DifficultyAttributes),
|
Fruits(fruits::DifficultyAttributes),
|
||||||
|
#[cfg(feature = "mania")]
|
||||||
Mania(mania::DifficultyAttributes),
|
Mania(mania::DifficultyAttributes),
|
||||||
|
#[cfg(feature = "osu")]
|
||||||
Osu(osu::DifficultyAttributes),
|
Osu(osu::DifficultyAttributes),
|
||||||
|
#[cfg(feature = "taiko")]
|
||||||
Taiko(taiko::DifficultyAttributes),
|
Taiko(taiko::DifficultyAttributes),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,9 +337,13 @@ impl StarResult {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn stars(&self) -> f32 {
|
pub fn stars(&self) -> f32 {
|
||||||
match self {
|
match self {
|
||||||
|
#[cfg(feature = "fruits")]
|
||||||
Self::Fruits(attributes) => attributes.stars,
|
Self::Fruits(attributes) => attributes.stars,
|
||||||
|
#[cfg(feature = "mania")]
|
||||||
Self::Mania(attributes) => attributes.stars,
|
Self::Mania(attributes) => attributes.stars,
|
||||||
|
#[cfg(feature = "osu")]
|
||||||
Self::Osu(attributes) => attributes.stars,
|
Self::Osu(attributes) => attributes.stars,
|
||||||
|
#[cfg(feature = "taiko")]
|
||||||
Self::Taiko(attributes) => attributes.stars,
|
Self::Taiko(attributes) => attributes.stars,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,6 +370,7 @@ impl PpResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "osu", feature = "taiko"))]
|
||||||
#[inline]
|
#[inline]
|
||||||
fn difficulty_range(val: f32, max: f32, avg: f32, min: f32) -> f32 {
|
fn difficulty_range(val: f32, max: f32, avg: f32, min: f32) -> f32 {
|
||||||
if val > 5.0 {
|
if val > 5.0 {
|
||||||
@@ -231,3 +381,41 @@ fn difficulty_range(val: f32, max: f32, avg: f32, min: f32) -> f32 {
|
|||||||
avg
|
avg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(
|
||||||
|
feature = "osu",
|
||||||
|
feature = "taiko",
|
||||||
|
feature = "fruits",
|
||||||
|
feature = "mania"
|
||||||
|
)))]
|
||||||
|
compile_error!("At least one of the features `osu`, `taiko`, `fruits`, `mania` must be enabled");
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
feature = "osu",
|
||||||
|
not(any(
|
||||||
|
feature = "all_included",
|
||||||
|
feature = "no_leniency",
|
||||||
|
feature = "no_sliders_no_leniency"
|
||||||
|
))
|
||||||
|
))]
|
||||||
|
compile_error!("Since the `osu` feature is enabled, either `no_leniency`, `no_sliders_no_leniency`, or `all_included` must be enabled aswell");
|
||||||
|
|
||||||
|
#[cfg(any(
|
||||||
|
all(feature = "no_leniency", feature = "no_sliders_no_leniency"),
|
||||||
|
all(feature = "no_leniency", feature = "all_included"),
|
||||||
|
all(feature = "all_included", feature = "no_sliders_no_leniency"),
|
||||||
|
))]
|
||||||
|
compile_error!("Only one of the features `no_leniency`, `no_sliders_no_leniency`, `all_included` can be enabled");
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
not(feature = "osu"),
|
||||||
|
any(
|
||||||
|
feature = "no_leniency",
|
||||||
|
feature = "no_sliders_no_leniency",
|
||||||
|
feature = "all_included"
|
||||||
|
)
|
||||||
|
))]
|
||||||
|
compile_error!("The features `no_leniency`, `no_sliders_no_leniency`, and `all_included` should only be enabled in combination with the `osu` feature");
|
||||||
|
|
||||||
|
#[cfg(feature = "all_included")]
|
||||||
|
compile_error!("The `all_included` version is still WIP and unavailable, use either the `no_leniency` or `no_sliders_no_leniency` feature");
|
||||||
|
|||||||
+4
-2
@@ -1,3 +1,5 @@
|
|||||||
|
#![cfg(feature = "mania")]
|
||||||
|
|
||||||
mod pp;
|
mod pp;
|
||||||
mod strain;
|
mod strain;
|
||||||
|
|
||||||
@@ -151,7 +153,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn mania_single() {
|
fn mania_single() {
|
||||||
let file = match File::open("./maps/1529790.osu") {
|
let file = match File::open("./maps/1355822.osu") {
|
||||||
Ok(file) => file,
|
Ok(file) => file,
|
||||||
Err(why) => panic!("Could not open file: {}", why),
|
Err(why) => panic!("Could not open file: {}", why),
|
||||||
};
|
};
|
||||||
@@ -161,7 +163,7 @@ mod tests {
|
|||||||
Err(why) => panic!("Error while parsing map: {}", why),
|
Err(why) => panic!("Error while parsing map: {}", why),
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = ManiaPP::new(&map).mods(0).calculate();
|
let result = ManiaPP::new(&map).mods(256).calculate();
|
||||||
|
|
||||||
println!("Stars: {}", result.stars());
|
println!("Stars: {}", result.stars());
|
||||||
println!("PP: {}", result.pp());
|
println!("PP: {}", result.pp());
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ impl ManiaStarProvider for DifficultyAttributes {
|
|||||||
|
|
||||||
impl ManiaStarProvider for StarResult {
|
impl ManiaStarProvider for StarResult {
|
||||||
fn attributes(self) -> Option<f32> {
|
fn attributes(self) -> Option<f32> {
|
||||||
|
#[allow(irrefutable_let_patterns)]
|
||||||
if let Self::Mania(attributes) = self {
|
if let Self::Mania(attributes) = self {
|
||||||
Some(attributes.stars)
|
Some(attributes.stars)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
#![cfg(any(
|
||||||
|
feature = "fruits",
|
||||||
|
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||||
|
))]
|
||||||
|
|
||||||
use crate::Pos2;
|
use crate::Pos2;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![cfg(feature = "osu")]
|
||||||
|
|
||||||
mod pp;
|
mod pp;
|
||||||
mod versions;
|
mod versions;
|
||||||
|
|
||||||
|
|||||||
+25
-4
@@ -13,6 +13,7 @@ impl OsuAttributeProvider for DifficultyAttributes {
|
|||||||
|
|
||||||
impl OsuAttributeProvider for StarResult {
|
impl OsuAttributeProvider for StarResult {
|
||||||
fn attributes(self) -> Option<DifficultyAttributes> {
|
fn attributes(self) -> Option<DifficultyAttributes> {
|
||||||
|
#[allow(irrefutable_let_patterns)]
|
||||||
if let Self::Osu(attributes) = self {
|
if let Self::Osu(attributes) = self {
|
||||||
Some(attributes)
|
Some(attributes)
|
||||||
} else {
|
} else {
|
||||||
@@ -144,6 +145,7 @@ impl<'m> OsuPP<'m> {
|
|||||||
let acc = acc / 100.0;
|
let acc = acc / 100.0;
|
||||||
|
|
||||||
if self.n100.or(self.n50).is_some() {
|
if self.n100.or(self.n50).is_some() {
|
||||||
|
// TODO: Improve this branch
|
||||||
self.n300.replace(
|
self.n300.replace(
|
||||||
n_objects - self.n100.unwrap_or(0) - self.n50.unwrap_or(0) - self.n_misses,
|
n_objects - self.n100.unwrap_or(0) - self.n50.unwrap_or(0) - self.n_misses,
|
||||||
);
|
);
|
||||||
@@ -168,13 +170,32 @@ impl<'m> OsuPP<'m> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns an object which contains the pp and [`DifficultyAttributes`](crate::osu::DifficultyAttributes)
|
||||||
|
/// containing stars and other attributes.
|
||||||
|
#[cfg(feature = "no_leniency")]
|
||||||
|
pub fn calculate(self) -> PpResult {
|
||||||
|
self.calculate_with_func(super::no_leniency::stars)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an object which contains the pp and [`DifficultyAttributes`](crate::osu::DifficultyAttributes)
|
||||||
|
/// containing stars and other attributes.
|
||||||
|
#[cfg(feature = "no_sliders_no_leniency")]
|
||||||
|
pub fn calculate(self) -> PpResult {
|
||||||
|
self.calculate_with_func(super::no_sliders_no_leniency::stars)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an object which contains the pp and [`DifficultyAttributes`](crate::osu::DifficultyAttributes)
|
||||||
|
/// containing stars and other attributes.
|
||||||
|
#[cfg(feature = "all_included")]
|
||||||
|
pub fn calculate(self) -> PpResult {
|
||||||
|
self.calculate_with_func(super::all_included::stars)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns an object which contains the pp and [`DifficultyAttributes`](crate::osu::DifficultyAttributes)
|
/// Returns an object which contains the pp and [`DifficultyAttributes`](crate::osu::DifficultyAttributes)
|
||||||
/// containing stars and other attributes.
|
/// containing stars and other attributes.
|
||||||
///
|
///
|
||||||
/// `stars_func` will be used to calculate the difficulty attributes if not already given.
|
/// `stars_func` will be used to calculate the difficulty attributes if they are not yet given.
|
||||||
/// Only called if `attributes` was not set.
|
fn calculate_with_func(
|
||||||
/// The default is suggested to be [`no_leniency::stars`](crate::osu::no_leniency::stars).
|
|
||||||
pub fn calculate(
|
|
||||||
mut self,
|
mut self,
|
||||||
stars_func: impl FnOnce(&Beatmap, u32, Option<usize>) -> StarResult,
|
stars_func: impl FnOnce(&Beatmap, u32, Option<usize>) -> StarResult,
|
||||||
) -> PpResult {
|
) -> PpResult {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#![cfg(feature = "all_included")]
|
||||||
#![allow(unused)]
|
#![allow(unused)]
|
||||||
|
|
||||||
mod difficulty_object;
|
mod difficulty_object;
|
||||||
@@ -11,7 +12,7 @@ use skill::Skill;
|
|||||||
use skill_kind::SkillKind;
|
use skill_kind::SkillKind;
|
||||||
|
|
||||||
use super::super::DifficultyAttributes;
|
use super::super::DifficultyAttributes;
|
||||||
use crate::{Beatmap, Mods, StarResult};
|
use crate::{Beatmap, Mods, StarResult, Strains};
|
||||||
|
|
||||||
const SECTION_LEN: f32 = 400.0;
|
const SECTION_LEN: f32 = 400.0;
|
||||||
const DIFFICULTY_MULTIPLIER: f32 = 0.0675;
|
const DIFFICULTY_MULTIPLIER: f32 = 0.0675;
|
||||||
@@ -128,7 +129,13 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
|||||||
StarResult::Osu(attributes)
|
StarResult::Osu(attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: strains function
|
/// Essentially the same as the `stars` function but instead of
|
||||||
|
/// evaluating the final strains, it just returns them as is.
|
||||||
|
///
|
||||||
|
/// Suitable to plot the difficulty of a map over time.
|
||||||
|
pub fn strains(_map: &Beatmap, _mods: impl Mods) -> Strains {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@@ -150,7 +157,7 @@ mod tests {
|
|||||||
Err(why) => panic!("Error while parsing map: {}", why),
|
Err(why) => panic!("Error while parsing map: {}", why),
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = OsuPP::new(&map).mods(64).calculate(stars);
|
let result = OsuPP::new(&map).mods(64).calculate();
|
||||||
|
|
||||||
println!("Stars: {}", result.stars());
|
println!("Stars: {}", result.stars());
|
||||||
println!("PP: {}", result.pp());
|
println!("PP: {}", result.pp());
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
mod all_included;
|
#[cfg(feature = "all_included")]
|
||||||
|
pub mod all_included;
|
||||||
|
|
||||||
|
#[cfg(feature = "no_leniency")]
|
||||||
pub mod no_leniency;
|
pub mod no_leniency;
|
||||||
|
|
||||||
|
#[cfg(feature = "no_sliders_no_leniency")]
|
||||||
pub mod no_sliders_no_leniency;
|
pub mod no_sliders_no_leniency;
|
||||||
|
|
||||||
const OSU_OD_MAX: f32 = 20.0;
|
const OSU_OD_MAX: f32 = 20.0;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
//! This means the jump distance inbetween notes might be slightly off, resulting in small inaccuracies.
|
//! This means the jump distance inbetween notes might be slightly off, resulting in small inaccuracies.
|
||||||
//! Since calculating these offsets is relatively expensive though, this version is considerably faster than `all_included`.
|
//! Since calculating these offsets is relatively expensive though, this version is considerably faster than `all_included`.
|
||||||
|
|
||||||
|
#![cfg(feature = "no_leniency")]
|
||||||
|
|
||||||
use super::super::DifficultyAttributes;
|
use super::super::DifficultyAttributes;
|
||||||
|
|
||||||
mod control_point_iter;
|
mod control_point_iter;
|
||||||
@@ -271,7 +273,6 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::super::super::OsuPP;
|
use super::super::super::OsuPP;
|
||||||
use super::stars;
|
|
||||||
use crate::Beatmap;
|
use crate::Beatmap;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
|
||||||
@@ -288,7 +289,7 @@ mod tests {
|
|||||||
Err(why) => panic!("Error while parsing map: {}", why),
|
Err(why) => panic!("Error while parsing map: {}", why),
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = OsuPP::new(&map).mods(0).calculate(stars);
|
let result = OsuPP::new(&map).mods(0).calculate();
|
||||||
|
|
||||||
println!("Stars: {}", result.stars());
|
println!("Stars: {}", result.stars());
|
||||||
println!("PP: {}", result.pp());
|
println!("PP: {}", result.pp());
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
//! This means the travel distance of notes is completely omitted which may cause further inaccuracies.
|
//! This means the travel distance of notes is completely omitted which may cause further inaccuracies.
|
||||||
//! Since the slider paths don't have to be computed though, it should generally be faster than `no_leniency`.
|
//! Since the slider paths don't have to be computed though, it should generally be faster than `no_leniency`.
|
||||||
|
|
||||||
|
#![cfg(feature = "no_sliders_no_leniency")]
|
||||||
|
|
||||||
use super::super::DifficultyAttributes;
|
use super::super::DifficultyAttributes;
|
||||||
|
|
||||||
mod control_point_iter;
|
mod control_point_iter;
|
||||||
@@ -285,7 +287,6 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::super::super::OsuPP;
|
use super::super::super::OsuPP;
|
||||||
use super::stars;
|
|
||||||
use crate::Beatmap;
|
use crate::Beatmap;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
|
||||||
@@ -302,7 +303,7 @@ mod tests {
|
|||||||
Err(why) => panic!("Error while parsing map: {}", why),
|
Err(why) => panic!("Error while parsing map: {}", why),
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = OsuPP::new(&map).mods(2).calculate(stars);
|
let result = OsuPP::new(&map).mods(2).calculate();
|
||||||
|
|
||||||
println!("Stars: {}", result.stars());
|
println!("Stars: {}", result.stars());
|
||||||
println!("PP: {}", result.pp());
|
println!("PP: {}", result.pp());
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
use super::OSU_FILE_HEADER;
|
use super::OSU_FILE_HEADER;
|
||||||
|
|
||||||
|
#[cfg(not(all(
|
||||||
|
feature = "osu",
|
||||||
|
feature = "taiko",
|
||||||
|
feature = "fruits",
|
||||||
|
feature = "mania"
|
||||||
|
)))]
|
||||||
|
use super::GameMode;
|
||||||
|
|
||||||
use std::error::Error as StdError;
|
use std::error::Error as StdError;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::io::Error as IOError;
|
use std::io::Error as IOError;
|
||||||
@@ -20,6 +28,14 @@ pub enum ParseError {
|
|||||||
InvalidTimingSignature,
|
InvalidTimingSignature,
|
||||||
MissingField(&'static str),
|
MissingField(&'static str),
|
||||||
UnknownHitObjectKind,
|
UnknownHitObjectKind,
|
||||||
|
|
||||||
|
#[cfg(not(all(
|
||||||
|
feature = "osu",
|
||||||
|
feature = "taiko",
|
||||||
|
feature = "fruits",
|
||||||
|
feature = "mania"
|
||||||
|
)))]
|
||||||
|
UnincludedMode(GameMode),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ParseError {
|
impl fmt::Display for ParseError {
|
||||||
@@ -38,6 +54,18 @@ impl fmt::Display for ParseError {
|
|||||||
Self::InvalidTimingSignature => f.write_str("invalid timing signature"),
|
Self::InvalidTimingSignature => f.write_str("invalid timing signature"),
|
||||||
Self::MissingField(field) => write!(f, "missing field `{}`", field),
|
Self::MissingField(field) => write!(f, "missing field `{}`", field),
|
||||||
Self::UnknownHitObjectKind => f.write_str("unsupported hitobject kind"),
|
Self::UnknownHitObjectKind => f.write_str("unsupported hitobject kind"),
|
||||||
|
|
||||||
|
#[cfg(not(all(
|
||||||
|
feature = "osu",
|
||||||
|
feature = "taiko",
|
||||||
|
feature = "fruits",
|
||||||
|
feature = "mania"
|
||||||
|
)))]
|
||||||
|
Self::UnincludedMode(mode) => write!(
|
||||||
|
f,
|
||||||
|
"cannot process {:?} map its mode's feature has not been included",
|
||||||
|
mode
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,6 +84,14 @@ impl StdError for ParseError {
|
|||||||
Self::InvalidTimingSignature => None,
|
Self::InvalidTimingSignature => None,
|
||||||
Self::MissingField(_) => None,
|
Self::MissingField(_) => None,
|
||||||
Self::UnknownHitObjectKind => None,
|
Self::UnknownHitObjectKind => None,
|
||||||
|
|
||||||
|
#[cfg(not(all(
|
||||||
|
feature = "osu",
|
||||||
|
feature = "taiko",
|
||||||
|
feature = "fruits",
|
||||||
|
feature = "mania"
|
||||||
|
)))]
|
||||||
|
Self::UnincludedMode(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-1
@@ -1,4 +1,10 @@
|
|||||||
use super::{PathType, Pos2};
|
use super::Pos2;
|
||||||
|
|
||||||
|
#[cfg(any(
|
||||||
|
feature = "fruits",
|
||||||
|
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||||
|
))]
|
||||||
|
use super::PathType;
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
@@ -50,12 +56,24 @@ impl PartialOrd for HitObject {
|
|||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum HitObjectKind {
|
pub enum HitObjectKind {
|
||||||
Circle,
|
Circle,
|
||||||
|
#[cfg(any(
|
||||||
|
feature = "fruits",
|
||||||
|
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||||
|
))]
|
||||||
Slider {
|
Slider {
|
||||||
pixel_len: f32,
|
pixel_len: f32,
|
||||||
repeats: usize,
|
repeats: usize,
|
||||||
curve_points: Vec<Pos2>,
|
curve_points: Vec<Pos2>,
|
||||||
path_type: PathType,
|
path_type: PathType,
|
||||||
},
|
},
|
||||||
|
#[cfg(not(any(
|
||||||
|
feature = "fruits",
|
||||||
|
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||||
|
)))]
|
||||||
|
Slider {
|
||||||
|
pixel_len: f32,
|
||||||
|
repeats: usize,
|
||||||
|
},
|
||||||
Spinner {
|
Spinner {
|
||||||
end_time: f32,
|
end_time: f32,
|
||||||
},
|
},
|
||||||
|
|||||||
+136
-40
@@ -98,11 +98,16 @@ pub struct Beatmap {
|
|||||||
pub hp: f32,
|
pub hp: f32,
|
||||||
pub sv: f32,
|
pub sv: f32,
|
||||||
pub tick_rate: f32,
|
pub tick_rate: f32,
|
||||||
pub stack_leniency: f32,
|
|
||||||
|
|
||||||
pub hit_objects: Vec<HitObject>,
|
pub hit_objects: Vec<HitObject>,
|
||||||
|
|
||||||
|
#[cfg(any(feature = "osu", feature = "fruits"))]
|
||||||
pub timing_points: Vec<TimingPoint>,
|
pub timing_points: Vec<TimingPoint>,
|
||||||
|
|
||||||
|
#[cfg(any(feature = "osu", feature = "fruits"))]
|
||||||
pub difficulty_points: Vec<DifficultyPoint>,
|
pub difficulty_points: Vec<DifficultyPoint>,
|
||||||
|
|
||||||
|
#[cfg(all(feature = "osu", feature = "all_included"))]
|
||||||
|
pub stack_leniency: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) const OSU_FILE_HEADER: &str = "osu file format v";
|
pub(crate) const OSU_FILE_HEADER: &str = "osu file format v";
|
||||||
@@ -110,11 +115,9 @@ pub(crate) const OSU_FILE_HEADER: &str = "osu file format v";
|
|||||||
impl Beatmap {
|
impl Beatmap {
|
||||||
const CIRCLE_FLAG: u8 = 1 << 0;
|
const CIRCLE_FLAG: u8 = 1 << 0;
|
||||||
const SLIDER_FLAG: u8 = 1 << 1;
|
const SLIDER_FLAG: u8 = 1 << 1;
|
||||||
#[allow(unused)]
|
// const NEW_COMBO_FLAG: u8 = 1 << 2;
|
||||||
const NEW_COMBO_FLAG: u8 = 1 << 2;
|
|
||||||
const SPINNER_FLAG: u8 = 1 << 3;
|
const SPINNER_FLAG: u8 = 1 << 3;
|
||||||
#[allow(unused)]
|
// const COMBO_OFFSET_FLAG: u8 = (1 << 4) | (1 << 5) | (1 << 6);
|
||||||
const COMBO_OFFSET_FLAG: u8 = (1 << 4) | (1 << 5) | (1 << 6);
|
|
||||||
const HOLD_FLAG: u8 = 1 << 7;
|
const HOLD_FLAG: u8 = 1 << 7;
|
||||||
|
|
||||||
pub fn parse<R: Read>(input: R) -> ParseResult<Self> {
|
pub fn parse<R: Read>(input: R) -> ParseResult<Self> {
|
||||||
@@ -178,9 +181,11 @@ impl Beatmap {
|
|||||||
section: &mut Section,
|
section: &mut Section,
|
||||||
) -> ParseResult<bool> {
|
) -> ParseResult<bool> {
|
||||||
let mut mode = None;
|
let mut mode = None;
|
||||||
let mut stack_leniency = None;
|
|
||||||
let mut empty = true;
|
let mut empty = true;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "osu", feature = "all_included"))]
|
||||||
|
let mut stack_leniency = None;
|
||||||
|
|
||||||
while reader.read_line(buf)? != 0 {
|
while reader.read_line(buf)? != 0 {
|
||||||
let line = line_prepare!(buf);
|
let line = line_prepare!(buf);
|
||||||
|
|
||||||
@@ -201,7 +206,10 @@ impl Beatmap {
|
|||||||
"3" => Some(GameMode::MNA),
|
"3" => Some(GameMode::MNA),
|
||||||
_ => return Err(ParseError::InvalidMode),
|
_ => return Err(ParseError::InvalidMode),
|
||||||
};
|
};
|
||||||
} else if key == "StackLeniency" {
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "osu", feature = "all_included"))]
|
||||||
|
if key == "StackLeniency" {
|
||||||
stack_leniency = Some(value.parse()?);
|
stack_leniency = Some(value.parse()?);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +217,31 @@ impl Beatmap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.mode = mode.unwrap_or(GameMode::STD);
|
self.mode = mode.unwrap_or(GameMode::STD);
|
||||||
self.stack_leniency = stack_leniency.unwrap_or(0.7);
|
|
||||||
|
#[cfg(not(feature = "osu"))]
|
||||||
|
if self.mode == GameMode::STD {
|
||||||
|
return Err(ParseError::UnincludedMode(GameMode::STD));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "taiko"))]
|
||||||
|
if self.mode == GameMode::TKO {
|
||||||
|
return Err(ParseError::UnincludedMode(GameMode::TKO));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "fruits"))]
|
||||||
|
if self.mode == GameMode::CTB {
|
||||||
|
return Err(ParseError::UnincludedMode(GameMode::CTB));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "mania"))]
|
||||||
|
if self.mode == GameMode::MNA {
|
||||||
|
return Err(ParseError::UnincludedMode(GameMode::MNA));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "osu", feature = "all_included"))]
|
||||||
|
{
|
||||||
|
self.stack_leniency = stack_leniency.unwrap_or(0.7);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(empty)
|
Ok(empty)
|
||||||
}
|
}
|
||||||
@@ -264,6 +296,32 @@ impl Beatmap {
|
|||||||
Ok(empty)
|
Ok(empty)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(feature = "osu", feature = "fruits")))]
|
||||||
|
fn parse_timingpoints<R: Read>(
|
||||||
|
&mut self,
|
||||||
|
reader: &mut BufReader<R>,
|
||||||
|
buf: &mut String,
|
||||||
|
section: &mut Section,
|
||||||
|
) -> ParseResult<bool> {
|
||||||
|
let mut empty = true;
|
||||||
|
|
||||||
|
while reader.read_line(buf)? != 0 {
|
||||||
|
let line = line_prepare!(buf);
|
||||||
|
|
||||||
|
if line.starts_with('[') && line.ends_with(']') {
|
||||||
|
*section = Section::from_str(&line[1..line.len() - 1]);
|
||||||
|
empty = false;
|
||||||
|
buf.clear();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(empty)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "osu", feature = "fruits"))]
|
||||||
fn parse_timingpoints<R: Read>(
|
fn parse_timingpoints<R: Read>(
|
||||||
&mut self,
|
&mut self,
|
||||||
reader: &mut BufReader<R>,
|
reader: &mut BufReader<R>,
|
||||||
@@ -386,45 +444,67 @@ impl Beatmap {
|
|||||||
HitObjectKind::Circle
|
HitObjectKind::Circle
|
||||||
} else if kind & Self::SLIDER_FLAG > 0 {
|
} else if kind & Self::SLIDER_FLAG > 0 {
|
||||||
self.n_sliders += 1;
|
self.n_sliders += 1;
|
||||||
let mut curve_points = Vec::with_capacity(16);
|
|
||||||
curve_points.push(pos);
|
|
||||||
|
|
||||||
let mut curve_point_iter = next_field!(split.next(), curve points).split('|');
|
#[cfg(any(
|
||||||
|
feature = "fruits",
|
||||||
|
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||||
|
))]
|
||||||
|
{
|
||||||
|
let mut curve_points = Vec::with_capacity(16);
|
||||||
|
curve_points.push(pos);
|
||||||
|
|
||||||
let mut path_type: PathType =
|
let mut curve_point_iter = next_field!(split.next(), curve points).split('|');
|
||||||
next_field!(curve_point_iter.next(), path kind).parse()?;
|
|
||||||
|
|
||||||
for pos in curve_point_iter {
|
let mut path_type: PathType =
|
||||||
let mut v = pos.split(':').map(str::parse);
|
next_field!(curve_point_iter.next(), path kind).parse()?;
|
||||||
|
|
||||||
match (v.next(), v.next()) {
|
for pos in curve_point_iter {
|
||||||
(Some(Ok(x)), Some(Ok(y))) => curve_points.push(Pos2 { x, y }),
|
let mut v = pos.split(':').map(str::parse);
|
||||||
_ => return Err(ParseError::InvalidCurvePoints),
|
|
||||||
|
match (v.next(), v.next()) {
|
||||||
|
(Some(Ok(x)), Some(Ok(y))) => curve_points.push(Pos2 { x, y }),
|
||||||
|
_ => return Err(ParseError::InvalidCurvePoints),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.version <= 6 && curve_points.len() >= 2 {
|
||||||
|
if path_type == PathType::Linear {
|
||||||
|
path_type = PathType::Bezier;
|
||||||
|
}
|
||||||
|
|
||||||
|
if curve_points.len() == 2
|
||||||
|
&& (pos == curve_points[0] || pos == curve_points[1])
|
||||||
|
{
|
||||||
|
path_type = PathType::Linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if curve_points.is_empty() {
|
||||||
|
HitObjectKind::Circle
|
||||||
|
} else {
|
||||||
|
let repeats = next_field!(split.next(), repeats).parse::<usize>()?;
|
||||||
|
let len: f32 = next_field!(split.next(), pixel len).parse()?;
|
||||||
|
|
||||||
|
HitObjectKind::Slider {
|
||||||
|
repeats,
|
||||||
|
pixel_len: len,
|
||||||
|
curve_points,
|
||||||
|
path_type,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.version <= 6 && curve_points.len() >= 2 {
|
#[cfg(not(any(
|
||||||
if path_type == PathType::Linear {
|
feature = "fruits",
|
||||||
path_type = PathType::Bezier;
|
all(feature = "osu", not(feature = "no_sliders_no_leniency"))
|
||||||
}
|
)))]
|
||||||
|
{
|
||||||
if curve_points.len() == 2 && (pos == curve_points[0] || pos == curve_points[1])
|
let repeats = next_field!(split.nth(1), repeats).parse::<usize>()?;
|
||||||
{
|
|
||||||
path_type = PathType::Linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if curve_points.is_empty() {
|
|
||||||
HitObjectKind::Circle
|
|
||||||
} else {
|
|
||||||
let repeats = next_field!(split.next(), repeats).parse::<usize>()?;
|
|
||||||
let len: f32 = next_field!(split.next(), pixel len).parse()?;
|
let len: f32 = next_field!(split.next(), pixel len).parse()?;
|
||||||
|
|
||||||
HitObjectKind::Slider {
|
HitObjectKind::Slider {
|
||||||
repeats,
|
repeats,
|
||||||
pixel_len: len,
|
pixel_len: len,
|
||||||
curve_points,
|
|
||||||
path_type,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if kind & Self::SPINNER_FLAG > 0 {
|
} else if kind & Self::SPINNER_FLAG > 0 {
|
||||||
@@ -536,7 +616,17 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parsing() {
|
fn parsing() {
|
||||||
let file = match File::open("./maps/61843.osu") {
|
let map_id = if cfg!(feature = "osu") {
|
||||||
|
61843
|
||||||
|
} else if cfg!(feature = "mania") {
|
||||||
|
1355822
|
||||||
|
} else if cfg!(feature = "fruits") {
|
||||||
|
1977380
|
||||||
|
} else {
|
||||||
|
110219
|
||||||
|
};
|
||||||
|
|
||||||
|
let file = match File::open(format!("./maps/{}.osu", map_id)) {
|
||||||
Ok(file) => file,
|
Ok(file) => file,
|
||||||
Err(why) => panic!("Could not read file: {}", why),
|
Err(why) => panic!("Could not read file: {}", why),
|
||||||
};
|
};
|
||||||
@@ -556,9 +646,15 @@ mod tests {
|
|||||||
println!("hp: {}", map.hp);
|
println!("hp: {}", map.hp);
|
||||||
println!("sv: {}", map.sv);
|
println!("sv: {}", map.sv);
|
||||||
println!("tick_rate: {}", map.tick_rate);
|
println!("tick_rate: {}", map.tick_rate);
|
||||||
println!("stack_leniency: {}", map.stack_leniency);
|
|
||||||
println!("hit_objects: {}", map.hit_objects.len());
|
println!("hit_objects: {}", map.hit_objects.len());
|
||||||
println!("timing_points: {}", map.timing_points.len());
|
|
||||||
println!("difficulty_points: {}", map.difficulty_points.len());
|
#[cfg(any(feature = "osu", feature = "fruits"))]
|
||||||
|
{
|
||||||
|
#[cfg(feature = "all_included")]
|
||||||
|
println!("stack_leniency: {}", map.stack_leniency);
|
||||||
|
|
||||||
|
println!("timing_points: {}", map.timing_points.len());
|
||||||
|
println!("difficulty_points: {}", map.difficulty_points.len());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![cfg(feature = "taiko")]
|
||||||
|
|
||||||
mod difficulty_object;
|
mod difficulty_object;
|
||||||
mod hitobject_rhythm;
|
mod hitobject_rhythm;
|
||||||
mod limited_queue;
|
mod limited_queue;
|
||||||
|
|||||||
+2
-3
@@ -19,6 +19,7 @@ impl TaikoStarProvider for DifficultyAttributes {
|
|||||||
|
|
||||||
impl TaikoStarProvider for StarResult {
|
impl TaikoStarProvider for StarResult {
|
||||||
fn attributes(self) -> Option<f32> {
|
fn attributes(self) -> Option<f32> {
|
||||||
|
#[allow(irrefutable_let_patterns)]
|
||||||
if let StarResult::Taiko(attributes) = self {
|
if let StarResult::Taiko(attributes) = self {
|
||||||
Some(attributes.stars)
|
Some(attributes.stars)
|
||||||
} else {
|
} else {
|
||||||
@@ -48,13 +49,11 @@ pub struct TaikoPP<'m> {
|
|||||||
impl<'m> TaikoPP<'m> {
|
impl<'m> TaikoPP<'m> {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn new(map: &'m Beatmap) -> Self {
|
pub fn new(map: &'m Beatmap) -> Self {
|
||||||
let max_combo = map.hit_objects.iter().filter(|h| h.is_circle()).count();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
map,
|
map,
|
||||||
stars: None,
|
stars: None,
|
||||||
mods: 0,
|
mods: 0,
|
||||||
max_combo,
|
max_combo: map.n_circles as usize,
|
||||||
combo: None,
|
combo: None,
|
||||||
acc: 1.0,
|
acc: 1.0,
|
||||||
n_misses: 0,
|
n_misses: 0,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![cfg(feature = "fruits")]
|
||||||
|
|
||||||
extern crate rosu_pp;
|
extern crate rosu_pp;
|
||||||
|
|
||||||
use rosu_pp::Beatmap;
|
use rosu_pp::Beatmap;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![cfg(feature = "mania")]
|
||||||
|
|
||||||
extern crate rosu_pp;
|
extern crate rosu_pp;
|
||||||
|
|
||||||
use rosu_pp::Beatmap;
|
use rosu_pp::Beatmap;
|
||||||
|
|||||||
+15
-64
@@ -1,6 +1,8 @@
|
|||||||
|
#![cfg(feature = "osu")]
|
||||||
|
|
||||||
extern crate rosu_pp;
|
extern crate rosu_pp;
|
||||||
|
|
||||||
use rosu_pp::{osu, Beatmap};
|
use rosu_pp::Beatmap;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
|
||||||
struct MapResult {
|
struct MapResult {
|
||||||
@@ -11,9 +13,17 @@ struct MapResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn osu_no_sliders() {
|
fn osu() {
|
||||||
let star_margin = 0.0075;
|
let margin = if cfg!(feature = "no_leniency") {
|
||||||
let pp_margin = 0.0075;
|
0.0025
|
||||||
|
} else if cfg!(feature = "no_sliders_no_leniency") {
|
||||||
|
0.0075
|
||||||
|
} else {
|
||||||
|
0.001
|
||||||
|
};
|
||||||
|
|
||||||
|
let star_margin = margin;
|
||||||
|
let pp_margin = margin;
|
||||||
|
|
||||||
for result in RESULTS {
|
for result in RESULTS {
|
||||||
let MapResult {
|
let MapResult {
|
||||||
@@ -33,66 +43,7 @@ fn osu_no_sliders() {
|
|||||||
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
|
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = rosu_pp::OsuPP::new(&map)
|
let result = rosu_pp::OsuPP::new(&map).mods(*mods).calculate();
|
||||||
.mods(*mods)
|
|
||||||
.calculate(osu::no_sliders_no_leniency::stars);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
(result.stars() - stars).abs() < star_margin * stars,
|
|
||||||
"\nStars:\n\
|
|
||||||
Calculated: {calculated} | Expected: {expected}\n \
|
|
||||||
=> {margin} margin ({allowed} allowed)\n\
|
|
||||||
[map {map} | mods {mods}]\n",
|
|
||||||
calculated = result.stars(),
|
|
||||||
expected = stars,
|
|
||||||
margin = (result.stars() - stars).abs(),
|
|
||||||
allowed = star_margin * stars,
|
|
||||||
map = map_id,
|
|
||||||
mods = mods
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
(result.pp() - pp).abs() < pp_margin * pp,
|
|
||||||
"\nPP:\n\
|
|
||||||
Calculated: {calculated} | Expected: {expected}\n \
|
|
||||||
=> {margin} margin ({allowed} allowed)\n\
|
|
||||||
[map {map} | mods {mods}]\n",
|
|
||||||
calculated = result.pp(),
|
|
||||||
expected = pp,
|
|
||||||
margin = (result.pp() - pp).abs(),
|
|
||||||
allowed = pp_margin * pp,
|
|
||||||
map = map_id,
|
|
||||||
mods = mods
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn osu_no_leniency() {
|
|
||||||
let star_margin = 0.0025;
|
|
||||||
let pp_margin = 0.0025;
|
|
||||||
|
|
||||||
for result in RESULTS {
|
|
||||||
let MapResult {
|
|
||||||
map_id,
|
|
||||||
mods,
|
|
||||||
stars,
|
|
||||||
pp,
|
|
||||||
} = result;
|
|
||||||
|
|
||||||
let file = match File::open(format!("./maps/{}.osu", map_id)) {
|
|
||||||
Ok(file) => file,
|
|
||||||
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
|
|
||||||
};
|
|
||||||
|
|
||||||
let map = match Beatmap::parse(file) {
|
|
||||||
Ok(map) => map,
|
|
||||||
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = rosu_pp::osu::OsuPP::new(&map)
|
|
||||||
.mods(*mods)
|
|
||||||
.calculate(osu::no_leniency::stars);
|
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
(result.stars() - stars).abs() < star_margin * stars,
|
(result.stars() - stars).abs() < star_margin * stars,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![cfg(feature = "taiko")]
|
||||||
|
|
||||||
extern crate rosu_pp;
|
extern crate rosu_pp;
|
||||||
|
|
||||||
use rosu_pp::Beatmap;
|
use rosu_pp::Beatmap;
|
||||||
|
|||||||
Reference in New Issue
Block a user