restructured osu!standard feature flags
This commit is contained in:
+6
-1
@@ -1,8 +1,13 @@
|
||||
## Upcoming
|
||||
|
||||
- added internal binary crate `pp-gen` to calculate difficulty & pp values via `PerformanceCalculator.dll`
|
||||
- [BREAKING] The crate features for osu!standard have been adjusted:
|
||||
- `no_leniency` has been removed
|
||||
- `no_sliders_no_leniency` has been renamed to `osu_fast`
|
||||
- `all_included` has been renamed to `osu_precise` and now serves as default
|
||||
Additionally, instead of importing through `rosu_pp::osu::{version}`, you now have to import through `rosu_pp::osu`
|
||||
- [BREAKING] Instead of returning `PpResult`, performance calculations now return `PerformanceAttributes` depending on the mode.
|
||||
- [BREAKING] Instead of returning `StarResult`, difficulty calculations now return `DifficultyAttributes` depending on the mode.
|
||||
- added internal binary crate `pp-gen` to calculate difficulty & pp values via `PerformanceCalculator.dll`
|
||||
- osu: Updated up to commit [6944151486e677bfd11f2390163aca9161defbbf](https://github.com/ppy/osu/commit/6944151486e677bfd11f2390163aca9161defbbf) (2021-10-27)
|
||||
|
||||
# v0.2.3
|
||||
|
||||
+5
-7
@@ -11,24 +11,22 @@ description = "osu! difficulty and pp calculation for all modes"
|
||||
keywords = ["osu", "pp", "stars", "async"]
|
||||
|
||||
[features]
|
||||
default = ["osu", "taiko", "fruits", "mania", "no_leniency"]
|
||||
# default = ["osu_precise", "taiko", "fruits", "mania"]
|
||||
default = ["osu_fast", "taiko", "fruits", "mania"]
|
||||
|
||||
# game modes
|
||||
osu = []
|
||||
osu_fast = ["osu"]
|
||||
osu_precise = ["osu", "sliders"]
|
||||
taiko = []
|
||||
fruits = ["sliders"]
|
||||
mania = []
|
||||
|
||||
# osu!standard version
|
||||
all_included = ["sliders"]
|
||||
no_leniency = ["sliders"]
|
||||
no_sliders_no_leniency = []
|
||||
|
||||
# async version
|
||||
async_std = ["async-std"]
|
||||
async_tokio = ["tokio"]
|
||||
|
||||
# auxiliary, no need to set yourself
|
||||
osu = []
|
||||
sliders = []
|
||||
|
||||
[dependencies.async-std]
|
||||
|
||||
@@ -90,28 +90,25 @@ println!("PP: {}", result.pp());
|
||||
|
||||
### osu!standard versions
|
||||
|
||||
- `all_included`: Both stack leniency & slider paths are considered so that the difficulty and pp calculation immitates osu! as close as possible. Pro: Most precise; Con: Least performant.
|
||||
- `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](https://github.com/Francesco149/oppai-ng)): In addition 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 is generally faster than `no_leniency`.
|
||||
- `osu_precise`: Both stack leniency & slider paths are considered so that the difficulty and pp calculation immitates osu! as close as possible. Pro: Very accurate values; Con: Less performant.
|
||||
- `osu_fast` (i.e. [oppai](https://github.com/Francesco149/oppai-ng)): Fully ignoring sliders aswell as the positional offset caused by stack leniency. This means the stacked position and travel distance of notes is completely omitted which results in notable inaccuracies but is also considerably faster than `osu_precise`.
|
||||
|
||||
**Note**: If the `fruits` feature is enabled, sliders will be parsed regardless, resulting in a reduced performance advantage of `no_sliders_no_leniency`.
|
||||
- **Note**: If the `fruits` feature is enabled, sliders will be parsed regardless, resulting in a reduced performance advantage of `osu_fast`. Hence, it is only recommended to use `osu_fast` if `fruits` is not enabled.
|
||||
|
||||
### Features
|
||||
|
||||
| Flag | Description |
|
||||
|-----|-----|
|
||||
| `default` | Enable all modes and choose the `no_leniency` version for osu!standard. |
|
||||
| `default` | Enable all modes and choose the `osu_precise` 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, hence the 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`. |
|
||||
| `osu_fast` | When calculating difficulty attributes in osu!standard, ignore stack leniency and sliders. Great performance but less precision values. |
|
||||
| `osu_precise` | When calculating difficulty attributes in osu!standard, consider both stack leniency and sliders. Great precision but significantly worse performance than `osu_fast`. |
|
||||
| `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) |
|
||||
|
||||
### Benchmarks
|
||||
### Benchmarks (TODO, update w.r.t new feature flags)
|
||||
|
||||
Comparing the PP calculation speed between [osu-perf](https://gitlab.com/JackRedstonia/osu-perf/) (alternative rust pp calculculation crate), an [oppai-ng](https://github.com/Francesco149/oppai-ng) rust binding, and rosu-pp's `no_sliders_no_leniency`:
|
||||
|
||||
@@ -128,22 +125,3 @@ Comparing the PP (in)accuracy between rosu-pp's `all_included`, `no_leniency`, a
|
||||
Comparing the stars (in)accuracy between rosu-pp's `all_included`, `no_leniency`, and `no_sliders_no_leniency` versions:
|
||||
|
||||
<img src="./benchmark_results/stars_inaccuracy.svg">
|
||||
|
||||
### Roadmap
|
||||
|
||||
- \[x\] osu sr versions
|
||||
- \[x\] all included
|
||||
- \[x\] no_leniency
|
||||
- \[x\] no_sliders_no_leniency
|
||||
- \[x\] taiko sr
|
||||
- \[x\] ctb sr
|
||||
- \[x\] mania sr
|
||||
---
|
||||
- \[x\] osu pp
|
||||
- \[x\] taiko pp
|
||||
- \[x\] ctb pp
|
||||
- \[x\] mania pp
|
||||
---
|
||||
- \[x\] refactoring
|
||||
- \[x\] benchmarking
|
||||
- \[x\] async
|
||||
@@ -40,7 +40,7 @@ impl<'p> ControlPointIter<'p> {
|
||||
pub(crate) enum ControlPoint {
|
||||
Timing {
|
||||
time: f32,
|
||||
#[allow(dead_code)] // not used in `no_sliders_no_leniency` feature
|
||||
#[allow(dead_code)] // not used in `osu_fast` feature
|
||||
beat_len: f32,
|
||||
},
|
||||
Difficulty {
|
||||
@@ -49,9 +49,9 @@ pub(crate) enum ControlPoint {
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "osu_precise", feature = "fruits"))]
|
||||
impl ControlPoint {
|
||||
#[inline]
|
||||
#[cfg(not(feature = "no_sliders_no_leniency"))]
|
||||
pub(crate) fn time(&self) -> f32 {
|
||||
match self {
|
||||
Self::Timing { time, .. } => *time,
|
||||
|
||||
@@ -638,6 +638,7 @@ impl PerformanceAttributes {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn custom_fruits() {
|
||||
use std::{fs::File, time::Instant};
|
||||
|
||||
|
||||
+12
-49
@@ -93,45 +93,24 @@
|
||||
//!
|
||||
//! ## osu!standard versions
|
||||
//!
|
||||
//! - `all_included`: Both stack leniency & slider paths are considered so that the difficulty and pp calculation immitates osu! as close as possible. Pro: Most precise; Con: Least performant.
|
||||
//! - `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](https://github.com/Francesco149/oppai-ng)): In addition 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 is generally faster than `no_leniency`.
|
||||
//! - `osu_precise`: Both stack leniency & slider paths are considered so that the difficulty and pp calculation immitates osu! as close as possible. Pro: Very accurate values; Con: Less performant.
|
||||
//! - `osu_fast` (i.e. [oppai](https://github.com/Francesco149/oppai-ng)): Fully ignoring sliders aswell as the positional offset caused by stack leniency. This means the stacked position and travel distance of notes is completely omitted which results in notable inaccuracies but is also considerably faster than `osu_precise`.
|
||||
//!
|
||||
//! **Note**: If the `fruits` feature is enabled, sliders will be parsed regardless, resulting in a reduced performance advantage of `no_sliders_no_leniency`.
|
||||
//! **Note**: If the `fruits` feature is enabled, sliders will be parsed regardless, resulting in a reduced performance advantage of `osu_fast`. Hence, it is only recommended to use `osu_fast` if `fruits` is not enabled.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! | Flag | Description |
|
||||
//! |-----|-----|
|
||||
//! | `default` | Enable all modes and choose the `no_leniency` version for osu!standard. |
|
||||
//! | `default` | Enable all modes and choose the `osu_precise` 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, hence the 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`. |
|
||||
//! | `osu_fast` | When calculating difficulty attributes in osu!standard, ignore stack leniency and sliders. Great performance but less precise values. |
|
||||
//! | `osu_precise` | When calculating difficulty attributes in osu!standard, consider both stack leniency and sliders. Great precision but significantly worse performance than `osu_fast`. |
|
||||
//! | `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) |
|
||||
//!
|
||||
//! ## Roadmap
|
||||
//!
|
||||
//! - \[x\] osu sr versions
|
||||
//! - \[x\] all included
|
||||
//! - \[x\] no_leniency
|
||||
//! - \[x\] no_sliders_no_leniency
|
||||
//! - \[x\] taiko sr
|
||||
//! - \[x\] ctb sr
|
||||
//! - \[x\] mania sr
|
||||
//! ---
|
||||
//! - \[x\] osu pp
|
||||
//! - \[x\] taiko pp
|
||||
//! - \[x\] ctb pp
|
||||
//! - \[x\] mania pp
|
||||
//! ---
|
||||
//! - \[x\] refactoring
|
||||
//! - \[x\] benchmarking
|
||||
//! - \[x\] async parsing
|
||||
|
||||
#![cfg_attr(docsrs, feature(doc_cfg), deny(broken_intra_doc_links))]
|
||||
#![deny(clippy::all, nonstandard_style, rust_2018_idioms, unused, warnings)]
|
||||
@@ -420,30 +399,14 @@ compile_error!("At least one of the features `osu`, `taiko`, `fruits`, `mania` m
|
||||
|
||||
#[cfg(all(
|
||||
feature = "osu",
|
||||
not(any(
|
||||
feature = "all_included",
|
||||
feature = "no_leniency",
|
||||
feature = "no_sliders_no_leniency"
|
||||
))
|
||||
not(any(feature = "osu_precise", feature = "osu_fast"))
|
||||
))]
|
||||
compile_error!("Since the `osu` feature is enabled, either `no_leniency`, `no_sliders_no_leniency`, or `all_included` must be enabled aswell");
|
||||
compile_error!(
|
||||
"Since the `osu` feature is enabled, either `osu_precise` or `osu_fast` 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(any(all(feature = "osu_precise", feature = "osu_fast"),))]
|
||||
compile_error!("Only one of the features `osu_precise` and `osu_fast` should be enabled");
|
||||
|
||||
#[cfg(all(feature = "async_tokio", feature = "async_std"))]
|
||||
compile_error!("Only one of the features `async_tokio` and `async_std` should be enabled");
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
//! 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 is generally faster than `no_leniency`.
|
||||
//! This means stacked positions aswell as the travel distance of notes is completely omitted which
|
||||
//! will cause notable inaccuracies.
|
||||
//! The advantage is that it's considerably faster than `osu_precise`.
|
||||
|
||||
#![cfg(feature = "no_sliders_no_leniency")]
|
||||
#![cfg(feature = "osu_fast")]
|
||||
|
||||
use std::mem;
|
||||
|
||||
use super::super::DifficultyAttributes;
|
||||
use super::DifficultyAttributes;
|
||||
|
||||
mod difficulty_object;
|
||||
mod osu_object;
|
||||
@@ -30,8 +31,8 @@ const NORMALIZED_RADIUS: f32 = 52.0;
|
||||
/// Star calculation for osu!standard maps.
|
||||
///
|
||||
/// Sliders are considered as regular hitcircles and stack leniency is ignored.
|
||||
/// Still very good results but the least precise version in general.
|
||||
/// However, this is the most efficient one.
|
||||
/// Still decently accurate results but definitely less precise than `osu_precise`.
|
||||
/// However, this version is considerably faster.
|
||||
///
|
||||
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
|
||||
pub fn stars(
|
||||
+20
-2
@@ -1,10 +1,22 @@
|
||||
#![cfg(feature = "osu")]
|
||||
|
||||
mod pp;
|
||||
mod versions;
|
||||
|
||||
pub use pp::*;
|
||||
pub use versions::*;
|
||||
|
||||
#[cfg(feature = "osu_precise")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "osu_precise")))]
|
||||
mod precise;
|
||||
|
||||
#[cfg(feature = "osu_precise")]
|
||||
pub use precise::*;
|
||||
|
||||
#[cfg(feature = "osu_fast")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "osu_fast")))]
|
||||
mod fast;
|
||||
|
||||
#[cfg(feature = "osu_fast")]
|
||||
pub use fast::*;
|
||||
|
||||
/// Various data created through the star calculation.
|
||||
/// This data is necessary to calculate PP.
|
||||
@@ -48,7 +60,13 @@ impl PerformanceAttributes {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn difficulty_range_od(od: f32) -> f32 {
|
||||
super::difficulty_range(od, 20.0, 50.0, 80.0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn custom_osu() {
|
||||
use std::{fs::File, time::Instant};
|
||||
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
//! Every aspect of osu!'s pp calculation is being used.
|
||||
//! This should result in the most accurate values but with
|
||||
//! drawback of being slower than the other versions.
|
||||
//! drawback of being slower than `osu_fast`.
|
||||
|
||||
#![cfg(feature = "all_included")]
|
||||
#![cfg(feature = "osu_precise")]
|
||||
|
||||
use std::mem;
|
||||
|
||||
use self::osu_object::ObjectParameters;
|
||||
|
||||
use super::super::DifficultyAttributes;
|
||||
use crate::{curve::CurveBuffers, parse::Pos2};
|
||||
|
||||
mod difficulty_object;
|
||||
mod osu_object;
|
||||
mod skill;
|
||||
@@ -18,12 +13,14 @@ mod skill_kind;
|
||||
mod slider_state;
|
||||
|
||||
use difficulty_object::DifficultyObject;
|
||||
use osu_object::OsuObject;
|
||||
use osu_object::{ObjectParameters, OsuObject};
|
||||
use skill::Skill;
|
||||
use skill_kind::SkillKind;
|
||||
use slider_state::SliderState;
|
||||
|
||||
use crate::{Beatmap, Mods, Strains};
|
||||
use crate::{curve::CurveBuffers, parse::Pos2, Beatmap, Mods, Strains};
|
||||
|
||||
use super::DifficultyAttributes;
|
||||
|
||||
const OBJECT_RADIUS: f32 = 64.0;
|
||||
const SECTION_LEN: f32 = 400.0;
|
||||
@@ -33,10 +30,8 @@ const STACK_DISTANCE: f32 = 3.0;
|
||||
|
||||
/// Star calculation for osu!standard maps.
|
||||
///
|
||||
/// Both slider paths and stack leniency are considered.
|
||||
/// Since taking stack leniency into account is fairly expensive,
|
||||
/// this version is slower than the others but in turn gives the
|
||||
/// most precise results.
|
||||
/// Slider paths aswell as stack leniency are considered.
|
||||
/// Both of these drag the performance down but in turn the values are much more accurate
|
||||
///
|
||||
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
|
||||
pub fn stars(
|
||||
@@ -1,25 +0,0 @@
|
||||
#[cfg(feature = "all_included")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "all_included")))]
|
||||
mod all_included;
|
||||
|
||||
#[cfg(feature = "all_included")]
|
||||
pub use all_included::*;
|
||||
|
||||
#[cfg(feature = "no_leniency")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "no_leniency")))]
|
||||
mod no_leniency;
|
||||
|
||||
#[cfg(feature = "no_leniency")]
|
||||
pub use no_leniency::*;
|
||||
|
||||
#[cfg(feature = "no_sliders_no_leniency")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "no_sliders_no_leniency")))]
|
||||
mod no_sliders_no_leniency;
|
||||
|
||||
#[cfg(feature = "no_sliders_no_leniency")]
|
||||
pub use no_sliders_no_leniency::*;
|
||||
|
||||
#[inline]
|
||||
fn difficulty_range_od(od: f32) -> f32 {
|
||||
super::super::difficulty_range(od, 20.0, 50.0, 80.0)
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use super::OsuObject;
|
||||
|
||||
pub(crate) struct DifficultyObject<'h> {
|
||||
pub(crate) base: &'h OsuObject,
|
||||
pub(crate) prev: Option<(f32, f32)>, // (jump_dist, strain_time)
|
||||
|
||||
pub(crate) jump_dist: f32,
|
||||
pub(crate) travel_dist: f32,
|
||||
pub(crate) angle: Option<f32>,
|
||||
|
||||
pub(crate) delta: f32,
|
||||
pub(crate) strain_time: f32,
|
||||
}
|
||||
|
||||
impl<'h> DifficultyObject<'h> {
|
||||
pub(crate) fn new(
|
||||
base: &'h OsuObject,
|
||||
prev: &OsuObject,
|
||||
prev_vals: Option<(f32, f32)>, // (jump_dist, strain_time)
|
||||
prev_prev: Option<OsuObject>,
|
||||
scaling_factor: f32,
|
||||
) -> Self {
|
||||
let delta = base.time - prev.time;
|
||||
|
||||
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects
|
||||
let strain_time = delta.max(25.0);
|
||||
|
||||
let pos = base.pos;
|
||||
let travel_dist = prev.travel_dist();
|
||||
let prev_cursor_pos = prev.lazy_end_pos();
|
||||
|
||||
// We don't need to calculate either angle or distance
|
||||
// when one of the last->curr objects is a spinner
|
||||
let (jump_dist, angle) = if base.is_spinner() {
|
||||
(0.0, None)
|
||||
} else {
|
||||
let jump_dist = ((pos - prev_cursor_pos) * scaling_factor).length();
|
||||
|
||||
let angle = prev_prev.map(|prev_prev| {
|
||||
let prev_prev_cursor_pos = prev_prev.lazy_end_pos();
|
||||
|
||||
let v1 = prev_prev_cursor_pos - prev.pos;
|
||||
let v2 = pos - prev_cursor_pos;
|
||||
|
||||
let dot = v1.dot(v2);
|
||||
let det = v1.x * v2.y - v1.y * v2.x;
|
||||
|
||||
det.atan2(dot).abs()
|
||||
});
|
||||
|
||||
(jump_dist, angle)
|
||||
};
|
||||
|
||||
Self {
|
||||
base,
|
||||
prev: prev_vals,
|
||||
|
||||
jump_dist,
|
||||
travel_dist,
|
||||
angle,
|
||||
|
||||
delta,
|
||||
strain_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,337 +0,0 @@
|
||||
//! 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 generally faster than `all_included`.
|
||||
|
||||
#![cfg(feature = "no_leniency")]
|
||||
|
||||
use std::mem;
|
||||
|
||||
use self::osu_object::ObjectParameters;
|
||||
|
||||
use super::super::DifficultyAttributes;
|
||||
|
||||
mod difficulty_object;
|
||||
mod osu_object;
|
||||
mod skill;
|
||||
mod skill_kind;
|
||||
mod slider_state;
|
||||
|
||||
use difficulty_object::DifficultyObject;
|
||||
use osu_object::OsuObject;
|
||||
use skill::Skill;
|
||||
use skill_kind::SkillKind;
|
||||
use slider_state::SliderState;
|
||||
|
||||
use crate::{curve::CurveBuffers, Beatmap, Mods, Strains};
|
||||
|
||||
const OBJECT_RADIUS: f32 = 64.0;
|
||||
const SECTION_LEN: f32 = 400.0;
|
||||
const DIFFICULTY_MULTIPLIER: f32 = 0.0675;
|
||||
const NORMALIZED_RADIUS: f32 = 52.0;
|
||||
|
||||
/// Star calculation for osu!standard maps.
|
||||
///
|
||||
/// Slider paths are considered but stack leniency is ignored.
|
||||
/// As most maps don't even make use of leniency and even if,
|
||||
/// it has generally little effect on stars, the results are close to perfect.
|
||||
/// This version is considerably more efficient than `all_included` since
|
||||
/// processing stack leniency is relatively expensive.
|
||||
///
|
||||
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
|
||||
pub fn stars(
|
||||
map: &Beatmap,
|
||||
mods: impl Mods,
|
||||
passed_objects: Option<usize>,
|
||||
) -> DifficultyAttributes {
|
||||
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||
|
||||
let map_attributes = map.attributes().mods(mods);
|
||||
let hit_window = super::difficulty_range_od(map_attributes.od) / map_attributes.clock_rate;
|
||||
let od = (80.0 - hit_window) / 6.0;
|
||||
|
||||
let mut diff_attributes = DifficultyAttributes {
|
||||
ar: map_attributes.ar,
|
||||
hp: map_attributes.hp,
|
||||
od,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if take < 2 {
|
||||
return diff_attributes;
|
||||
}
|
||||
|
||||
let radius = OBJECT_RADIUS * (1.0 - 0.7 * (map_attributes.cs - 5.0) / 5.0) / 2.0;
|
||||
let mut scaling_factor = NORMALIZED_RADIUS / radius;
|
||||
|
||||
if radius < 30.0 {
|
||||
let small_circle_bonus = (30.0 - radius).min(5.0) / 50.0;
|
||||
scaling_factor *= 1.0 + small_circle_bonus;
|
||||
}
|
||||
|
||||
let mut params = ObjectParameters {
|
||||
map,
|
||||
radius,
|
||||
scaling_factor,
|
||||
attributes: &mut diff_attributes,
|
||||
slider_state: SliderState::new(map),
|
||||
ticks: Vec::new(),
|
||||
curve_bufs: CurveBuffers::default(),
|
||||
};
|
||||
|
||||
let mut hit_objects = map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.take(take)
|
||||
.filter_map(|h| OsuObject::new(h, &mut params))
|
||||
.map(|mut h| {
|
||||
h.time /= map_attributes.clock_rate;
|
||||
|
||||
h
|
||||
});
|
||||
|
||||
let fl = mods.fl();
|
||||
let mut skills = Vec::with_capacity(2 + fl as usize);
|
||||
|
||||
skills.push(Skill::new(SkillKind::Aim));
|
||||
skills.push(Skill::new(SkillKind::speed(hit_window)));
|
||||
|
||||
if fl {
|
||||
skills.push(Skill::new(SkillKind::flashlight(scaling_factor)));
|
||||
};
|
||||
|
||||
let mut prev_prev = None;
|
||||
let mut prev = hit_objects.next().unwrap();
|
||||
let mut prev_vals = None;
|
||||
|
||||
// First object has no predecessor and thus no strain, handle distinctly
|
||||
let mut current_section_end = (prev.time / SECTION_LEN).ceil() * SECTION_LEN;
|
||||
|
||||
// Handle second object separately to remove later if-branching
|
||||
let curr = hit_objects.next().unwrap();
|
||||
let h = DifficultyObject::new(&curr, &prev, prev_vals, prev_prev, scaling_factor);
|
||||
|
||||
while h.base.time > current_section_end {
|
||||
for skill in skills.iter_mut() {
|
||||
skill.start_new_section_from(current_section_end);
|
||||
}
|
||||
|
||||
current_section_end += SECTION_LEN;
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.process(&h);
|
||||
}
|
||||
|
||||
prev_prev = Some(prev);
|
||||
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||
prev = curr;
|
||||
|
||||
// Handle all other objects
|
||||
for curr in hit_objects {
|
||||
let h = DifficultyObject::new(&curr, &prev, prev_vals, prev_prev, scaling_factor);
|
||||
|
||||
while h.base.time > current_section_end {
|
||||
for skill in skills.iter_mut() {
|
||||
skill.save_current_peak();
|
||||
skill.start_new_section_from(current_section_end);
|
||||
}
|
||||
|
||||
current_section_end += SECTION_LEN;
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.process(&h);
|
||||
}
|
||||
|
||||
prev_prev = Some(prev);
|
||||
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||
prev = curr;
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.save_current_peak();
|
||||
}
|
||||
|
||||
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
|
||||
let speed_rating = if mods.rx() {
|
||||
0.0
|
||||
} else {
|
||||
skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
|
||||
};
|
||||
|
||||
let flashlight_rating = skills.get_mut(2).map_or(0.0, |skill| {
|
||||
skill.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
|
||||
});
|
||||
|
||||
let base_aim_performance = {
|
||||
let base = 5.0 * (aim_rating / 0.0675).max(1.0) - 4.0;
|
||||
|
||||
base * base * base / 100_000.0
|
||||
};
|
||||
|
||||
let base_speed_performance = {
|
||||
let base = 5.0 * (speed_rating / 0.0675).max(1.0) - 4.0;
|
||||
|
||||
base * base * base / 100_000.0
|
||||
};
|
||||
|
||||
let base_flashlight_performance = if fl {
|
||||
flashlight_rating * flashlight_rating * 25.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let base_performance = (base_aim_performance.powf(1.1)
|
||||
+ base_speed_performance.powf(1.1)
|
||||
+ base_flashlight_performance.powf(1.1))
|
||||
.powf(1.0 / 1.1);
|
||||
|
||||
let star_rating = if base_performance > 0.00001 {
|
||||
1.12_f32.cbrt()
|
||||
* 0.027
|
||||
* ((100_000.0 / (1.0_f32 / 1.1).exp2() * base_performance).cbrt() + 4.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
diff_attributes.aim_strain = aim_rating;
|
||||
diff_attributes.speed_strain = speed_rating;
|
||||
diff_attributes.flashlight_rating = flashlight_rating;
|
||||
diff_attributes.n_circles = map.n_circles as usize;
|
||||
diff_attributes.n_sliders = map.n_sliders as usize;
|
||||
diff_attributes.n_spinners = map.n_spinners as usize;
|
||||
diff_attributes.stars = star_rating;
|
||||
|
||||
diff_attributes
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
let map_attributes = map.attributes().mods(mods);
|
||||
let hit_window = super::difficulty_range_od(map_attributes.od) / map_attributes.clock_rate;
|
||||
let od = (80.0 - hit_window) / 6.0;
|
||||
|
||||
let mut diff_attributes = DifficultyAttributes {
|
||||
ar: map_attributes.ar,
|
||||
hp: map_attributes.hp,
|
||||
od,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if map.hit_objects.len() < 2 {
|
||||
return Strains::default();
|
||||
}
|
||||
|
||||
let radius = OBJECT_RADIUS * (1.0 - 0.7 * (map_attributes.cs - 5.0) / 5.0) / 2.0;
|
||||
let mut scaling_factor = NORMALIZED_RADIUS / radius;
|
||||
|
||||
if radius < 30.0 {
|
||||
let small_circle_bonus = (30.0 - radius).min(5.0) / 50.0;
|
||||
scaling_factor *= 1.0 + small_circle_bonus;
|
||||
}
|
||||
|
||||
let mut params = ObjectParameters {
|
||||
map,
|
||||
radius,
|
||||
scaling_factor,
|
||||
attributes: &mut diff_attributes,
|
||||
slider_state: SliderState::new(map),
|
||||
ticks: Vec::new(),
|
||||
curve_bufs: CurveBuffers::default(),
|
||||
};
|
||||
|
||||
let mut hit_objects = map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.filter_map(|h| OsuObject::new(h, &mut params));
|
||||
|
||||
let fl = mods.fl();
|
||||
let mut skills = Vec::with_capacity(2 + fl as usize);
|
||||
|
||||
skills.push(Skill::new(SkillKind::Aim));
|
||||
skills.push(Skill::new(SkillKind::speed(hit_window)));
|
||||
|
||||
if fl {
|
||||
skills.push(Skill::new(SkillKind::flashlight(scaling_factor)));
|
||||
};
|
||||
|
||||
let mut prev_prev = None;
|
||||
let mut prev = hit_objects.next().unwrap();
|
||||
let mut prev_vals = None;
|
||||
|
||||
// First object has no predecessor and thus no strain, handle distinctly
|
||||
let mut current_section_end = (prev.time / SECTION_LEN).ceil() * SECTION_LEN;
|
||||
|
||||
// Handle second object separately to remove later if-branching
|
||||
let curr = hit_objects.next().unwrap();
|
||||
let h = DifficultyObject::new(&curr, &prev, prev_vals, prev_prev, scaling_factor);
|
||||
|
||||
while h.base.time > current_section_end {
|
||||
for skill in skills.iter_mut() {
|
||||
skill.start_new_section_from(current_section_end);
|
||||
}
|
||||
|
||||
current_section_end += SECTION_LEN;
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.process(&h);
|
||||
}
|
||||
|
||||
prev_prev = Some(prev);
|
||||
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||
prev = curr;
|
||||
|
||||
// Handle all other objects
|
||||
for curr in hit_objects {
|
||||
let h = DifficultyObject::new(&curr, &prev, prev_vals, prev_prev, scaling_factor);
|
||||
|
||||
while h.base.time > current_section_end {
|
||||
for skill in skills.iter_mut() {
|
||||
skill.save_current_peak();
|
||||
skill.start_new_section_from(current_section_end);
|
||||
}
|
||||
|
||||
current_section_end += SECTION_LEN;
|
||||
}
|
||||
|
||||
prev_prev = Some(prev);
|
||||
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||
prev = curr;
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.save_current_peak();
|
||||
}
|
||||
|
||||
let mut speed_strains = skills.pop().unwrap().strain_peaks;
|
||||
let mut aim_strains = skills.pop().unwrap().strain_peaks;
|
||||
|
||||
let strains = if let Some(mut flashlight_strains) = skills.pop().map(|s| s.strain_peaks) {
|
||||
mem::swap(&mut speed_strains, &mut aim_strains);
|
||||
mem::swap(&mut aim_strains, &mut flashlight_strains);
|
||||
|
||||
aim_strains
|
||||
.into_iter()
|
||||
.zip(speed_strains)
|
||||
.zip(flashlight_strains)
|
||||
.map(|((aim, speed), flashlight)| aim + speed + flashlight)
|
||||
.collect()
|
||||
} else {
|
||||
aim_strains
|
||||
.into_iter()
|
||||
.zip(speed_strains)
|
||||
.map(|(aim, speed)| aim + speed)
|
||||
.collect()
|
||||
};
|
||||
|
||||
Strains {
|
||||
section_length: SECTION_LEN,
|
||||
strains,
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
use super::super::super::DifficultyAttributes;
|
||||
use super::slider_state::SliderState;
|
||||
|
||||
use crate::{
|
||||
curve::{Curve, CurveBuffers},
|
||||
parse::{HitObject, HitObjectKind, Pos2},
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
|
||||
const BASE_SCORING_DISTANCE: f32 = 100.0;
|
||||
|
||||
pub(crate) struct OsuObject {
|
||||
pub(crate) time: f32,
|
||||
pub(crate) pos: Pos2,
|
||||
pub(crate) kind: OsuObjectKind,
|
||||
}
|
||||
|
||||
pub(crate) enum OsuObjectKind {
|
||||
Circle,
|
||||
Slider {
|
||||
end_pos: Pos2,
|
||||
lazy_end_pos: Pos2,
|
||||
travel_dist: f32,
|
||||
},
|
||||
Spinner,
|
||||
}
|
||||
|
||||
pub(crate) struct ObjectParameters<'a> {
|
||||
pub(crate) map: &'a Beatmap,
|
||||
pub(crate) radius: f32,
|
||||
pub(crate) scaling_factor: f32,
|
||||
pub(crate) attributes: &'a mut DifficultyAttributes,
|
||||
pub(crate) ticks: Vec<f32>,
|
||||
pub(crate) slider_state: SliderState<'a>,
|
||||
pub(crate) curve_bufs: CurveBuffers,
|
||||
}
|
||||
|
||||
impl OsuObject {
|
||||
pub(crate) fn new(h: &HitObject, params: &mut ObjectParameters<'_>) -> Option<Self> {
|
||||
let ObjectParameters {
|
||||
map,
|
||||
radius,
|
||||
scaling_factor,
|
||||
attributes,
|
||||
ticks,
|
||||
slider_state,
|
||||
curve_bufs,
|
||||
} = params;
|
||||
|
||||
attributes.max_combo += 1; // hitcircle, slider head, or spinner
|
||||
|
||||
let obj = match &h.kind {
|
||||
HitObjectKind::Circle => Self {
|
||||
time: h.start_time,
|
||||
pos: h.pos,
|
||||
kind: OsuObjectKind::Circle,
|
||||
},
|
||||
HitObjectKind::Slider {
|
||||
pixel_len,
|
||||
repeats,
|
||||
control_points,
|
||||
} => {
|
||||
// Key values which are computed here
|
||||
let mut lazy_end_pos = h.pos;
|
||||
let mut travel_dist = 0.0;
|
||||
|
||||
// Responsible for timing point values
|
||||
slider_state.update(h.start_time);
|
||||
|
||||
let span_count = (*repeats + 1) as f32;
|
||||
|
||||
let approx_follow_circle_radius = *radius * 3.0;
|
||||
let mut tick_dist = 100.0 * map.slider_mult / map.tick_rate;
|
||||
|
||||
if map.version >= 8 {
|
||||
tick_dist /=
|
||||
(100.0 / slider_state.slider_velocity).max(10.0).min(1000.0) / 100.0;
|
||||
}
|
||||
|
||||
// Build the curve w.r.t. the curve points
|
||||
let curve = Curve::new(control_points, *pixel_len, curve_bufs);
|
||||
|
||||
let velocity =
|
||||
(BASE_SCORING_DISTANCE * map.slider_mult * slider_state.slider_velocity)
|
||||
/ slider_state.beat_len;
|
||||
|
||||
let end_time = h.start_time + span_count * curve.dist() / velocity;
|
||||
let duration = end_time - h.start_time;
|
||||
let span_duration = duration / span_count;
|
||||
|
||||
// Called on each slider object except for the head.
|
||||
// Increases combo and adjusts `end_pos` and `travel_dist`
|
||||
// w.r.t. the object position at the given time on the slider curve.
|
||||
let mut compute_vertex = |time: f32| {
|
||||
attributes.max_combo += 1;
|
||||
|
||||
let mut progress = (time - h.start_time) / span_duration;
|
||||
|
||||
if progress % 2.0 >= 1.0 {
|
||||
progress = 1.0 - progress % 1.0;
|
||||
} else {
|
||||
progress %= 1.0;
|
||||
}
|
||||
|
||||
let curr_pos = curve.position_at(progress);
|
||||
let diff = h.pos + curr_pos - lazy_end_pos;
|
||||
let mut dist = diff.length();
|
||||
|
||||
if dist > approx_follow_circle_radius {
|
||||
// * The cursor would be outside the follow circle, we need to move it
|
||||
dist -= approx_follow_circle_radius;
|
||||
lazy_end_pos += diff.normalize() * dist;
|
||||
travel_dist += dist;
|
||||
}
|
||||
};
|
||||
|
||||
let mut current_distance = tick_dist;
|
||||
let time_add = duration * (tick_dist / (pixel_len * span_count));
|
||||
|
||||
let target = pixel_len - tick_dist / 8.0;
|
||||
ticks.reserve((target / tick_dist) as usize);
|
||||
|
||||
// Tick of the first span
|
||||
if current_distance < target {
|
||||
for tick_idx in 1.. {
|
||||
let time = h.start_time + time_add * tick_idx as f32;
|
||||
compute_vertex(time);
|
||||
ticks.push(time);
|
||||
current_distance += tick_dist;
|
||||
|
||||
if current_distance >= target {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Other spans
|
||||
if *repeats > 1 {
|
||||
for repeat_id in 1..*repeats {
|
||||
let time_offset = (duration / *repeats as f32) * repeat_id as f32;
|
||||
|
||||
// Reverse tick
|
||||
compute_vertex(h.start_time + time_offset);
|
||||
|
||||
// Actual ticks
|
||||
if repeat_id & 1 == 1 {
|
||||
ticks.iter().rev().for_each(|&time| compute_vertex(time));
|
||||
} else {
|
||||
ticks.iter().for_each(|&time| compute_vertex(time));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Slider tail
|
||||
let final_span_idx = repeats.saturating_sub(1);
|
||||
let final_span_start_time = h.start_time + final_span_idx as f32 * span_duration;
|
||||
let final_span_end_time = (h.start_time + duration / 2.0)
|
||||
.max(final_span_start_time + span_duration - LEGACY_LAST_TICK_OFFSET);
|
||||
compute_vertex(final_span_end_time);
|
||||
|
||||
ticks.clear();
|
||||
|
||||
let progress = (*repeats % 2 == 0) as u8 as f32;
|
||||
let end_pos = h.pos + curve.position_at(progress);
|
||||
travel_dist *= *scaling_factor;
|
||||
|
||||
Self {
|
||||
time: h.start_time,
|
||||
pos: h.pos,
|
||||
kind: OsuObjectKind::Slider {
|
||||
end_pos,
|
||||
lazy_end_pos,
|
||||
travel_dist,
|
||||
},
|
||||
}
|
||||
}
|
||||
HitObjectKind::Spinner { .. } => Self {
|
||||
time: h.start_time,
|
||||
pos: h.pos,
|
||||
kind: OsuObjectKind::Spinner,
|
||||
},
|
||||
HitObjectKind::Hold { .. } => return None,
|
||||
};
|
||||
|
||||
Some(obj)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn end_pos(&self) -> Pos2 {
|
||||
match self.kind {
|
||||
OsuObjectKind::Circle | OsuObjectKind::Spinner => self.pos,
|
||||
OsuObjectKind::Slider { end_pos, .. } => end_pos,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn lazy_end_pos(&self) -> Pos2 {
|
||||
match self.kind {
|
||||
OsuObjectKind::Circle | OsuObjectKind::Spinner => self.pos,
|
||||
OsuObjectKind::Slider { lazy_end_pos, .. } => lazy_end_pos,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn travel_dist(&self) -> f32 {
|
||||
match self.kind {
|
||||
OsuObjectKind::Circle | OsuObjectKind::Spinner => 0.0,
|
||||
OsuObjectKind::Slider { travel_dist, .. } => travel_dist,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_slider(&self) -> bool {
|
||||
matches!(self.kind, OsuObjectKind::Slider { .. })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_spinner(&self) -> bool {
|
||||
matches!(self.kind, OsuObjectKind::Spinner)
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
use crate::math_util;
|
||||
|
||||
use super::{skill_kind::calculate_speed_rhythm_bonus, DifficultyObject, SkillKind};
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
const REDUCED_STRAIN_BASELINE: f32 = 0.75;
|
||||
|
||||
pub(crate) struct Skill {
|
||||
curr_strain: f32,
|
||||
curr_section_peak: f32,
|
||||
|
||||
kind: SkillKind,
|
||||
pub(crate) strain_peaks: Vec<f32>,
|
||||
|
||||
prev_time: Option<f32>,
|
||||
}
|
||||
|
||||
impl Skill {
|
||||
#[inline]
|
||||
pub(crate) fn new(kind: SkillKind) -> Self {
|
||||
Self {
|
||||
curr_strain: 1.0,
|
||||
curr_section_peak: 0.0,
|
||||
|
||||
kind,
|
||||
strain_peaks: Vec::with_capacity(128),
|
||||
|
||||
prev_time: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn process(&mut self, curr: &DifficultyObject<'_>) {
|
||||
self.kind.pre_process();
|
||||
self.curr_section_peak = self.strain_value_at(curr).max(self.curr_section_peak);
|
||||
self.prev_time = Some(curr.base.time);
|
||||
self.kind.post_process(curr);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn save_current_peak(&mut self) {
|
||||
self.strain_peaks.push(self.curr_section_peak);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn start_new_section_from(&mut self, time: f32) {
|
||||
// The maximum strain of the new section is not zero by default
|
||||
self.curr_section_peak = self.calculate_initial_strain(time);
|
||||
}
|
||||
|
||||
pub(crate) fn difficulty_value(&mut self) -> f32 {
|
||||
let mut difficulty = 0.0;
|
||||
let mut weight = 1.0;
|
||||
let decay_weight = self.kind.decay_weight();
|
||||
|
||||
let (reduced_section_count, difficulty_multiplier) = self.kind.difficulty_values();
|
||||
let reduced_section_count_f32 = reduced_section_count as f32;
|
||||
|
||||
self.strain_peaks
|
||||
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||
|
||||
let peaks = self.strain_peaks.iter_mut();
|
||||
|
||||
for (i, strain) in peaks.take(reduced_section_count).enumerate() {
|
||||
let clamped = (i as f32 / reduced_section_count_f32).clamp(0.0, 1.0);
|
||||
let scale = (math_util::lerp(1.0, 10.0, clamped)).log10();
|
||||
*strain *= math_util::lerp(REDUCED_STRAIN_BASELINE, 1.0, scale);
|
||||
}
|
||||
|
||||
self.strain_peaks
|
||||
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||
|
||||
for &strain in self.strain_peaks.iter() {
|
||||
difficulty += strain * weight;
|
||||
weight *= decay_weight;
|
||||
}
|
||||
|
||||
difficulty * difficulty_multiplier
|
||||
}
|
||||
|
||||
pub(crate) fn calculate_initial_strain(&self, time: f32) -> f32 {
|
||||
let prev_time = self.prev_time.unwrap_or(0.0);
|
||||
let decayed_strain = self.curr_strain * self.kind.strain_decay(time - prev_time);
|
||||
|
||||
match &self.kind {
|
||||
SkillKind::Aim | SkillKind::Flashlight { .. } => decayed_strain,
|
||||
SkillKind::Speed { curr_rhythm, .. } => curr_rhythm * decayed_strain,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn strain_value_at(&mut self, curr: &DifficultyObject<'_>) -> f32 {
|
||||
self.curr_strain *= self.kind.strain_decay(curr.delta);
|
||||
self.curr_strain += self.kind.strain_value_of(curr) * self.kind.skill_multiplier();
|
||||
|
||||
match &mut self.kind {
|
||||
SkillKind::Aim | SkillKind::Flashlight { .. } => self.curr_strain,
|
||||
SkillKind::Speed {
|
||||
curr_rhythm,
|
||||
history,
|
||||
hit_window,
|
||||
} => {
|
||||
*curr_rhythm = calculate_speed_rhythm_bonus(curr, history, *hit_window);
|
||||
|
||||
self.curr_strain * *curr_rhythm
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
use std::{collections::VecDeque, f32::consts::PI, iter};
|
||||
|
||||
use crate::{math_util, parse::Pos2};
|
||||
|
||||
use super::DifficultyObject;
|
||||
|
||||
const SINGLE_SPACING_TRESHOLD: f32 = 125.0;
|
||||
|
||||
const MIN_SPEED_BONUS: f32 = 75.0;
|
||||
const SPEED_BALANCING_FACTOR: f32 = 40.0;
|
||||
|
||||
const TIMING_THRESHOLD: f32 = 107.0;
|
||||
|
||||
const AIM_SKILL_MULTIPLIER: f32 = 26.25;
|
||||
const AIM_STRAIN_DECAY_BASE: f32 = 0.15;
|
||||
const AIM_DECAY_WEIGHT: f32 = 0.9;
|
||||
const AIM_DIFFICULTY_MULTIPLIER: f32 = 1.06;
|
||||
const AIM_REDUCED_SECTION_COUNT: usize = 10;
|
||||
|
||||
const AIM_ANGLE_BONUS_BEGIN: f32 = std::f32::consts::FRAC_PI_3;
|
||||
|
||||
const SPEED_SKILL_MULTIPLIER: f32 = 1375.0;
|
||||
const SPEED_STRAIN_DECAY_BASE: f32 = 0.3;
|
||||
const SPEED_DECAY_WEIGHT: f32 = 0.9;
|
||||
const SPEED_DIFFICULTY_MULTIPLIER: f32 = 1.04;
|
||||
const SPEED_REDUCED_SECTION_COUNT: usize = 5;
|
||||
|
||||
const SPEED_HISTORY_LENGTH: usize = 32;
|
||||
const SPEED_HISTORY_TIME_MAX: f32 = 5000.0;
|
||||
const SPEED_RHYTHM_MULTIPLIER: f32 = 0.75;
|
||||
|
||||
const FLASHLIGHT_SKILL_MULTIPLIER: f32 = 0.15;
|
||||
const FLASHLIGHT_STRAIN_DECAY_BASE: f32 = 0.15;
|
||||
const FLASHLIGHT_DECAY_WEIGHT: f32 = 1.0;
|
||||
const FLASHLIGHT_DIFFICULTY_MULTIPLIER: f32 = 1.06;
|
||||
const FLASHLIGHT_REDUCED_SECTION_COUNT: usize = 10;
|
||||
|
||||
const FLASHLIGHT_HISTORY_LENGTH: usize = 10;
|
||||
|
||||
pub(crate) struct FlashlightHistoryEntry {
|
||||
end_pos: Pos2,
|
||||
is_spinner: bool,
|
||||
jump_dist: f32,
|
||||
strain_time: f32,
|
||||
}
|
||||
|
||||
impl From<&DifficultyObject<'_>> for FlashlightHistoryEntry {
|
||||
fn from(h: &DifficultyObject<'_>) -> Self {
|
||||
Self {
|
||||
end_pos: h.base.end_pos(),
|
||||
is_spinner: h.base.is_spinner(),
|
||||
jump_dist: h.jump_dist,
|
||||
strain_time: h.strain_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SpeedHistoryEntry {
|
||||
is_slider: bool,
|
||||
start_time: f32,
|
||||
strain_time: f32,
|
||||
}
|
||||
|
||||
impl From<&DifficultyObject<'_>> for SpeedHistoryEntry {
|
||||
fn from(h: &DifficultyObject<'_>) -> Self {
|
||||
Self {
|
||||
is_slider: h.base.is_slider(),
|
||||
start_time: h.base.time,
|
||||
strain_time: h.strain_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum SkillKind {
|
||||
Aim,
|
||||
Flashlight {
|
||||
history: VecDeque<FlashlightHistoryEntry>,
|
||||
scaling_factor: f32,
|
||||
},
|
||||
Speed {
|
||||
curr_rhythm: f32,
|
||||
history: VecDeque<SpeedHistoryEntry>,
|
||||
hit_window: f32,
|
||||
},
|
||||
}
|
||||
|
||||
impl SkillKind {
|
||||
pub(crate) fn flashlight(scaling_factor: f32) -> Self {
|
||||
Self::Flashlight {
|
||||
history: VecDeque::with_capacity(FLASHLIGHT_HISTORY_LENGTH),
|
||||
scaling_factor,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn speed(hit_window: f32) -> Self {
|
||||
Self::Speed {
|
||||
curr_rhythm: 1.0,
|
||||
history: VecDeque::with_capacity(SPEED_HISTORY_LENGTH),
|
||||
hit_window,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pre_process(&mut self) {
|
||||
match self {
|
||||
Self::Aim => {}
|
||||
Self::Flashlight { history, .. } => history.truncate(FLASHLIGHT_HISTORY_LENGTH),
|
||||
Self::Speed { history, .. } => history.truncate(SPEED_HISTORY_LENGTH),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn post_process(&mut self, current: &DifficultyObject<'_>) {
|
||||
match self {
|
||||
Self::Aim => {}
|
||||
Self::Flashlight { history, .. } => history.push_front(current.into()),
|
||||
Self::Speed { history, .. } => history.push_front(current.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn strain_value_of(&self, curr: &DifficultyObject<'_>) -> f32 {
|
||||
match self {
|
||||
Self::Aim => {
|
||||
if curr.base.is_spinner() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut aim_strain = 0.0;
|
||||
|
||||
if let Some((prev_jump_dist, prev_strain_time)) = curr.prev {
|
||||
if let Some(angle) = curr.angle.filter(|a| *a > AIM_ANGLE_BONUS_BEGIN) {
|
||||
let scale = 90.0;
|
||||
|
||||
let angle_bonus = (((angle - AIM_ANGLE_BONUS_BEGIN).sin()).powi(2)
|
||||
* (prev_jump_dist - scale).max(0.0)
|
||||
* (curr.jump_dist - scale).max(0.0))
|
||||
.sqrt();
|
||||
|
||||
aim_strain = 1.4 * apply_diminishing_exp(angle_bonus.max(0.0))
|
||||
/ (TIMING_THRESHOLD).max(prev_strain_time)
|
||||
}
|
||||
}
|
||||
|
||||
let jump_dist_exp = apply_diminishing_exp(curr.jump_dist);
|
||||
let travel_dist_exp = apply_diminishing_exp(curr.travel_dist);
|
||||
|
||||
let dist_exp =
|
||||
jump_dist_exp + travel_dist_exp + (travel_dist_exp * jump_dist_exp).sqrt();
|
||||
|
||||
(aim_strain + dist_exp / (curr.strain_time).max(TIMING_THRESHOLD))
|
||||
.max(dist_exp / curr.strain_time)
|
||||
}
|
||||
Self::Flashlight {
|
||||
history,
|
||||
scaling_factor,
|
||||
} => {
|
||||
if curr.base.is_spinner() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut small_dist_nerf = 1.0;
|
||||
let mut result = 0.0;
|
||||
let mut cumulative_strain_time = 0.0;
|
||||
let mut history_iter = history.iter();
|
||||
|
||||
if let Some(prev) = history_iter.next() {
|
||||
// Handle first entry distinctly for slight optimization
|
||||
if !prev.is_spinner {
|
||||
let jump_dist = (curr.base.pos - prev.end_pos).length();
|
||||
cumulative_strain_time += prev.strain_time;
|
||||
|
||||
// We want to nerf objects that can be easily seen within the Flashlight circle radius
|
||||
small_dist_nerf = (jump_dist / 75.0).min(1.0);
|
||||
|
||||
// We also want to nerf stacks so that only the first object of the stack is accounted for
|
||||
// -- since jump distance is 0 on stacked notes in this version, approximate value as 0.2
|
||||
let stack_nerf =
|
||||
((prev.jump_dist / scaling_factor) / 25.0).min(1.0).max(0.2);
|
||||
|
||||
result += stack_nerf * scaling_factor * jump_dist / cumulative_strain_time;
|
||||
}
|
||||
|
||||
let factors = iter::successors(Some(0.8), |s| Some(s * 0.8));
|
||||
|
||||
for (factor, prev) in factors.zip(history_iter) {
|
||||
if !prev.is_spinner {
|
||||
let jump_dist = (curr.base.pos - prev.end_pos).length();
|
||||
cumulative_strain_time += prev.strain_time;
|
||||
let stack_nerf =
|
||||
((prev.jump_dist / scaling_factor) / 25.0).min(1.0).max(0.2);
|
||||
|
||||
result += factor * stack_nerf * scaling_factor * jump_dist
|
||||
/ cumulative_strain_time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result *= small_dist_nerf;
|
||||
|
||||
result * result
|
||||
}
|
||||
Self::Speed {
|
||||
history,
|
||||
hit_window,
|
||||
..
|
||||
} => {
|
||||
if curr.base.is_spinner() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut strain_time = curr.strain_time;
|
||||
let hit_window_full = hit_window * 2.0;
|
||||
let speed_window_ratio = strain_time / hit_window_full;
|
||||
let prev = history.front();
|
||||
|
||||
// Aim to nerf cheesy rhythms (very fast consecutive doubles with large delta times between)
|
||||
if let Some(prev) =
|
||||
prev.filter(|p| strain_time < hit_window_full && p.strain_time > strain_time)
|
||||
{
|
||||
strain_time =
|
||||
math_util::lerp(prev.strain_time, strain_time, speed_window_ratio);
|
||||
}
|
||||
|
||||
// Cap delta time to the OD 300 hit window
|
||||
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly,
|
||||
// whilst 0.92 limits the effect of the cap
|
||||
strain_time /= (strain_time / hit_window_full / 0.93).clamp(0.92, 1.0);
|
||||
|
||||
// Derive speed bonus for calculation
|
||||
let mut speed_bonus = 1.0;
|
||||
|
||||
if strain_time < MIN_SPEED_BONUS {
|
||||
let base = (MIN_SPEED_BONUS - strain_time) / SPEED_BALANCING_FACTOR;
|
||||
speed_bonus = 1.0 + 0.75 * base * base;
|
||||
}
|
||||
|
||||
let dist = SINGLE_SPACING_TRESHOLD.min(curr.travel_dist + curr.jump_dist);
|
||||
|
||||
(speed_bonus + speed_bonus * (dist / SINGLE_SPACING_TRESHOLD).powf(3.5))
|
||||
/ strain_time
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn difficulty_values(&self) -> (usize, f32) {
|
||||
match self {
|
||||
Self::Aim => (AIM_REDUCED_SECTION_COUNT, AIM_DIFFICULTY_MULTIPLIER),
|
||||
Self::Flashlight { .. } => (
|
||||
FLASHLIGHT_REDUCED_SECTION_COUNT,
|
||||
FLASHLIGHT_DIFFICULTY_MULTIPLIER,
|
||||
),
|
||||
Self::Speed { .. } => (SPEED_REDUCED_SECTION_COUNT, SPEED_DIFFICULTY_MULTIPLIER),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn skill_multiplier(&self) -> f32 {
|
||||
match self {
|
||||
SkillKind::Aim => AIM_SKILL_MULTIPLIER,
|
||||
SkillKind::Flashlight { .. } => FLASHLIGHT_SKILL_MULTIPLIER,
|
||||
SkillKind::Speed { .. } => SPEED_SKILL_MULTIPLIER,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn strain_decay_base(&self) -> f32 {
|
||||
match self {
|
||||
SkillKind::Aim => AIM_STRAIN_DECAY_BASE,
|
||||
SkillKind::Flashlight { .. } => FLASHLIGHT_STRAIN_DECAY_BASE,
|
||||
SkillKind::Speed { .. } => SPEED_STRAIN_DECAY_BASE,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn decay_weight(&self) -> f32 {
|
||||
match self {
|
||||
SkillKind::Aim => AIM_DECAY_WEIGHT,
|
||||
SkillKind::Flashlight { .. } => FLASHLIGHT_DECAY_WEIGHT,
|
||||
SkillKind::Speed { .. } => SPEED_DECAY_WEIGHT,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn strain_decay(&self, ms: f32) -> f32 {
|
||||
self.strain_decay_base().powf(ms / 1000.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn calculate_speed_rhythm_bonus(
|
||||
current: &DifficultyObject<'_>,
|
||||
history: &VecDeque<SpeedHistoryEntry>,
|
||||
hit_window: f32,
|
||||
) -> f32 {
|
||||
if current.base.is_spinner() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut prev_island_size = 0;
|
||||
let mut rhythm_complexity_sum = 0.0;
|
||||
let mut island_size = 1;
|
||||
let mut first_delta_switch = false;
|
||||
let adjusted_hit_window = hit_window * 0.6;
|
||||
let history_len = history.len() as f32;
|
||||
|
||||
// Store the ratio of the current start of an island to buff for tighter rhythms
|
||||
let mut start_ratio = 0.0;
|
||||
|
||||
let currs = history.iter();
|
||||
let prevs = history.iter().skip(1);
|
||||
let lasts = history.iter().skip(2);
|
||||
|
||||
for (((prev, curr), last), i) in prevs.zip(currs).zip(lasts).rev().zip(2..) {
|
||||
let mut curr_historical_decay =
|
||||
(SPEED_HISTORY_TIME_MAX - (current.base.time - curr.start_time)).max(0.0)
|
||||
/ SPEED_HISTORY_TIME_MAX;
|
||||
|
||||
if curr_historical_decay.abs() > f32::EPSILON {
|
||||
// Either we're limited by time or limited by object count
|
||||
curr_historical_decay = curr_historical_decay.min(i as f32 / history_len);
|
||||
|
||||
let curr_delta = curr.strain_time;
|
||||
let prev_delta = prev.strain_time;
|
||||
let last_delta = last.strain_time;
|
||||
|
||||
// Fancy function to calculate rhythm bonuses
|
||||
let base = (PI / (prev_delta.min(curr_delta) / prev_delta.max(curr_delta))).sin();
|
||||
let curr_ratio = 1.0 + 6.0 * (base * base).min(0.5);
|
||||
|
||||
let lower_penalty = ((prev_delta - curr_delta).abs() - adjusted_hit_window).max(0.0);
|
||||
let window_penalty = (lower_penalty / adjusted_hit_window).min(1.0);
|
||||
|
||||
let mut effective_ratio = window_penalty * curr_ratio;
|
||||
|
||||
if first_delta_switch {
|
||||
if !(prev_delta > 1.25 * curr_delta || prev_delta * 1.25 < curr_delta) {
|
||||
if island_size < 7 {
|
||||
island_size += 1;
|
||||
}
|
||||
} else {
|
||||
if curr.is_slider {
|
||||
// bpm change is into slider, this is easy acc window
|
||||
effective_ratio *= 0.125;
|
||||
}
|
||||
|
||||
if prev.is_slider {
|
||||
// bpm change was from a slider, this is easier typically than circle -> circle
|
||||
effective_ratio *= 0.25;
|
||||
}
|
||||
|
||||
if prev_island_size == island_size {
|
||||
// repeated island size (ex: triplet -> triplet)
|
||||
effective_ratio *= 0.25;
|
||||
}
|
||||
|
||||
if prev_island_size % 2 == island_size % 2 {
|
||||
// repeated island polarity (2 -> 4, 3 -> 5)
|
||||
effective_ratio *= 0.5;
|
||||
}
|
||||
|
||||
if last_delta > prev_delta + 10.0 && prev_delta > curr_delta + 10.0 {
|
||||
// previous increase happened a note ago, 1/1 -> 1/2-1/4, don't want to buff this
|
||||
effective_ratio *= 0.125;
|
||||
}
|
||||
|
||||
rhythm_complexity_sum += (effective_ratio * start_ratio).sqrt()
|
||||
* curr_historical_decay
|
||||
* ((4 + island_size) as f32).sqrt()
|
||||
* ((4 + prev_island_size) as f32).sqrt()
|
||||
/ 4.0;
|
||||
|
||||
start_ratio = effective_ratio;
|
||||
prev_island_size = island_size;
|
||||
island_size = 1;
|
||||
|
||||
// we're slowing down, stop counting
|
||||
if prev_delta * 1.25 < curr_delta {
|
||||
// if we're speeding up, this stays true and we keep counting island size
|
||||
first_delta_switch = false;
|
||||
}
|
||||
}
|
||||
} else if prev_delta > 1.25 * curr_delta {
|
||||
// we want to be speeding up
|
||||
// begin counting island until we change speed again
|
||||
first_delta_switch = true;
|
||||
start_ratio = effective_ratio;
|
||||
island_size = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
||||
(4.0 + rhythm_complexity_sum * SPEED_RHYTHM_MULTIPLIER).sqrt() / 2.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn apply_diminishing_exp(val: f32) -> f32 {
|
||||
val.powf(0.99)
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
use crate::{Beatmap, ControlPoint, ControlPointIter};
|
||||
|
||||
pub(crate) struct SliderState<'p> {
|
||||
control_points: ControlPointIter<'p>,
|
||||
next: Option<ControlPoint>,
|
||||
pub(crate) beat_len: f32,
|
||||
pub(crate) slider_velocity: f32,
|
||||
}
|
||||
|
||||
impl<'p> SliderState<'p> {
|
||||
#[inline]
|
||||
pub(crate) fn new(map: &'p Beatmap) -> Self {
|
||||
let mut control_points = ControlPointIter::new(map);
|
||||
|
||||
let (beat_len, slider_velocity) = match control_points.next() {
|
||||
Some(ControlPoint::Timing { beat_len, .. }) => (beat_len, 1.0),
|
||||
Some(ControlPoint::Difficulty {
|
||||
slider_velocity: speed_mult,
|
||||
..
|
||||
}) => (1000.0, speed_mult),
|
||||
None => (1000.0, 1.0),
|
||||
};
|
||||
|
||||
Self {
|
||||
next: control_points.next(),
|
||||
control_points,
|
||||
beat_len,
|
||||
slider_velocity,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn update(&mut self, time: f32) {
|
||||
while let Some(next) = self.next.as_ref().filter(|n| time >= n.time()) {
|
||||
match next {
|
||||
ControlPoint::Timing { beat_len, .. } => {
|
||||
self.beat_len = *beat_len;
|
||||
self.slider_velocity = 1.0;
|
||||
}
|
||||
ControlPoint::Difficulty {
|
||||
slider_velocity: speed_mult,
|
||||
..
|
||||
} => self.slider_velocity = *speed_mult,
|
||||
}
|
||||
|
||||
self.next = self.control_points.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
parse::{DifficultyPoint, TimingPoint},
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
use super::SliderState;
|
||||
|
||||
#[test]
|
||||
fn osu_slider_state() {
|
||||
let map = Beatmap {
|
||||
timing_points: vec![
|
||||
TimingPoint {
|
||||
time: 1.0,
|
||||
beat_len: 10.0,
|
||||
},
|
||||
TimingPoint {
|
||||
time: 3.0,
|
||||
beat_len: 20.0,
|
||||
},
|
||||
TimingPoint {
|
||||
time: 4.0,
|
||||
beat_len: 30.0,
|
||||
},
|
||||
],
|
||||
difficulty_points: vec![
|
||||
DifficultyPoint {
|
||||
time: 2.0,
|
||||
speed_multiplier: 15.0,
|
||||
},
|
||||
DifficultyPoint {
|
||||
time: 5.0,
|
||||
speed_multiplier: 45.0,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut state = SliderState::new(&map);
|
||||
|
||||
state.update(2.0);
|
||||
assert!((state.beat_len - 10.0).abs() <= f32::EPSILON);
|
||||
|
||||
state.update(3.0);
|
||||
assert!((state.beat_len - 20.0).abs() <= f32::EPSILON);
|
||||
assert!((state.slider_velocity - 1.0).abs() <= f32::EPSILON);
|
||||
|
||||
state.update(5.0);
|
||||
assert!((state.beat_len - 30.0).abs() <= f32::EPSILON);
|
||||
assert!((state.slider_velocity - 45.0).abs() <= f32::EPSILON);
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -108,7 +108,7 @@ macro_rules! parse_general_body {
|
||||
let mut mode = None;
|
||||
let mut empty = true;
|
||||
|
||||
#[cfg(all(feature = "osu", feature = "all_included"))]
|
||||
#[cfg(all(feature = "osu", feature = "osu_precise"))]
|
||||
let mut stack_leniency = None;
|
||||
|
||||
while read_line!($reader, $buf)? != 0 {
|
||||
@@ -133,7 +133,7 @@ macro_rules! parse_general_body {
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "osu", feature = "all_included"))]
|
||||
#[cfg(all(feature = "osu", feature = "osu_precise"))]
|
||||
if key == "StackLeniency" {
|
||||
stack_leniency = Some(value.parse()?);
|
||||
}
|
||||
@@ -163,7 +163,7 @@ macro_rules! parse_general_body {
|
||||
return Err(ParseError::UnincludedMode(GameMode::MNA));
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "osu", feature = "all_included"))]
|
||||
#[cfg(all(feature = "osu", feature = "osu_precise"))]
|
||||
{
|
||||
$self.stack_leniency = stack_leniency.unwrap_or(0.7);
|
||||
}
|
||||
@@ -397,15 +397,15 @@ macro_rules! parse_hitobjects_body {
|
||||
let mut prev_time = 0.0;
|
||||
let mut empty = true;
|
||||
|
||||
#[cfg(feature = "sliders")]
|
||||
// `point_split` will be of type `Vec<&str>
|
||||
// with each element having its lifetime bound to `buf`.
|
||||
// To cirvumvent this, `point_split_raw` will contain
|
||||
// To circumvent this, `point_split_raw` will contain
|
||||
// the actual `&str` elements transmuted into `usize`.
|
||||
#[cfg(feature = "sliders")]
|
||||
let mut point_split_raw: Vec<usize> = Vec::new();
|
||||
|
||||
#[cfg(feature = "sliders")]
|
||||
// Buffer to re-use for all sliders
|
||||
#[cfg(feature = "sliders")]
|
||||
let mut vertices = Vec::new();
|
||||
|
||||
while read_line!($reader, $buf)? != 0 {
|
||||
@@ -723,7 +723,7 @@ pub struct Beatmap {
|
||||
#[cfg(any(feature = "osu", feature = "fruits"))]
|
||||
pub difficulty_points: Vec<DifficultyPoint>,
|
||||
|
||||
#[cfg(all(feature = "osu", feature = "all_included"))]
|
||||
#[cfg(all(feature = "osu", feature = "osu_precise"))]
|
||||
pub stack_leniency: f32,
|
||||
}
|
||||
|
||||
@@ -1042,7 +1042,7 @@ mod tests {
|
||||
|
||||
#[cfg(any(feature = "osu", feature = "fruits"))]
|
||||
{
|
||||
#[cfg(feature = "all_included")]
|
||||
#[cfg(feature = "osu_precise")]
|
||||
println!("stack_leniency: {}", map.stack_leniency);
|
||||
|
||||
println!("timing_points: {}", map.timing_points.len());
|
||||
|
||||
Reference in New Issue
Block a user