provided a way for partial plays
This commit is contained in:
+11
-2
@@ -24,9 +24,17 @@ const CATCHER_SIZE: f32 = 106.75;
|
||||
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
|
||||
|
||||
/// Star calculation for osu!ctb maps
|
||||
///
|
||||
/// 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
|
||||
pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
||||
if map.hit_objects.len() < 2 {
|
||||
pub fn stars(
|
||||
map: &Beatmap,
|
||||
mods: impl Mods,
|
||||
passed_objects: Option<usize>,
|
||||
) -> DifficultyAttributes {
|
||||
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||
|
||||
if take < 2 {
|
||||
return DifficultyAttributes::default();
|
||||
}
|
||||
|
||||
@@ -43,6 +51,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
||||
let mut hit_objects = map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.take(take)
|
||||
.scan((None, 0.0), |(last_pos, last_time), h| match &h.kind {
|
||||
HitObjectKind::Circle => {
|
||||
let mut h = CatchObject::new((h.pos, h.start_time));
|
||||
|
||||
+13
-3
@@ -17,7 +17,6 @@ impl PpProvider for Beatmap {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Allow partial plays
|
||||
pub struct PpCalculator<'m> {
|
||||
map: &'m Beatmap,
|
||||
attributes: Option<DifficultyAttributes>,
|
||||
@@ -29,6 +28,7 @@ pub struct PpCalculator<'m> {
|
||||
n_tiny_droplets: Option<usize>,
|
||||
n_tiny_droplet_misses: Option<usize>,
|
||||
n_misses: usize,
|
||||
passed_objects: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'m> PpCalculator<'m> {
|
||||
@@ -45,6 +45,7 @@ impl<'m> PpCalculator<'m> {
|
||||
n_tiny_droplets: None,
|
||||
n_tiny_droplet_misses: None,
|
||||
n_misses: 0,
|
||||
passed_objects: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,12 +105,21 @@ impl<'m> PpCalculator<'m> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Amount of passed objects for partial plays, e.g. a fail.
|
||||
#[inline]
|
||||
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
|
||||
self.passed_objects.replace(passed_objects);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Generate the hit results with respect to the given accuracy between `0` and `100`.
|
||||
///
|
||||
/// Be sure to set `misses` beforehand! Also, if available, set `attributes` beforehand.
|
||||
pub fn accuracy(mut self, acc: f32) -> Self {
|
||||
if self.attributes.is_none() {
|
||||
self.attributes.replace(stars(self.map, self.mods));
|
||||
self.attributes
|
||||
.replace(stars(self.map, self.mods, self.passed_objects));
|
||||
}
|
||||
|
||||
let attributes = self.attributes.as_ref().unwrap();
|
||||
@@ -146,7 +156,7 @@ impl<'m> PpCalculator<'m> {
|
||||
let attributes = self
|
||||
.attributes
|
||||
.take()
|
||||
.unwrap_or_else(|| stars(self.map, self.mods));
|
||||
.unwrap_or_else(|| stars(self.map, self.mods, self.passed_objects));
|
||||
|
||||
let stars = attributes.stars;
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ mod mods;
|
||||
pub use mods::Mods;
|
||||
pub use parse::*;
|
||||
|
||||
// TODO
|
||||
pub trait BeatmapExt {}
|
||||
|
||||
#[inline]
|
||||
fn difficulty_range(val: f32, max: f32, avg: f32, min: f32) -> f32 {
|
||||
if val > 5.0 {
|
||||
|
||||
+8
-7
@@ -10,8 +10,12 @@ const SECTION_LEN: f32 = 400.0;
|
||||
const STAR_SCALING_FACTOR: f32 = 0.018;
|
||||
|
||||
/// Star calculation for osu!mania maps
|
||||
pub fn stars(map: &Beatmap, mods: impl Mods) -> f32 {
|
||||
if map.hit_objects.len() < 2 {
|
||||
///
|
||||
/// 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>) -> f32 {
|
||||
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||
|
||||
if take < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@@ -22,6 +26,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> f32 {
|
||||
let mut hit_objects = map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.take(take)
|
||||
.skip(1)
|
||||
.zip(map.hit_objects.iter())
|
||||
.map(|(base, prev)| DifficultyHitObject::new(base, prev, map.cs, clock_rate));
|
||||
@@ -84,7 +89,7 @@ mod tests {
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn mania_single() {
|
||||
let file = match File::open("E:/Games/osu!/beatmaps/1355822.osu") {
|
||||
let file = match File::open("./maps/1355822.osu") {
|
||||
Ok(file) => file,
|
||||
Err(why) => panic!("Could not open file: {}", why),
|
||||
};
|
||||
@@ -94,10 +99,6 @@ mod tests {
|
||||
Err(why) => panic!("Error while parsing map: {}", why),
|
||||
};
|
||||
|
||||
// println!("timings:\n{:#?}", map.timing_points);
|
||||
// println!("---");
|
||||
// println!("difficulties:\n{:#?}", map.difficulty_points);
|
||||
|
||||
let result = PpCalculator::new(&map).mods(256).calculate();
|
||||
|
||||
println!("Stars: {}", result.stars);
|
||||
|
||||
+15
-2
@@ -17,12 +17,12 @@ impl PpProvider for Beatmap {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Allow partial plays
|
||||
pub struct PpCalculator<'m> {
|
||||
map: &'m Beatmap,
|
||||
stars: Option<f32>,
|
||||
mods: u32,
|
||||
score: Option<f32>,
|
||||
passed_objects: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'m> PpCalculator<'m> {
|
||||
@@ -33,6 +33,7 @@ impl<'m> PpCalculator<'m> {
|
||||
stars: None,
|
||||
mods: 0,
|
||||
score: None,
|
||||
passed_objects: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +51,8 @@ impl<'m> PpCalculator<'m> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Score of a play.
|
||||
/// On NM its between 0 and 1,000,000, on EZ between 0 and 500,000, etc
|
||||
#[inline]
|
||||
pub fn score(mut self, score: u32) -> Self {
|
||||
self.score.replace(score as f32);
|
||||
@@ -57,8 +60,18 @@ impl<'m> PpCalculator<'m> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Amount of passed objects for partial plays, e.g. a fail.
|
||||
#[inline]
|
||||
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
|
||||
self.passed_objects.replace(passed_objects);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn calculate(self) -> PpResult {
|
||||
let stars = self.stars.unwrap_or_else(|| stars(self.map, self.mods));
|
||||
let stars = self
|
||||
.stars
|
||||
.unwrap_or_else(|| stars(self.map, self.mods, self.passed_objects));
|
||||
|
||||
let ez = self.mods.ez();
|
||||
let nf = self.mods.nf();
|
||||
|
||||
+2
-7
@@ -43,17 +43,12 @@ impl Strain {
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn save_current_peak(&mut self) {
|
||||
// TODO: Remove branching
|
||||
if self.prev_time.is_some() {
|
||||
self.strain_peaks.push(self.current_section_peak);
|
||||
}
|
||||
self.strain_peaks.push(self.current_section_peak);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn start_new_section_from(&mut self, time: f32) {
|
||||
if let Some(prev) = self.prev_time {
|
||||
self.current_section_peak = self.peak_strain(time - prev);
|
||||
}
|
||||
self.current_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
+24
-5
@@ -17,7 +17,6 @@ impl PpProvider for Beatmap {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Allow partial plays
|
||||
pub struct PpCalculator<'m> {
|
||||
map: &'m Beatmap,
|
||||
attributes: Option<Attributes>,
|
||||
@@ -29,6 +28,7 @@ pub struct PpCalculator<'m> {
|
||||
n100: Option<usize>,
|
||||
n50: Option<usize>,
|
||||
n_misses: usize,
|
||||
passed_objects: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'m> PpCalculator<'m> {
|
||||
@@ -45,6 +45,7 @@ impl<'m> PpCalculator<'m> {
|
||||
n100: None,
|
||||
n50: None,
|
||||
n_misses: 0,
|
||||
passed_objects: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,11 +98,23 @@ impl<'m> PpCalculator<'m> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Amount of passed objects for partial plays, e.g. a fail.
|
||||
#[inline]
|
||||
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
|
||||
self.passed_objects.replace(passed_objects);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Generate the hit results with respect to the given accuracy between `0` and `100`.
|
||||
///
|
||||
/// Be sure to set `misses` beforehand!
|
||||
/// In case of a partial play, be also sure to set `passed_objects` beforehand!
|
||||
pub fn accuracy(mut self, acc: f32) -> Self {
|
||||
let n_objects = self.map.hit_objects.len();
|
||||
let n_objects = self
|
||||
.passed_objects
|
||||
.unwrap_or_else(|| self.map.hit_objects.len());
|
||||
|
||||
let acc = acc / 100.0;
|
||||
|
||||
if self.n100.or(self.n50).is_some() {
|
||||
@@ -132,14 +145,20 @@ impl<'m> PpCalculator<'m> {
|
||||
/// Consume the calculator and calculate the pp.
|
||||
///
|
||||
/// `stars_func` will be used to calculate stars and other necessary attributes if not already given.
|
||||
pub fn calculate(mut self, stars_func: impl FnOnce(&Beatmap, u32) -> Attributes) -> PpResult {
|
||||
/// Only called if `attributes` was not set.
|
||||
pub fn calculate(
|
||||
mut self,
|
||||
stars_func: impl FnOnce(&Beatmap, u32, Option<usize>) -> Attributes,
|
||||
) -> PpResult {
|
||||
if self.attributes.is_none() {
|
||||
let attributes = stars_func(self.map, self.mods);
|
||||
let attributes = stars_func(self.map, self.mods, self.passed_objects);
|
||||
self.attributes.replace(attributes);
|
||||
}
|
||||
|
||||
if self.acc.is_none() {
|
||||
let n_objects = self.map.hit_objects.len();
|
||||
let n_objects = self
|
||||
.passed_objects
|
||||
.unwrap_or_else(|| self.map.hit_objects.len());
|
||||
|
||||
let remaining = n_objects
|
||||
.saturating_sub(self.n300.unwrap_or(0))
|
||||
|
||||
@@ -15,12 +15,20 @@ const SECTION_LEN: f32 = 400.0;
|
||||
const DIFFICULTY_MULTIPLIER: f32 = 0.0675;
|
||||
|
||||
/// Star calculation for osu!standard maps
|
||||
pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
||||
///
|
||||
/// 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 attributes = map.attributes().mods(mods);
|
||||
let hitwindow = super::difficulty_range(attributes.od).floor() / attributes.clock_rate;
|
||||
let od = (80.0 - hitwindow) / 6.0;
|
||||
|
||||
if map.hit_objects.len() < 2 {
|
||||
if take < 2 {
|
||||
return DifficultyAttributes {
|
||||
ar: attributes.ar,
|
||||
od,
|
||||
@@ -33,6 +41,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
||||
let mut hit_objects = map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.take(take)
|
||||
.map(|h| OsuObject::new(h, map, &attributes));
|
||||
|
||||
let mut skills = vec![Skill::new(SkillKind::Aim), Skill::new(SkillKind::Speed)];
|
||||
|
||||
@@ -27,7 +27,15 @@ const NORMALIZED_RADIUS: f32 = 52.0;
|
||||
/// 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.
|
||||
pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
||||
///
|
||||
/// 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 hitwindow = super::difficulty_range(map_attributes.od).floor() / map_attributes.clock_rate;
|
||||
let od = (80.0 - hitwindow) / 6.0;
|
||||
@@ -38,7 +46,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if map.hit_objects.len() < 2 {
|
||||
if take < 2 {
|
||||
return diff_attributes;
|
||||
}
|
||||
|
||||
@@ -54,7 +62,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
||||
let mut slider_state = SliderState::new(map);
|
||||
let mut ticks_buf = Vec::new();
|
||||
|
||||
let mut hit_objects = map.hit_objects.iter().map(|h| {
|
||||
let mut hit_objects = map.hit_objects.iter().take(take).map(|h| {
|
||||
OsuObject::new(
|
||||
h,
|
||||
map,
|
||||
|
||||
@@ -25,12 +25,20 @@ const NORMALIZED_RADIUS: f32 = 52.0;
|
||||
/// 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.
|
||||
pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
||||
///
|
||||
/// 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 attributes = map.attributes().mods(mods);
|
||||
let hitwindow = super::difficulty_range(attributes.od).floor() / attributes.clock_rate;
|
||||
let od = (80.0 - hitwindow) / 6.0;
|
||||
|
||||
if map.hit_objects.len() < 2 {
|
||||
if take < 2 {
|
||||
return DifficultyAttributes {
|
||||
ar: attributes.ar,
|
||||
od,
|
||||
@@ -52,7 +60,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
||||
let mut n_spinners = 0;
|
||||
let mut state = SliderState::new(&map);
|
||||
|
||||
let mut hit_objects = map.hit_objects.iter().map(|h| match &h.kind {
|
||||
let mut hit_objects = map.hit_objects.iter().take(take).map(|h| match &h.kind {
|
||||
HitObjectKind::Circle => {
|
||||
max_combo += 1;
|
||||
n_circles += 1;
|
||||
|
||||
+8
-3
@@ -27,9 +27,13 @@ const COLOR_SKILL_MULTIPLIER: f32 = 0.01;
|
||||
const RHYTHM_SKILL_MULTIPLIER: f32 = 0.014;
|
||||
const STAMINA_SKILL_MULTIPLIER: f32 = 0.02;
|
||||
|
||||
/// Star calculation for osu!taiko maps
|
||||
pub fn stars(map: &Beatmap, mods: impl Mods) -> f32 {
|
||||
if map.hit_objects.len() < 2 {
|
||||
/// Star calculation for osu!taiko maps.
|
||||
///
|
||||
/// 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>) -> f32 {
|
||||
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||
|
||||
if take < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@@ -53,6 +57,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> f32 {
|
||||
let mut hit_objects = map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.take(take)
|
||||
.enumerate()
|
||||
.skip(2)
|
||||
.zip(map.hit_objects.iter().skip(1))
|
||||
|
||||
+11
-2
@@ -16,7 +16,6 @@ impl PpProvider for Beatmap {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Allow partial plays
|
||||
pub struct PpCalculator<'m> {
|
||||
map: &'m Beatmap,
|
||||
stars: Option<f32>,
|
||||
@@ -25,6 +24,7 @@ pub struct PpCalculator<'m> {
|
||||
combo: Option<usize>,
|
||||
acc: f32,
|
||||
n_misses: usize,
|
||||
passed_objects: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'m> PpCalculator<'m> {
|
||||
@@ -40,6 +40,7 @@ impl<'m> PpCalculator<'m> {
|
||||
combo: None,
|
||||
acc: 1.0,
|
||||
n_misses: 0,
|
||||
passed_objects: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,10 +80,18 @@ impl<'m> PpCalculator<'m> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Amount of passed objects for partial plays, e.g. a fail.
|
||||
#[inline]
|
||||
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
|
||||
self.passed_objects.replace(passed_objects);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn calculate(self) -> PpResult {
|
||||
let stars = self
|
||||
.stars
|
||||
.unwrap_or_else(|| super::stars(self.map, self.mods));
|
||||
.unwrap_or_else(|| super::stars(self.map, self.mods, self.passed_objects));
|
||||
|
||||
let mut multiplier = 1.1;
|
||||
|
||||
|
||||
+15
-8
@@ -4,6 +4,15 @@ use std::cmp::Ordering;
|
||||
|
||||
const DECAY_WEIGHT: f32 = 0.9;
|
||||
|
||||
const COLOR_SKILL_MULTIPLIER: f32 = 1.0;
|
||||
const COLOR_STRAIN_DECAY_BASE: f32 = 0.4;
|
||||
|
||||
const RHYTHM_SKILL_MULTIPLIER: f32 = 10.0;
|
||||
const RHYTHM_STRAIN_DECAY_BASE: f32 = 0.0;
|
||||
|
||||
const STAMINA_SKILL_MULTIPLIER: f32 = 1.0;
|
||||
const STAMINA_STRAIN_DECAY_BASE: f32 = 0.4;
|
||||
|
||||
pub(crate) struct Skill {
|
||||
pub current_strain: f32,
|
||||
current_section_peak: f32,
|
||||
@@ -65,21 +74,19 @@ impl Skill {
|
||||
|
||||
#[inline]
|
||||
fn skill_multiplier(&self) -> f32 {
|
||||
// TODO: Use constants
|
||||
match self.kind {
|
||||
SkillKind::Color { .. } => 1.0,
|
||||
SkillKind::Rhythm { .. } => 10.0,
|
||||
SkillKind::Stamina { .. } => 1.0,
|
||||
SkillKind::Color { .. } => COLOR_SKILL_MULTIPLIER,
|
||||
SkillKind::Rhythm { .. } => RHYTHM_SKILL_MULTIPLIER,
|
||||
SkillKind::Stamina { .. } => STAMINA_SKILL_MULTIPLIER,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn strain_decay_base(&self) -> f32 {
|
||||
// TODO: Use constants
|
||||
match self.kind {
|
||||
SkillKind::Color { .. } => 0.4,
|
||||
SkillKind::Rhythm { .. } => 0.0,
|
||||
SkillKind::Stamina { .. } => 0.4,
|
||||
SkillKind::Color { .. } => COLOR_STRAIN_DECAY_BASE,
|
||||
SkillKind::Rhythm { .. } => RHYTHM_STRAIN_DECAY_BASE,
|
||||
SkillKind::Stamina { .. } => STAMINA_STRAIN_DECAY_BASE,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user