return DifficultyAttributes instead of StarResult
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
- added internal binary crate `pp-gen` to calculate difficulty & pp values via `PerformanceCalculator.dll`
|
- added internal binary crate `pp-gen` to calculate difficulty & pp values via `PerformanceCalculator.dll`
|
||||||
- [BREAKING] Instead of returning `PpResult`, performance calculations now return `PerformanceAttributes` depending on the mode.
|
- [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.
|
||||||
- osu: Updated up to commit [6944151486e677bfd11f2390163aca9161defbbf](https://github.com/ppy/osu/commit/6944151486e677bfd11f2390163aca9161defbbf) (2021-10-27)
|
- osu: Updated up to commit [6944151486e677bfd11f2390163aca9161defbbf](https://github.com/ppy/osu/commit/6944151486e677bfd11f2390163aca9161defbbf) (2021-10-27)
|
||||||
|
|
||||||
# v0.2.3
|
# v0.2.3
|
||||||
|
|||||||
+9
-7
@@ -15,7 +15,7 @@ use slider_state::SliderState;
|
|||||||
use crate::{
|
use crate::{
|
||||||
curve::Curve,
|
curve::Curve,
|
||||||
parse::{HitObjectKind, Pos2},
|
parse::{HitObjectKind, Pos2},
|
||||||
Beatmap, Mods, StarResult, Strains,
|
Beatmap, Mods, Strains,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SECTION_LENGTH: f32 = 750.0;
|
const SECTION_LENGTH: f32 = 750.0;
|
||||||
@@ -30,9 +30,13 @@ const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
|
|||||||
///
|
///
|
||||||
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
|
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
|
||||||
// Slider parsing based on https://github.com/osufx/catch-the-pp
|
// Slider parsing based on https://github.com/osufx/catch-the-pp
|
||||||
pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> StarResult {
|
pub fn stars(
|
||||||
|
map: &Beatmap,
|
||||||
|
mods: impl Mods,
|
||||||
|
passed_objects: Option<usize>,
|
||||||
|
) -> DifficultyAttributes {
|
||||||
if map.hit_objects.len() < 2 {
|
if map.hit_objects.len() < 2 {
|
||||||
return StarResult::Fruits(DifficultyAttributes::default());
|
return DifficultyAttributes::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
let take = passed_objects.unwrap_or(usize::MAX);
|
let take = passed_objects.unwrap_or(usize::MAX);
|
||||||
@@ -255,16 +259,14 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
|||||||
|
|
||||||
let stars = movement.difficulty_value().sqrt() * STAR_SCALING_FACTOR;
|
let stars = movement.difficulty_value().sqrt() * STAR_SCALING_FACTOR;
|
||||||
|
|
||||||
let attributes = DifficultyAttributes {
|
DifficultyAttributes {
|
||||||
stars,
|
stars,
|
||||||
ar: attributes.ar,
|
ar: attributes.ar,
|
||||||
n_fruits: fruits,
|
n_fruits: fruits,
|
||||||
n_droplets: droplets,
|
n_droplets: droplets,
|
||||||
n_tiny_droplets: tiny_droplets,
|
n_tiny_droplets: tiny_droplets,
|
||||||
max_combo: fruits + droplets,
|
max_combo: fruits + droplets,
|
||||||
};
|
}
|
||||||
|
|
||||||
StarResult::Fruits(attributes)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Essentially the same as the `stars` function but instead of
|
/// Essentially the same as the `stars` function but instead of
|
||||||
|
|||||||
+59
-31
@@ -186,7 +186,7 @@ impl<'m> FruitsPP<'m> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_hitresults(&mut self, attributes: &DifficultyAttributes) {
|
fn assert_hitresults(self, attributes: DifficultyAttributes) -> FruitsPPInner {
|
||||||
let correct_combo_hits = self
|
let correct_combo_hits = self
|
||||||
.n_fruits
|
.n_fruits
|
||||||
.and_then(|f| self.n_droplets.map(|d| f + d + self.n_misses))
|
.and_then(|f| self.n_droplets.map(|d| f + d + self.n_misses))
|
||||||
@@ -232,25 +232,56 @@ impl<'m> FruitsPP<'m> {
|
|||||||
.saturating_sub(n_tiny_droplets)
|
.saturating_sub(n_tiny_droplets)
|
||||||
.saturating_sub(n_tiny_droplet_misses);
|
.saturating_sub(n_tiny_droplet_misses);
|
||||||
|
|
||||||
self.n_fruits.replace(n_fruits);
|
return FruitsPPInner {
|
||||||
self.n_droplets.replace(n_droplets);
|
attributes,
|
||||||
self.n_tiny_droplets.replace(n_tiny_droplets);
|
mods: self.mods,
|
||||||
self.n_tiny_droplet_misses.replace(n_tiny_droplet_misses);
|
combo: self.combo,
|
||||||
|
n_fruits,
|
||||||
|
n_droplets,
|
||||||
|
n_tiny_droplets,
|
||||||
|
n_tiny_droplet_misses,
|
||||||
|
n_misses: self.n_misses,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
FruitsPPInner {
|
||||||
|
attributes,
|
||||||
|
mods: self.mods,
|
||||||
|
combo: self.combo,
|
||||||
|
n_fruits: self.n_fruits.unwrap_or(0),
|
||||||
|
n_droplets: self.n_droplets.unwrap_or(0),
|
||||||
|
n_tiny_droplets: self.n_tiny_droplets.unwrap_or(0),
|
||||||
|
n_tiny_droplet_misses: self.n_tiny_droplet_misses.unwrap_or(0),
|
||||||
|
n_misses: self.n_misses,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns an object which contains the pp and [`DifficultyAttributes`](crate::fruits::DifficultyAttributes)
|
/// Returns an object which contains the pp and [`DifficultyAttributes`](crate::fruits::DifficultyAttributes)
|
||||||
/// containing stars and other attributes.
|
/// containing stars and other attributes.
|
||||||
pub fn calculate(mut self) -> PerformanceAttributes {
|
pub fn calculate(mut self) -> PerformanceAttributes {
|
||||||
let attributes = self.attributes.take().unwrap_or_else(|| {
|
let attributes = self
|
||||||
stars(self.map, self.mods, self.passed_objects)
|
.attributes
|
||||||
.attributes()
|
.take()
|
||||||
.unwrap()
|
.unwrap_or_else(|| stars(self.map, self.mods, self.passed_objects));
|
||||||
});
|
|
||||||
|
|
||||||
// Make sure all objects are set
|
self.assert_hitresults(attributes).calculate()
|
||||||
self.assert_hitresults(&attributes);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FruitsPPInner {
|
||||||
|
attributes: DifficultyAttributes,
|
||||||
|
mods: u32,
|
||||||
|
combo: Option<usize>,
|
||||||
|
n_fruits: usize,
|
||||||
|
n_droplets: usize,
|
||||||
|
n_tiny_droplets: usize,
|
||||||
|
n_tiny_droplet_misses: usize,
|
||||||
|
n_misses: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FruitsPPInner {
|
||||||
|
fn calculate(self) -> PerformanceAttributes {
|
||||||
|
let attributes = &self.attributes;
|
||||||
let stars = attributes.stars;
|
let stars = attributes.stars;
|
||||||
|
|
||||||
// Relying heavily on aim
|
// Relying heavily on aim
|
||||||
@@ -310,24 +341,25 @@ impl<'m> FruitsPP<'m> {
|
|||||||
pp *= 0.9;
|
pp *= 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
PerformanceAttributes { attributes, pp }
|
PerformanceAttributes {
|
||||||
|
attributes: self.attributes,
|
||||||
|
pp,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn combo_hits(&self) -> usize {
|
fn combo_hits(&self) -> usize {
|
||||||
self.n_fruits.unwrap_or(0) + self.n_droplets.unwrap_or(0) + self.n_misses
|
self.n_fruits + self.n_droplets + self.n_misses
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn successful_hits(&self) -> usize {
|
fn successful_hits(&self) -> usize {
|
||||||
self.n_fruits.unwrap_or(0)
|
self.n_fruits + self.n_droplets + self.n_tiny_droplets
|
||||||
+ self.n_droplets.unwrap_or(0)
|
|
||||||
+ self.n_tiny_droplets.unwrap_or(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn total_hits(&self) -> usize {
|
fn total_hits(&self) -> usize {
|
||||||
self.successful_hits() + self.n_tiny_droplet_misses.unwrap_or(0) + self.n_misses
|
self.successful_hits() + self.n_tiny_droplet_misses + self.n_misses
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -481,42 +513,38 @@ mod test {
|
|||||||
let n_tiny_droplet_misses = 20;
|
let n_tiny_droplet_misses = 20;
|
||||||
let n_misses = 2;
|
let n_misses = 2;
|
||||||
|
|
||||||
let mut calculator = FruitsPP::new(&map)
|
let calculator = FruitsPP::new(&map)
|
||||||
.attributes(attributes.clone())
|
.attributes(attributes.clone())
|
||||||
.passed_objects(total_objects)
|
.passed_objects(total_objects)
|
||||||
.fruits(n_fruits)
|
.fruits(n_fruits)
|
||||||
.droplets(n_droplets)
|
.droplets(n_droplets)
|
||||||
.tiny_droplets(n_tiny_droplets)
|
.tiny_droplets(n_tiny_droplets)
|
||||||
.tiny_droplet_misses(n_tiny_droplet_misses)
|
.tiny_droplet_misses(n_tiny_droplet_misses)
|
||||||
.misses(n_misses);
|
.misses(n_misses)
|
||||||
|
.assert_hitresults(attributes.clone());
|
||||||
calculator.assert_hitresults(&attributes);
|
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
(attributes.n_fruits as i32 - calculator.n_fruits.unwrap() as i32).abs()
|
(attributes.n_fruits as i32 - calculator.n_fruits as i32).abs() <= n_misses as i32,
|
||||||
<= n_misses as i32,
|
|
||||||
"Expected: {} | Actual: {} [+/- {} misses]",
|
"Expected: {} | Actual: {} [+/- {} misses]",
|
||||||
attributes.n_fruits,
|
attributes.n_fruits,
|
||||||
calculator.n_fruits.unwrap(),
|
calculator.n_fruits,
|
||||||
n_misses
|
n_misses
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
attributes.n_droplets,
|
attributes.n_droplets,
|
||||||
calculator.n_droplets.unwrap()
|
calculator.n_droplets - (n_misses - (attributes.n_fruits - calculator.n_fruits)),
|
||||||
- (n_misses - (attributes.n_fruits - calculator.n_fruits.unwrap())),
|
|
||||||
"Expected: {} | Actual: {}",
|
"Expected: {} | Actual: {}",
|
||||||
attributes.n_droplets,
|
attributes.n_droplets,
|
||||||
calculator.n_droplets.unwrap()
|
calculator.n_droplets - (n_misses - (attributes.n_fruits - calculator.n_fruits)),
|
||||||
- (n_misses - (attributes.n_fruits - calculator.n_fruits.unwrap())),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
attributes.n_tiny_droplets,
|
attributes.n_tiny_droplets,
|
||||||
calculator.n_tiny_droplets.unwrap() + calculator.n_tiny_droplet_misses.unwrap(),
|
calculator.n_tiny_droplets + calculator.n_tiny_droplet_misses,
|
||||||
"Expected: {} | Actual: {}",
|
"Expected: {} | Actual: {}",
|
||||||
attributes.n_tiny_droplets,
|
attributes.n_tiny_droplets,
|
||||||
calculator.n_tiny_droplets.unwrap() + calculator.n_tiny_droplet_misses.unwrap(),
|
calculator.n_tiny_droplets + calculator.n_tiny_droplet_misses,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-6
@@ -215,12 +215,16 @@ impl BeatmapExt for Beatmap {
|
|||||||
{
|
{
|
||||||
#[cfg(feature = "no_leniency")]
|
#[cfg(feature = "no_leniency")]
|
||||||
{
|
{
|
||||||
osu::no_leniency::stars(self, mods, passed_objects)
|
StarResult::Osu(osu::no_leniency::stars(self, mods, passed_objects))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(not(feature = "no_leniency"), feature = "no_sliders_no_leniency"))]
|
#[cfg(all(not(feature = "no_leniency"), feature = "no_sliders_no_leniency"))]
|
||||||
{
|
{
|
||||||
osu::no_sliders_no_leniency::stars(self, mods, passed_objects)
|
StarResult::Osu(osu::no_sliders_no_leniency::stars(
|
||||||
|
self,
|
||||||
|
mods,
|
||||||
|
passed_objects,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(
|
#[cfg(all(
|
||||||
@@ -229,7 +233,7 @@ impl BeatmapExt for Beatmap {
|
|||||||
feature = "all_included"
|
feature = "all_included"
|
||||||
))]
|
))]
|
||||||
{
|
{
|
||||||
osu::all_included::stars(self, mods, passed_objects)
|
StarResult::Osu(osu::all_included::stars(self, mods, passed_objects))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(any(
|
#[cfg(not(any(
|
||||||
@@ -245,21 +249,21 @@ impl BeatmapExt for Beatmap {
|
|||||||
panic!("`mania` feature is not enabled");
|
panic!("`mania` feature is not enabled");
|
||||||
|
|
||||||
#[cfg(feature = "mania")]
|
#[cfg(feature = "mania")]
|
||||||
mania::stars(self, mods, passed_objects)
|
StarResult::Mania(mania::stars(self, mods, passed_objects))
|
||||||
}
|
}
|
||||||
GameMode::TKO => {
|
GameMode::TKO => {
|
||||||
#[cfg(not(feature = "taiko"))]
|
#[cfg(not(feature = "taiko"))]
|
||||||
panic!("`osu` feature is not enabled");
|
panic!("`osu` feature is not enabled");
|
||||||
|
|
||||||
#[cfg(feature = "taiko")]
|
#[cfg(feature = "taiko")]
|
||||||
taiko::stars(self, mods, passed_objects)
|
StarResult::Taiko(taiko::stars(self, mods, passed_objects))
|
||||||
}
|
}
|
||||||
GameMode::CTB => {
|
GameMode::CTB => {
|
||||||
#[cfg(not(feature = "fruits"))]
|
#[cfg(not(feature = "fruits"))]
|
||||||
panic!("`fruits` feature is not enabled");
|
panic!("`fruits` feature is not enabled");
|
||||||
|
|
||||||
#[cfg(feature = "fruits")]
|
#[cfg(feature = "fruits")]
|
||||||
fruits::stars(self, mods, passed_objects)
|
StarResult::Fruits(fruits::stars(self, mods, passed_objects))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-4
@@ -6,7 +6,7 @@ mod strain;
|
|||||||
pub use pp::*;
|
pub use pp::*;
|
||||||
use strain::Strain;
|
use strain::Strain;
|
||||||
|
|
||||||
use crate::{parse::HitObject, Beatmap, GameMode, Mods, StarResult, Strains};
|
use crate::{parse::HitObject, Beatmap, GameMode, Mods, Strains};
|
||||||
|
|
||||||
const SECTION_LEN: f32 = 400.0;
|
const SECTION_LEN: f32 = 400.0;
|
||||||
const STAR_SCALING_FACTOR: f32 = 0.018;
|
const STAR_SCALING_FACTOR: f32 = 0.018;
|
||||||
@@ -14,11 +14,15 @@ const STAR_SCALING_FACTOR: f32 = 0.018;
|
|||||||
/// Star calculation for osu!mania maps
|
/// Star calculation for osu!mania maps
|
||||||
///
|
///
|
||||||
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
|
/// 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>) -> StarResult {
|
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 take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||||
|
|
||||||
if take < 2 {
|
if take < 2 {
|
||||||
return StarResult::Mania(DifficultyAttributes::default());
|
return DifficultyAttributes::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
let rounded_cs = map.cs.round();
|
let rounded_cs = map.cs.round();
|
||||||
@@ -86,7 +90,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
|||||||
|
|
||||||
let stars = strain.difficulty_value() * STAR_SCALING_FACTOR;
|
let stars = strain.difficulty_value() * STAR_SCALING_FACTOR;
|
||||||
|
|
||||||
StarResult::Mania(DifficultyAttributes { stars })
|
DifficultyAttributes { stars }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Essentially the same as the `stars` function but instead of
|
/// Essentially the same as the `stars` function but instead of
|
||||||
|
|||||||
+1
-1
@@ -93,7 +93,7 @@ impl<'m> ManiaPP<'m> {
|
|||||||
pub fn calculate(self) -> PerformanceAttributes {
|
pub fn calculate(self) -> PerformanceAttributes {
|
||||||
let stars = self
|
let stars = self
|
||||||
.stars
|
.stars
|
||||||
.unwrap_or_else(|| stars(self.map, self.mods, self.passed_objects).stars());
|
.unwrap_or_else(|| stars(self.map, self.mods, self.passed_objects).stars);
|
||||||
|
|
||||||
let ez = self.mods.ez();
|
let ez = self.mods.ez();
|
||||||
let nf = self.mods.nf();
|
let nf = self.mods.nf();
|
||||||
|
|||||||
+134
-90
@@ -42,8 +42,6 @@ pub struct OsuPP<'m> {
|
|||||||
n50: Option<usize>,
|
n50: Option<usize>,
|
||||||
n_misses: usize,
|
n_misses: usize,
|
||||||
passed_objects: Option<usize>,
|
passed_objects: Option<usize>,
|
||||||
|
|
||||||
effective_misses: Option<usize>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'m> OsuPP<'m> {
|
impl<'m> OsuPP<'m> {
|
||||||
@@ -61,8 +59,6 @@ impl<'m> OsuPP<'m> {
|
|||||||
n50: None,
|
n50: None,
|
||||||
n_misses: 0,
|
n_misses: 0,
|
||||||
passed_objects: None,
|
passed_objects: None,
|
||||||
|
|
||||||
effective_misses: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,36 +198,82 @@ impl<'m> OsuPP<'m> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_hitresults(&mut self) {
|
fn assert_hitresults(self, attributes: DifficultyAttributes) -> OsuPPInner {
|
||||||
if self.acc.is_none() {
|
let mut n300 = self.n300;
|
||||||
|
let mut n100 = self.n100;
|
||||||
|
let mut n50 = self.n50;
|
||||||
|
|
||||||
|
let n_objects = self
|
||||||
|
.passed_objects
|
||||||
|
.unwrap_or_else(|| self.map.hit_objects.len());
|
||||||
|
|
||||||
|
if let Some(acc) = self.acc {
|
||||||
|
let n300 = n300.unwrap_or(0);
|
||||||
|
let n100 = n100.unwrap_or(0);
|
||||||
|
let n50 = n50.unwrap_or(0);
|
||||||
|
|
||||||
|
let total_hits = (n300 + n100 + n50 + self.n_misses).min(n_objects) as f32;
|
||||||
|
|
||||||
|
let effective_misses =
|
||||||
|
calculate_effective_misses(&attributes, self.combo, self.n_misses, total_hits);
|
||||||
|
|
||||||
|
OsuPPInner {
|
||||||
|
attributes,
|
||||||
|
mods: self.mods,
|
||||||
|
combo: self.combo,
|
||||||
|
acc,
|
||||||
|
n300,
|
||||||
|
n100,
|
||||||
|
n50,
|
||||||
|
total_hits,
|
||||||
|
effective_misses,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
let n_objects = self
|
let n_objects = self
|
||||||
.passed_objects
|
.passed_objects
|
||||||
.unwrap_or_else(|| self.map.hit_objects.len());
|
.unwrap_or_else(|| self.map.hit_objects.len());
|
||||||
|
|
||||||
let remaining = n_objects
|
let remaining = n_objects
|
||||||
.saturating_sub(self.n300.unwrap_or(0))
|
.saturating_sub(n300.unwrap_or(0))
|
||||||
.saturating_sub(self.n100.unwrap_or(0))
|
.saturating_sub(n100.unwrap_or(0))
|
||||||
.saturating_sub(self.n50.unwrap_or(0))
|
.saturating_sub(n50.unwrap_or(0))
|
||||||
.saturating_sub(self.n_misses);
|
.saturating_sub(self.n_misses);
|
||||||
|
|
||||||
if remaining > 0 {
|
if remaining > 0 {
|
||||||
if self.n300.is_none() {
|
if n300.is_none() {
|
||||||
self.n300.replace(remaining);
|
n300.replace(remaining);
|
||||||
} else if self.n100.is_none() {
|
} else if n100.is_none() {
|
||||||
self.n100.replace(remaining);
|
n100.replace(remaining);
|
||||||
} else if self.n50.is_none() {
|
} else if n50.is_none() {
|
||||||
self.n50.replace(remaining);
|
n50.replace(remaining);
|
||||||
} else {
|
} else {
|
||||||
*self.n300.as_mut().unwrap() += remaining;
|
*n300.as_mut().unwrap() += remaining;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let n300 = *self.n300.get_or_insert(0);
|
let n300 = n300.unwrap_or(0);
|
||||||
let n100 = *self.n100.get_or_insert(0);
|
let n100 = n100.unwrap_or(0);
|
||||||
let n50 = *self.n50.get_or_insert(0);
|
let n50 = n50.unwrap_or(0);
|
||||||
|
|
||||||
let numerator = n300 * 6 + n100 * 2 + n50;
|
let numerator = n300 * 6 + n100 * 2 + n50;
|
||||||
self.acc.replace(numerator as f32 / n_objects as f32 / 6.0);
|
let acc = numerator as f32 / n_objects as f32 / 6.0;
|
||||||
|
|
||||||
|
let total_hits = (n300 + n100 + n50 + self.n_misses).min(n_objects) as f32;
|
||||||
|
|
||||||
|
let effective_misses =
|
||||||
|
calculate_effective_misses(&attributes, self.combo, self.n_misses, total_hits);
|
||||||
|
|
||||||
|
OsuPPInner {
|
||||||
|
attributes,
|
||||||
|
mods: self.mods,
|
||||||
|
combo: self.combo,
|
||||||
|
acc,
|
||||||
|
n300,
|
||||||
|
n100,
|
||||||
|
n50,
|
||||||
|
total_hits,
|
||||||
|
effective_misses,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,6 +298,8 @@ impl<'m> OsuPP<'m> {
|
|||||||
self.calculate_with_func(super::all_included::stars)
|
self.calculate_with_func(super::all_included::stars)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: import `stars` function based on features
|
||||||
|
|
||||||
// Omits an unnecessary error when enabled features are invalid
|
// Omits an unnecessary error when enabled features are invalid
|
||||||
#[cfg(not(any(
|
#[cfg(not(any(
|
||||||
feature = "no_leniency",
|
feature = "no_leniency",
|
||||||
@@ -268,46 +312,56 @@ impl<'m> OsuPP<'m> {
|
|||||||
|
|
||||||
fn calculate_with_func(
|
fn calculate_with_func(
|
||||||
mut self,
|
mut self,
|
||||||
stars_func: impl FnOnce(&Beatmap, u32, Option<usize>) -> StarResult,
|
stars_func: impl FnOnce(&Beatmap, u32, Option<usize>) -> DifficultyAttributes,
|
||||||
) -> PerformanceAttributes {
|
) -> PerformanceAttributes {
|
||||||
if self.attributes.is_none() {
|
let attributes = self
|
||||||
let attributes = stars_func(self.map, self.mods, self.passed_objects)
|
.attributes
|
||||||
.attributes()
|
.take()
|
||||||
.unwrap();
|
.unwrap_or_else(|| stars_func(self.map, self.mods, self.passed_objects));
|
||||||
self.attributes.replace(attributes);
|
|
||||||
|
self.assert_hitresults(attributes).calculate()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure the hitresults and accuracy are set
|
struct OsuPPInner {
|
||||||
self.assert_hitresults();
|
attributes: DifficultyAttributes,
|
||||||
|
mods: u32,
|
||||||
|
combo: Option<usize>,
|
||||||
|
acc: f32,
|
||||||
|
|
||||||
let total_hits = self.total_hits() as f32;
|
n300: usize,
|
||||||
|
n100: usize,
|
||||||
|
n50: usize,
|
||||||
|
|
||||||
|
total_hits: f32,
|
||||||
|
effective_misses: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OsuPPInner {
|
||||||
|
fn calculate(mut self) -> PerformanceAttributes {
|
||||||
let mut multiplier = 1.12;
|
let mut multiplier = 1.12;
|
||||||
|
|
||||||
self.calculate_effective_misses(total_hits);
|
|
||||||
|
|
||||||
// NF penalty
|
// NF penalty
|
||||||
if self.mods.nf() {
|
if self.mods.nf() {
|
||||||
multiplier *= (1.0 - 0.02 * self.effective_misses.map_or(0.0, |m| m as f32)).max(0.9);
|
multiplier *= (1.0 - 0.02 * (self.effective_misses as f32)).max(0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
// SO penalty
|
// SO penalty
|
||||||
if self.mods.so() {
|
if self.mods.so() {
|
||||||
let n_spinners = self.attributes.as_ref().unwrap().n_spinners;
|
let n_spinners = self.attributes.n_spinners;
|
||||||
multiplier *= 1.0 - (n_spinners as f32 / total_hits).powf(0.85);
|
multiplier *= 1.0 - (n_spinners as f32 / self.total_hits).powf(0.85);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Relax penalty
|
// Relax penalty
|
||||||
if self.mods.rx() {
|
if self.mods.rx() {
|
||||||
*self.effective_misses.as_mut().unwrap() +=
|
self.effective_misses += self.n100 + self.n50;
|
||||||
self.n100.unwrap_or(0) + self.n50.unwrap_or(0);
|
|
||||||
|
|
||||||
multiplier *= 0.6;
|
multiplier *= 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
let aim_value = self.compute_aim_value(total_hits);
|
let aim_value = self.compute_aim_value();
|
||||||
let speed_value = self.compute_speed_value(total_hits);
|
let speed_value = self.compute_speed_value();
|
||||||
let acc_value = self.compute_accuracy_value(total_hits);
|
let acc_value = self.compute_accuracy_value();
|
||||||
let flashlight_value = self.compute_flashlight_value(total_hits);
|
let flashlight_value = self.compute_flashlight_value();
|
||||||
|
|
||||||
let pp = (aim_value.powf(1.1)
|
let pp = (aim_value.powf(1.1)
|
||||||
+ speed_value.powf(1.1)
|
+ speed_value.powf(1.1)
|
||||||
@@ -317,7 +371,7 @@ impl<'m> OsuPP<'m> {
|
|||||||
* multiplier;
|
* multiplier;
|
||||||
|
|
||||||
PerformanceAttributes {
|
PerformanceAttributes {
|
||||||
attributes: self.attributes.unwrap(),
|
attributes: self.attributes,
|
||||||
pp_acc: aim_value,
|
pp_acc: aim_value,
|
||||||
pp_aim: aim_value,
|
pp_aim: aim_value,
|
||||||
pp_flashlight: flashlight_value,
|
pp_flashlight: flashlight_value,
|
||||||
@@ -326,8 +380,9 @@ impl<'m> OsuPP<'m> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_aim_value(&self, total_hits: f32) -> f32 {
|
fn compute_aim_value(&self) -> f32 {
|
||||||
let attributes = self.attributes.as_ref().unwrap();
|
let attributes = &self.attributes;
|
||||||
|
let total_hits = self.total_hits;
|
||||||
|
|
||||||
// TD penalty
|
// TD penalty
|
||||||
let raw_aim = if self.mods.td() {
|
let raw_aim = if self.mods.td() {
|
||||||
@@ -345,7 +400,7 @@ impl<'m> OsuPP<'m> {
|
|||||||
aim_value *= len_bonus;
|
aim_value *= len_bonus;
|
||||||
|
|
||||||
// Penalize misses
|
// Penalize misses
|
||||||
let effective_misses = self.effective_misses.map_or(0, |m| m as i32);
|
let effective_misses = self.effective_misses as i32;
|
||||||
if effective_misses > 0 {
|
if effective_misses > 0 {
|
||||||
aim_value *= 0.97
|
aim_value *= 0.97
|
||||||
* (1.0 - (effective_misses as f32 / total_hits).powf(0.775)).powi(effective_misses);
|
* (1.0 - (effective_misses as f32 / total_hits).powf(0.775)).powi(effective_misses);
|
||||||
@@ -376,14 +431,15 @@ impl<'m> OsuPP<'m> {
|
|||||||
aim_value *= ar_bonus;
|
aim_value *= ar_bonus;
|
||||||
|
|
||||||
// Scale with accuracy
|
// Scale with accuracy
|
||||||
aim_value *= 0.5 + self.acc.unwrap() / 2.0;
|
aim_value *= 0.5 + self.acc / 2.0;
|
||||||
aim_value *= 0.98 + attributes.od * attributes.od / 2500.0;
|
aim_value *= 0.98 + attributes.od * attributes.od / 2500.0;
|
||||||
|
|
||||||
aim_value
|
aim_value
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_speed_value(&self, total_hits: f32) -> f32 {
|
fn compute_speed_value(&self) -> f32 {
|
||||||
let attributes = self.attributes.as_ref().unwrap();
|
let attributes = &self.attributes;
|
||||||
|
let total_hits = self.total_hits;
|
||||||
|
|
||||||
let mut speed_value =
|
let mut speed_value =
|
||||||
(5.0 * (attributes.speed_strain / 0.0675).max(1.0) - 4.0).powi(3) / 100_000.0;
|
(5.0 * (attributes.speed_strain / 0.0675).max(1.0) - 4.0).powi(3) / 100_000.0;
|
||||||
@@ -395,7 +451,7 @@ impl<'m> OsuPP<'m> {
|
|||||||
speed_value *= len_bonus;
|
speed_value *= len_bonus;
|
||||||
|
|
||||||
// Penalize misses
|
// Penalize misses
|
||||||
let effective_misses = self.effective_misses.map_or(0.0, |m| m as f32);
|
let effective_misses = self.effective_misses as f32;
|
||||||
if effective_misses > 0.0 {
|
if effective_misses > 0.0 {
|
||||||
speed_value *= 0.97
|
speed_value *= 0.97
|
||||||
* (1.0 - (effective_misses / total_hits).powf(0.775))
|
* (1.0 - (effective_misses / total_hits).powf(0.775))
|
||||||
@@ -425,31 +481,29 @@ impl<'m> OsuPP<'m> {
|
|||||||
|
|
||||||
// Scaling the speed value with accuracy and OD
|
// Scaling the speed value with accuracy and OD
|
||||||
let od_factor = 0.95 + attributes.od * attributes.od / 750.0;
|
let od_factor = 0.95 + attributes.od * attributes.od / 750.0;
|
||||||
let acc_factor = self
|
let acc_factor = self.acc.powf((14.5 - attributes.od.max(8.0)) / 2.0);
|
||||||
.acc
|
|
||||||
.unwrap()
|
|
||||||
.powf((14.5 - attributes.od.max(8.0)) / 2.0);
|
|
||||||
speed_value *= od_factor * acc_factor;
|
speed_value *= od_factor * acc_factor;
|
||||||
|
|
||||||
// Penalize n50s
|
// Penalize n50s
|
||||||
speed_value *= 0.98_f32.powf(
|
speed_value *= 0.98_f32.powf(
|
||||||
(self.n50.unwrap_or(0) as f32 >= total_hits / 500.0) as u8 as f32
|
(self.n50 as f32 >= total_hits / 500.0) as u8 as f32
|
||||||
* (self.n50.unwrap_or(0) as f32 - total_hits / 500.0),
|
* (self.n50 as f32 - total_hits / 500.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
speed_value
|
speed_value
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_accuracy_value(&self, total_hits: f32) -> f32 {
|
fn compute_accuracy_value(&self) -> f32 {
|
||||||
if self.mods.rx() {
|
if self.mods.rx() {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let attributes = self.attributes.as_ref().unwrap();
|
let attributes = &self.attributes;
|
||||||
|
let total_hits = self.total_hits;
|
||||||
let n_circles = attributes.n_circles as f32;
|
let n_circles = attributes.n_circles as f32;
|
||||||
let n300 = self.n300.unwrap_or(0) as f32;
|
let n300 = self.n300 as f32;
|
||||||
let n100 = self.n100.unwrap_or(0) as f32;
|
let n100 = self.n100 as f32;
|
||||||
let n50 = self.n50.unwrap_or(0) as f32;
|
let n50 = self.n50 as f32;
|
||||||
|
|
||||||
let better_acc_percentage = (n_circles > 0.0) as u8 as f32
|
let better_acc_percentage = (n_circles > 0.0) as u8 as f32
|
||||||
* (((n300 - (total_hits - n_circles)) * 6.0 + n100 * 2.0 + n50) / (n_circles * 6.0))
|
* (((n300 - (total_hits - n_circles)) * 6.0 + n100 * 2.0 + n50) / (n_circles * 6.0))
|
||||||
@@ -473,12 +527,13 @@ impl<'m> OsuPP<'m> {
|
|||||||
acc_value
|
acc_value
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_flashlight_value(&self, total_hits: f32) -> f32 {
|
fn compute_flashlight_value(&self) -> f32 {
|
||||||
if !self.mods.fl() {
|
if !self.mods.fl() {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let attributes = self.attributes.as_ref().unwrap();
|
let attributes = &self.attributes;
|
||||||
|
let total_hits = self.total_hits;
|
||||||
|
|
||||||
// TD penalty
|
// TD penalty
|
||||||
let raw_flashlight = if self.mods.td() {
|
let raw_flashlight = if self.mods.td() {
|
||||||
@@ -496,7 +551,7 @@ impl<'m> OsuPP<'m> {
|
|||||||
|
|
||||||
// Penalize misses by assessing # of misses relative to the total # of objects.
|
// Penalize misses by assessing # of misses relative to the total # of objects.
|
||||||
// Default a 3% reduction for any # of misses
|
// Default a 3% reduction for any # of misses
|
||||||
let effective_misses = self.effective_misses.map_or(0.0, |m| m as f32);
|
let effective_misses = self.effective_misses as f32;
|
||||||
if effective_misses > 0.0 {
|
if effective_misses > 0.0 {
|
||||||
flashlight_value *= 0.97
|
flashlight_value *= 0.97
|
||||||
* (1.0 - (effective_misses / total_hits).powf(0.775))
|
* (1.0 - (effective_misses / total_hits).powf(0.775))
|
||||||
@@ -514,25 +569,28 @@ impl<'m> OsuPP<'m> {
|
|||||||
+ (total_hits > 200.0) as u8 as f32 * (0.2 * ((total_hits - 200.0) / 200.0).min(1.0));
|
+ (total_hits > 200.0) as u8 as f32 * (0.2 * ((total_hits - 200.0) / 200.0).min(1.0));
|
||||||
|
|
||||||
// Scale the aim value with accuracy _slightly_
|
// Scale the aim value with accuracy _slightly_
|
||||||
flashlight_value *= 0.5 + self.acc.unwrap() / 2.0;
|
flashlight_value *= 0.5 + self.acc / 2.0;
|
||||||
|
|
||||||
// It is important to also consider accuracy difficulty when doing that
|
// It is important to also consider accuracy difficulty when doing that
|
||||||
flashlight_value *= 0.98 + attributes.od * attributes.od / 2500.0;
|
flashlight_value *= 0.98 + attributes.od * attributes.od / 2500.0;
|
||||||
|
|
||||||
flashlight_value
|
flashlight_value
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn calculate_effective_misses(&mut self, total_hits: f32) {
|
fn calculate_effective_misses(
|
||||||
|
attributes: &DifficultyAttributes,
|
||||||
|
combo: Option<usize>,
|
||||||
|
n_misses: usize,
|
||||||
|
total_hits: f32,
|
||||||
|
) -> usize {
|
||||||
// Guess the number of misses + slider breaks from combo
|
// Guess the number of misses + slider breaks from combo
|
||||||
let mut combo_based_misses: f32 = 0.0;
|
let mut combo_based_misses: f32 = 0.0;
|
||||||
|
|
||||||
let attributes = self.attributes.as_ref().unwrap();
|
|
||||||
|
|
||||||
if attributes.n_sliders > 0 {
|
if attributes.n_sliders > 0 {
|
||||||
let full_combo_threshold =
|
let full_combo_threshold = attributes.max_combo as f32 - 0.1 * attributes.n_sliders as f32;
|
||||||
attributes.max_combo as f32 - 0.1 * attributes.n_sliders as f32;
|
|
||||||
|
|
||||||
let f32_combo = self.combo.map(|c| c as f32);
|
let f32_combo = combo.map(|c| c as f32);
|
||||||
|
|
||||||
if let Some(combo) = f32_combo.filter(|&c| c < full_combo_threshold) {
|
if let Some(combo) = f32_combo.filter(|&c| c < full_combo_threshold) {
|
||||||
combo_based_misses = full_combo_threshold / combo.max(1.0);
|
combo_based_misses = full_combo_threshold / combo.max(1.0);
|
||||||
@@ -543,18 +601,7 @@ impl<'m> OsuPP<'m> {
|
|||||||
// can be higher than total hits and that breaks some calculations
|
// can be higher than total hits and that breaks some calculations
|
||||||
combo_based_misses = combo_based_misses.min(total_hits);
|
combo_based_misses = combo_based_misses.min(total_hits);
|
||||||
|
|
||||||
self.effective_misses = Some(self.n_misses.max(combo_based_misses.floor() as usize))
|
n_misses.max(combo_based_misses.floor() as usize)
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn total_hits(&self) -> usize {
|
|
||||||
let n_objects = self
|
|
||||||
.passed_objects
|
|
||||||
.unwrap_or_else(|| self.map.hit_objects.len());
|
|
||||||
|
|
||||||
(self.n300.unwrap_or(0) + self.n100.unwrap_or(0) + self.n50.unwrap_or(0) + self.n_misses)
|
|
||||||
.min(n_objects)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait OsuAttributeProvider {
|
pub trait OsuAttributeProvider {
|
||||||
@@ -666,24 +713,21 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn osu_missing_objects() {
|
fn osu_missing_objects() {
|
||||||
let map = Beatmap::default();
|
let map = Beatmap::default();
|
||||||
|
let attributes = DifficultyAttributes::default();
|
||||||
|
|
||||||
let total_objects = 1234;
|
let total_objects = 1234;
|
||||||
let n300 = 1000;
|
let n300 = 1000;
|
||||||
let n100 = 200;
|
let n100 = 200;
|
||||||
let n50 = 30;
|
let n50 = 30;
|
||||||
|
|
||||||
let mut calculator = OsuPP::new(&map)
|
let calculator = OsuPP::new(&map)
|
||||||
.passed_objects(total_objects)
|
.passed_objects(total_objects)
|
||||||
.n300(n300)
|
.n300(n300)
|
||||||
.n100(n100)
|
.n100(n100)
|
||||||
.n50(n50);
|
.n50(n50)
|
||||||
|
.assert_hitresults(attributes);
|
||||||
|
|
||||||
calculator.assert_hitresults();
|
let n_objects = calculator.n300 + calculator.n100 + calculator.n50;
|
||||||
|
|
||||||
let n_objects = calculator.n300.unwrap()
|
|
||||||
+ calculator.n100.unwrap()
|
|
||||||
+ calculator.n50.unwrap()
|
|
||||||
+ calculator.n_misses;
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
total_objects, n_objects,
|
total_objects, n_objects,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use skill::Skill;
|
|||||||
use skill_kind::SkillKind;
|
use skill_kind::SkillKind;
|
||||||
use slider_state::SliderState;
|
use slider_state::SliderState;
|
||||||
|
|
||||||
use crate::{Beatmap, Mods, StarResult, Strains};
|
use crate::{Beatmap, Mods, Strains};
|
||||||
|
|
||||||
const OBJECT_RADIUS: f32 = 64.0;
|
const OBJECT_RADIUS: f32 = 64.0;
|
||||||
const SECTION_LEN: f32 = 400.0;
|
const SECTION_LEN: f32 = 400.0;
|
||||||
@@ -37,7 +37,11 @@ const STACK_DISTANCE: f32 = 3.0;
|
|||||||
/// most precise results.
|
/// most precise results.
|
||||||
///
|
///
|
||||||
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
|
/// 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>) -> StarResult {
|
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 take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||||
|
|
||||||
let map_attributes = map.attributes().mods(mods);
|
let map_attributes = map.attributes().mods(mods);
|
||||||
@@ -53,7 +57,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
|||||||
};
|
};
|
||||||
|
|
||||||
if take < 2 {
|
if take < 2 {
|
||||||
return StarResult::Osu(diff_attributes);
|
return diff_attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut raw_ar = map.ar;
|
let mut raw_ar = map.ar;
|
||||||
@@ -230,7 +234,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
|||||||
diff_attributes.aim_strain = aim_rating;
|
diff_attributes.aim_strain = aim_rating;
|
||||||
diff_attributes.flashlight_rating = flashlight_rating;
|
diff_attributes.flashlight_rating = flashlight_rating;
|
||||||
|
|
||||||
StarResult::Osu(diff_attributes)
|
diff_attributes
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Essentially the same as the `stars` function but instead of
|
/// Essentially the same as the `stars` function but instead of
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use skill::Skill;
|
|||||||
use skill_kind::SkillKind;
|
use skill_kind::SkillKind;
|
||||||
use slider_state::SliderState;
|
use slider_state::SliderState;
|
||||||
|
|
||||||
use crate::{Beatmap, Mods, StarResult, Strains};
|
use crate::{Beatmap, Mods, Strains};
|
||||||
|
|
||||||
const OBJECT_RADIUS: f32 = 64.0;
|
const OBJECT_RADIUS: f32 = 64.0;
|
||||||
const SECTION_LEN: f32 = 400.0;
|
const SECTION_LEN: f32 = 400.0;
|
||||||
@@ -37,7 +37,11 @@ const NORMALIZED_RADIUS: f32 = 52.0;
|
|||||||
/// processing stack leniency is relatively expensive.
|
/// processing stack leniency is relatively expensive.
|
||||||
///
|
///
|
||||||
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
|
/// 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>) -> StarResult {
|
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 take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||||
|
|
||||||
let map_attributes = map.attributes().mods(mods);
|
let map_attributes = map.attributes().mods(mods);
|
||||||
@@ -52,7 +56,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
|||||||
};
|
};
|
||||||
|
|
||||||
if take < 2 {
|
if take < 2 {
|
||||||
return StarResult::Osu(diff_attributes);
|
return diff_attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
let radius = OBJECT_RADIUS * (1.0 - 0.7 * (map_attributes.cs - 5.0) / 5.0) / 2.0;
|
let radius = OBJECT_RADIUS * (1.0 - 0.7 * (map_attributes.cs - 5.0) / 5.0) / 2.0;
|
||||||
@@ -201,7 +205,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
|||||||
diff_attributes.n_spinners = map.n_spinners as usize;
|
diff_attributes.n_spinners = map.n_spinners as usize;
|
||||||
diff_attributes.stars = star_rating;
|
diff_attributes.stars = star_rating;
|
||||||
|
|
||||||
StarResult::Osu(diff_attributes)
|
diff_attributes
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Essentially the same as the `stars` function but instead of
|
/// Essentially the same as the `stars` function but instead of
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use skill::Skill;
|
|||||||
use skill_kind::SkillKind;
|
use skill_kind::SkillKind;
|
||||||
use slider_state::SliderState;
|
use slider_state::SliderState;
|
||||||
|
|
||||||
use crate::{parse::HitObjectKind, Beatmap, Mods, StarResult, Strains};
|
use crate::{parse::HitObjectKind, Beatmap, Mods, Strains};
|
||||||
|
|
||||||
const OBJECT_RADIUS: f32 = 64.0;
|
const OBJECT_RADIUS: f32 = 64.0;
|
||||||
const SECTION_LEN: f32 = 400.0;
|
const SECTION_LEN: f32 = 400.0;
|
||||||
@@ -34,7 +34,11 @@ const NORMALIZED_RADIUS: f32 = 52.0;
|
|||||||
/// However, this is the most efficient one.
|
/// However, this is the most efficient one.
|
||||||
///
|
///
|
||||||
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
|
/// 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>) -> StarResult {
|
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 take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||||
|
|
||||||
let attributes = map.attributes().mods(mods);
|
let attributes = map.attributes().mods(mods);
|
||||||
@@ -42,12 +46,12 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
|||||||
let od = (80.0 - hit_window) / 6.0;
|
let od = (80.0 - hit_window) / 6.0;
|
||||||
|
|
||||||
if take < 2 {
|
if take < 2 {
|
||||||
return StarResult::Osu(DifficultyAttributes {
|
return DifficultyAttributes {
|
||||||
ar: attributes.ar,
|
ar: attributes.ar,
|
||||||
hp: attributes.hp,
|
hp: attributes.hp,
|
||||||
od,
|
od,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let radius = OBJECT_RADIUS * (1.0 - 0.7 * (attributes.cs - 5.0) / 5.0) / 2.0;
|
let radius = OBJECT_RADIUS * (1.0 - 0.7 * (attributes.cs - 5.0) / 5.0) / 2.0;
|
||||||
@@ -194,7 +198,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
|||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
StarResult::Osu(DifficultyAttributes {
|
DifficultyAttributes {
|
||||||
stars: star_rating,
|
stars: star_rating,
|
||||||
ar: attributes.ar,
|
ar: attributes.ar,
|
||||||
hp: attributes.hp,
|
hp: attributes.hp,
|
||||||
@@ -206,7 +210,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
|||||||
n_circles: map.n_circles as usize,
|
n_circles: map.n_circles as usize,
|
||||||
n_spinners: map.n_spinners as usize,
|
n_spinners: map.n_spinners as usize,
|
||||||
n_sliders: map.n_sliders as usize,
|
n_sliders: map.n_sliders as usize,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Essentially the same as the `stars` function but instead of
|
/// Essentially the same as the `stars` function but instead of
|
||||||
|
|||||||
+8
-4
@@ -18,7 +18,7 @@ use skill::Skill;
|
|||||||
use skill_kind::SkillKind;
|
use skill_kind::SkillKind;
|
||||||
use stamina_cheese::StaminaCheeseDetector;
|
use stamina_cheese::StaminaCheeseDetector;
|
||||||
|
|
||||||
use crate::{Beatmap, Mods, StarResult, Strains};
|
use crate::{Beatmap, Mods, Strains};
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::f32::consts::PI;
|
use std::f32::consts::PI;
|
||||||
@@ -32,11 +32,15 @@ const STAMINA_SKILL_MULTIPLIER: f32 = 0.02;
|
|||||||
/// Star calculation for osu!taiko maps.
|
/// Star calculation for osu!taiko maps.
|
||||||
///
|
///
|
||||||
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
|
/// 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>) -> StarResult {
|
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 take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||||
|
|
||||||
if take < 2 {
|
if take < 2 {
|
||||||
return StarResult::Taiko(DifficultyAttributes { stars: 0.0 });
|
return DifficultyAttributes { stars: 0.0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
// True if the object at that index is stamina cheese
|
// True if the object at that index is stamina cheese
|
||||||
@@ -116,7 +120,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
|||||||
|
|
||||||
let stars = rescale(1.4 * separate_rating + 0.5 * combined_rating);
|
let stars = rescale(1.4 * separate_rating + 0.5 * combined_rating);
|
||||||
|
|
||||||
StarResult::Taiko(DifficultyAttributes { stars })
|
DifficultyAttributes { stars }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Essentially the same as the `stars` function but instead of
|
/// Essentially the same as the `stars` function but instead of
|
||||||
|
|||||||
+27
-3
@@ -139,7 +139,7 @@ impl<'m> TaikoPP<'m> {
|
|||||||
pub fn calculate(mut self) -> PerformanceAttributes {
|
pub fn calculate(mut self) -> PerformanceAttributes {
|
||||||
let stars = self
|
let stars = self
|
||||||
.stars
|
.stars
|
||||||
.unwrap_or_else(|| stars(self.map, self.mods, self.passed_objects).stars());
|
.unwrap_or_else(|| stars(self.map, self.mods, self.passed_objects).stars);
|
||||||
|
|
||||||
if self.n300.or(self.n100).is_some() {
|
if self.n300.or(self.n100).is_some() {
|
||||||
let total = self.map.n_circles as usize;
|
let total = self.map.n_circles as usize;
|
||||||
@@ -161,6 +161,30 @@ impl<'m> TaikoPP<'m> {
|
|||||||
self.acc = (2 * n300 + n100) as f32 / (2 * (n300 + n100 + misses)) as f32;
|
self.acc = (2 * n300 + n100) as f32 / (2 * (n300 + n100 + misses)) as f32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let inner = TaikoPPInner {
|
||||||
|
map: self.map,
|
||||||
|
stars,
|
||||||
|
mods: self.mods,
|
||||||
|
max_combo: self.max_combo,
|
||||||
|
acc: self.acc,
|
||||||
|
n_misses: self.n_misses,
|
||||||
|
};
|
||||||
|
|
||||||
|
inner.calculate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TaikoPPInner<'m> {
|
||||||
|
map: &'m Beatmap,
|
||||||
|
stars: f32,
|
||||||
|
mods: u32,
|
||||||
|
max_combo: usize,
|
||||||
|
acc: f32,
|
||||||
|
n_misses: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'m> TaikoPPInner<'m> {
|
||||||
|
fn calculate(self) -> PerformanceAttributes {
|
||||||
let mut multiplier = 1.1;
|
let mut multiplier = 1.1;
|
||||||
|
|
||||||
if self.mods.nf() {
|
if self.mods.nf() {
|
||||||
@@ -171,13 +195,13 @@ impl<'m> TaikoPP<'m> {
|
|||||||
multiplier *= 1.1;
|
multiplier *= 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let strain_value = self.compute_strain_value(stars);
|
let strain_value = self.compute_strain_value(self.stars);
|
||||||
let acc_value = self.compute_accuracy_value();
|
let acc_value = self.compute_accuracy_value();
|
||||||
|
|
||||||
let pp = (strain_value.powf(1.1) + acc_value.powf(1.1)).powf(1.0 / 1.1) * multiplier;
|
let pp = (strain_value.powf(1.1) + acc_value.powf(1.1)).powf(1.0 / 1.1) * multiplier;
|
||||||
|
|
||||||
PerformanceAttributes {
|
PerformanceAttributes {
|
||||||
attributes: DifficultyAttributes { stars },
|
attributes: DifficultyAttributes { stars: self.stars },
|
||||||
pp,
|
pp,
|
||||||
pp_acc: acc_value,
|
pp_acc: acc_value,
|
||||||
pp_strain: strain_value,
|
pp_strain: strain_value,
|
||||||
|
|||||||
Reference in New Issue
Block a user