gradual calc for catch
This commit is contained in:
+94
-23
@@ -1,3 +1,5 @@
|
||||
use std::mem;
|
||||
|
||||
use crate::catch::performance::CatchPerformance;
|
||||
|
||||
/// The result of a difficulty calculation on an osu!catch map.
|
||||
@@ -36,6 +38,22 @@ impl CatchDifficultyAttributes {
|
||||
pub fn performance<'a>(self) -> CatchPerformance<'a> {
|
||||
self.into()
|
||||
}
|
||||
|
||||
pub(crate) fn set_object_count(&mut self, count: &ObjectCount) {
|
||||
self.n_fruits = count.fruits;
|
||||
self.n_droplets = count.droplets;
|
||||
self.n_tiny_droplets = count.tiny_droplets;
|
||||
}
|
||||
|
||||
pub(crate) fn add_object_count(&mut self, count: GradualObjectCount) {
|
||||
if count.fruit {
|
||||
self.n_fruits += 1;
|
||||
} else {
|
||||
self.n_droplets += 1;
|
||||
}
|
||||
|
||||
self.n_tiny_droplets += count.tiny_droplets;
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a performance calculation on an osu!catch map.
|
||||
@@ -71,43 +89,96 @@ impl CatchPerformanceAttributes {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CatchDifficultyAttributesBuilder {
|
||||
inner: CatchDifficultyAttributes,
|
||||
take: usize,
|
||||
#[derive(Clone, Default)]
|
||||
pub struct ObjectCount {
|
||||
fruits: u32,
|
||||
droplets: u32,
|
||||
tiny_droplets: u32,
|
||||
}
|
||||
|
||||
impl CatchDifficultyAttributesBuilder {
|
||||
pub const fn new(attrs: CatchDifficultyAttributes, take: usize) -> Self {
|
||||
Self { inner: attrs, take }
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub struct GradualObjectCount {
|
||||
fruit: bool,
|
||||
tiny_droplets: u32,
|
||||
}
|
||||
|
||||
pub enum ObjectCountBuilder {
|
||||
Regular {
|
||||
count: ObjectCount,
|
||||
take: usize,
|
||||
},
|
||||
Gradual {
|
||||
count: GradualObjectCount,
|
||||
all: Vec<GradualObjectCount>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ObjectCountBuilder {
|
||||
pub fn new_regular(take: usize) -> Self {
|
||||
Self::Regular {
|
||||
count: ObjectCount::default(),
|
||||
take,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn into_inner(self) -> CatchDifficultyAttributes {
|
||||
self.inner
|
||||
pub fn new_gradual() -> Self {
|
||||
Self::Gradual {
|
||||
count: GradualObjectCount::default(),
|
||||
all: Vec::with_capacity(512),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn take_more(&self) -> bool {
|
||||
self.take > 0
|
||||
pub fn into_regular(self) -> ObjectCount {
|
||||
if let Self::Regular { count, .. } = self {
|
||||
count
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inc_fruits(&mut self) {
|
||||
Self::inc_value(&mut self.inner.n_fruits, &mut self.take);
|
||||
pub fn into_gradual(self) -> Vec<GradualObjectCount> {
|
||||
if let Self::Gradual { all, .. } = self {
|
||||
all
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inc_droplets(&mut self) {
|
||||
Self::inc_value(&mut self.inner.n_droplets, &mut self.take);
|
||||
pub fn record_fruit(&mut self) {
|
||||
match self {
|
||||
Self::Regular { count, take } => {
|
||||
if *take > 0 {
|
||||
*take -= 1;
|
||||
count.fruits += 1;
|
||||
}
|
||||
}
|
||||
Self::Gradual { count, all } => {
|
||||
count.fruit = true;
|
||||
all.push(mem::take(count));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Should only be used if [`take_more`] returns `true`.
|
||||
///
|
||||
/// [`take_more`]: Self::take_more
|
||||
pub fn inc_tiny_droplets(&mut self) {
|
||||
self.inner.n_tiny_droplets += 1;
|
||||
pub fn record_droplet(&mut self) {
|
||||
match self {
|
||||
Self::Regular { count, take } => {
|
||||
if *take > 0 {
|
||||
*take -= 1;
|
||||
count.droplets += 1;
|
||||
}
|
||||
}
|
||||
Self::Gradual { count, all } => all.push(mem::take(count)),
|
||||
}
|
||||
}
|
||||
|
||||
fn inc_value(value: &mut u32, take: &mut usize) {
|
||||
if *take > 0 {
|
||||
*value += 1;
|
||||
*take -= 1;
|
||||
pub fn record_tiny_droplets(&mut self, n: u32) {
|
||||
match self {
|
||||
Self::Regular { count, take } => {
|
||||
if *take > 0 {
|
||||
count.tiny_droplets += n;
|
||||
}
|
||||
}
|
||||
Self::Gradual { count, .. } => count.tiny_droplets += n,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
attributes::CatchDifficultyAttributesBuilder,
|
||||
attributes::ObjectCountBuilder,
|
||||
catcher::Catcher,
|
||||
object::{
|
||||
banana_shower::BananaShower,
|
||||
@@ -42,7 +42,7 @@ pub fn try_convert(map: &mut Cow<'_, Beatmap>) -> ConvertStatus {
|
||||
|
||||
pub fn convert_objects(
|
||||
converted: &CatchBeatmap<'_>,
|
||||
attrs: &mut CatchDifficultyAttributesBuilder,
|
||||
count: &mut ObjectCountBuilder,
|
||||
hr: bool,
|
||||
cs: f32,
|
||||
) -> Vec<PalpableObject> {
|
||||
@@ -62,7 +62,7 @@ pub fn convert_objects(
|
||||
let mut last_start_time = 0.0;
|
||||
|
||||
for h in converted.map.hit_objects.iter() {
|
||||
let mut new_objects = convert_object(h, converted, attrs, &mut bufs);
|
||||
let mut new_objects = convert_object(h, converted, count, &mut bufs);
|
||||
|
||||
apply_pos_offset(
|
||||
&mut new_objects,
|
||||
@@ -96,14 +96,14 @@ pub fn convert_objects(
|
||||
fn convert_object<'a>(
|
||||
h: &'a HitObject,
|
||||
converted: &CatchBeatmap<'_>,
|
||||
attrs: &mut CatchDifficultyAttributesBuilder,
|
||||
count: &mut ObjectCountBuilder,
|
||||
bufs: &'a mut JuiceStreamBufs,
|
||||
) -> ObjectIter<'a> {
|
||||
let state = match h.kind {
|
||||
HitObjectKind::Circle => ObjectIterState::Fruit(Some(Fruit::new(attrs))),
|
||||
HitObjectKind::Circle => ObjectIterState::Fruit(Some(Fruit::new(count))),
|
||||
HitObjectKind::Slider(ref slider) => {
|
||||
let x = JuiceStream::clamp_to_playfield(h.pos.x);
|
||||
let stream = JuiceStream::new(x, h.start_time, slider, converted, attrs, bufs);
|
||||
let stream = JuiceStream::new(x, h.start_time, slider, converted, count, bufs);
|
||||
|
||||
ObjectIterState::JuiceStream(stream)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
use crate::{
|
||||
any::difficulty::skills::Skill,
|
||||
catch::{
|
||||
attributes::{GradualObjectCount, ObjectCountBuilder},
|
||||
convert::convert_objects,
|
||||
CatchBeatmap, CatchDifficultyAttributes,
|
||||
},
|
||||
util::mods::Mods,
|
||||
ModeDifficulty,
|
||||
};
|
||||
|
||||
use super::{
|
||||
object::CatchDifficultyObject, skills::movement::Movement, CatchDifficultySetup,
|
||||
DifficultyValues,
|
||||
};
|
||||
|
||||
/// Gradually calculate the difficulty attributes of an osu!catch map.
|
||||
///
|
||||
/// Note that this struct implements [`Iterator`].
|
||||
/// On every call of [`Iterator::next`], the map's next fruit or droplet
|
||||
/// will be processed and the [`CatchDifficultyAttributes`] will be updated and
|
||||
/// returned.
|
||||
///
|
||||
/// Note that it does not return attributes after a tiny droplet. Only for
|
||||
/// fruits and droplets.
|
||||
///
|
||||
/// If you want to calculate performance attributes, use
|
||||
/// [`CatchGradualPerformance`] instead.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use rosu_pp::{Beatmap, ModeDifficulty};
|
||||
/// use rosu_pp::catch::{Catch, CatchGradualDifficulty};
|
||||
///
|
||||
/// let map = Beatmap::from_path();
|
||||
/// .unwrap()
|
||||
/// .unchecked_into_converted::<Catch>();
|
||||
///
|
||||
/// let difficulty = ModeDifficulty::new().mods(64); // DT
|
||||
/// let mut iter = CatchGradualDifficulty::new(&difficulty, &converted);
|
||||
///
|
||||
/// // the difficulty of the map after the first hit object
|
||||
/// let attrs1 = iter.next();
|
||||
/// // ... after the second hit object
|
||||
/// let attrs2 = iter.next();
|
||||
///
|
||||
/// // Remaining hit objects
|
||||
/// for difficulty in iter {
|
||||
/// // ...
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// [`CatchGradualPerformance`]: crate::catch::CatchGradualPerformance
|
||||
pub struct CatchGradualDifficulty {
|
||||
pub(crate) idx: usize,
|
||||
pub(crate) mods: u32,
|
||||
pub(crate) clock_rate: f64,
|
||||
attrs: CatchDifficultyAttributes,
|
||||
/// The delta of object counts after each palpable object
|
||||
count: Vec<GradualObjectCount>,
|
||||
diff_objects: Box<[CatchDifficultyObject]>,
|
||||
movement: Movement,
|
||||
}
|
||||
|
||||
impl CatchGradualDifficulty {
|
||||
pub fn new(difficulty: &ModeDifficulty, converted: &CatchBeatmap<'_>) -> Self {
|
||||
let mods = difficulty.get_mods();
|
||||
let clock_rate = difficulty.get_clock_rate();
|
||||
|
||||
let CatchDifficultySetup { map_attrs, attrs } =
|
||||
CatchDifficultySetup::new(difficulty, converted);
|
||||
|
||||
let hr = mods.hr();
|
||||
let mut count = ObjectCountBuilder::new_gradual();
|
||||
let palpable_objects = convert_objects(converted, &mut count, hr, map_attrs.cs as f32);
|
||||
|
||||
let diff_objects = DifficultyValues::create_difficulty_objects(
|
||||
&map_attrs,
|
||||
clock_rate,
|
||||
palpable_objects.iter(),
|
||||
);
|
||||
|
||||
let count = count.into_gradual();
|
||||
let movement = Movement::new(clock_rate);
|
||||
|
||||
Self {
|
||||
idx: 0,
|
||||
mods,
|
||||
clock_rate,
|
||||
attrs,
|
||||
count,
|
||||
diff_objects,
|
||||
movement,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for CatchGradualDifficulty {
|
||||
type Item = CatchDifficultyAttributes;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// The first difficulty object belongs to the second palpable object
|
||||
// since each difficulty object requires the current and the last note.
|
||||
// Hence, if we're still on the first object, we don't have a difficulty
|
||||
// object yet and just skip processing.
|
||||
if self.idx > 0 {
|
||||
let curr = self.diff_objects.get(self.idx - 1)?;
|
||||
Skill::new(&mut self.movement, &self.diff_objects).process(curr);
|
||||
} else if self.count.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.attrs.add_object_count(self.count[self.idx]);
|
||||
self.idx += 1;
|
||||
|
||||
let mut attrs = self.attrs.clone();
|
||||
|
||||
let movement = self.movement.as_difficulty_value();
|
||||
DifficultyValues::eval(&mut attrs, movement);
|
||||
|
||||
Some(attrs)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let len = self.len();
|
||||
|
||||
(len, Some(len))
|
||||
}
|
||||
|
||||
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
||||
let skip_iter = self.diff_objects.iter().skip(self.idx.saturating_sub(1));
|
||||
|
||||
let mut take = n.min(self.len().saturating_sub(1));
|
||||
|
||||
// The first palpable object has no difficulty object
|
||||
if self.idx == 0 && take > 0 {
|
||||
take -= 1;
|
||||
self.attrs.add_object_count(self.count[self.idx]);
|
||||
self.idx += 1;
|
||||
}
|
||||
|
||||
let mut movement = Skill::new(&mut self.movement, &self.diff_objects);
|
||||
|
||||
for curr in skip_iter.take(take) {
|
||||
movement.process(curr);
|
||||
|
||||
self.attrs.add_object_count(self.count[self.idx]);
|
||||
self.idx += 1;
|
||||
}
|
||||
|
||||
self.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for CatchGradualDifficulty {
|
||||
fn len(&self) -> usize {
|
||||
self.diff_objects.len() + 1 - self.idx
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Beatmap;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let converted = Beatmap::from_bytes(&[]).unwrap().unchecked_into_converted();
|
||||
|
||||
let difficulty = ModeDifficulty::new();
|
||||
let mut gradual = CatchGradualDifficulty::new(&difficulty, &converted);
|
||||
|
||||
assert!(gradual.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_and_nth() {
|
||||
let converted = Beatmap::from_path("./resources/2118524.osu")
|
||||
.unwrap()
|
||||
.unchecked_into_converted();
|
||||
|
||||
let difficulty = ModeDifficulty::new();
|
||||
|
||||
let mut gradual = CatchGradualDifficulty::new(&difficulty, &converted);
|
||||
let mut gradual_2nd = CatchGradualDifficulty::new(&difficulty, &converted);
|
||||
let mut gradual_3rd = CatchGradualDifficulty::new(&difficulty, &converted);
|
||||
|
||||
for i in 1.. {
|
||||
let Some(next_gradual) = gradual.next() else {
|
||||
assert_eq!(i, 731);
|
||||
assert!(gradual_2nd.last().is_none()); // 730 % 2 == 0
|
||||
assert!(gradual_3rd.last().is_some()); // 730 % 3 == 1
|
||||
break;
|
||||
};
|
||||
|
||||
if i % 2 == 0 {
|
||||
let next_gradual_2nd = gradual_2nd.nth(1).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_2nd);
|
||||
}
|
||||
|
||||
if i % 3 == 0 {
|
||||
let next_gradual_3rd = gradual_3rd.nth(2).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_3rd);
|
||||
}
|
||||
|
||||
let expected = ModeDifficulty::new()
|
||||
.passed_objects(i as u32)
|
||||
.calculate(&converted);
|
||||
|
||||
assert_eq!(next_gradual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
+70
-36
@@ -3,16 +3,19 @@ use crate::{
|
||||
catch::{
|
||||
catcher::Catcher, convert::convert_objects, difficulty::object::CatchDifficultyObject,
|
||||
},
|
||||
model::beatmap::BeatmapAttributes,
|
||||
util::mods::Mods,
|
||||
};
|
||||
|
||||
use self::skills::movement::Movement;
|
||||
|
||||
use super::{
|
||||
attributes::{CatchDifficultyAttributes, CatchDifficultyAttributesBuilder},
|
||||
attributes::{CatchDifficultyAttributes, ObjectCountBuilder},
|
||||
convert::CatchBeatmap,
|
||||
object::palpable::PalpableObject,
|
||||
};
|
||||
|
||||
pub mod gradual;
|
||||
mod object;
|
||||
mod skills;
|
||||
|
||||
@@ -27,11 +30,37 @@ pub fn difficulty(
|
||||
mut attrs,
|
||||
} = DifficultyValues::calculate(difficulty, converted);
|
||||
|
||||
attrs.stars = movement.difficulty_value().sqrt() * STAR_SCALING_FACTOR;
|
||||
DifficultyValues::eval(&mut attrs, movement.difficulty_value());
|
||||
|
||||
attrs
|
||||
}
|
||||
|
||||
pub struct CatchDifficultySetup {
|
||||
map_attrs: BeatmapAttributes,
|
||||
attrs: CatchDifficultyAttributes,
|
||||
}
|
||||
|
||||
impl CatchDifficultySetup {
|
||||
pub fn new(difficulty: &ModeDifficulty, converted: &CatchBeatmap<'_>) -> Self {
|
||||
let mods = difficulty.get_mods();
|
||||
let clock_rate = difficulty.get_clock_rate();
|
||||
|
||||
let map_attrs = converted
|
||||
.attributes()
|
||||
.mods(mods)
|
||||
.clock_rate(clock_rate)
|
||||
.build();
|
||||
|
||||
let attrs = CatchDifficultyAttributes {
|
||||
ar: map_attrs.ar,
|
||||
is_convert: converted.is_convert,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Self { map_attrs, attrs }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DifficultyValues {
|
||||
pub movement: Movement,
|
||||
pub attrs: CatchDifficultyAttributes,
|
||||
@@ -40,33 +69,51 @@ pub struct DifficultyValues {
|
||||
impl DifficultyValues {
|
||||
pub fn calculate(difficulty: &ModeDifficulty, converted: &CatchBeatmap<'_>) -> Self {
|
||||
let take = difficulty.get_passed_objects();
|
||||
let mods = difficulty.get_mods();
|
||||
let clock_rate = difficulty.get_clock_rate();
|
||||
|
||||
let map_attrs = converted
|
||||
.attributes()
|
||||
.mods(difficulty.get_mods())
|
||||
.clock_rate(clock_rate)
|
||||
.build();
|
||||
let CatchDifficultySetup {
|
||||
map_attrs,
|
||||
mut attrs,
|
||||
} = CatchDifficultySetup::new(difficulty, converted);
|
||||
|
||||
let attrs = CatchDifficultyAttributes {
|
||||
ar: map_attrs.ar,
|
||||
is_convert: converted.is_convert,
|
||||
..Default::default()
|
||||
};
|
||||
let hr = mods.hr();
|
||||
let mut count = ObjectCountBuilder::new_regular(take);
|
||||
|
||||
let mut attrs = CatchDifficultyAttributesBuilder::new(attrs, take);
|
||||
let palpable_objects = convert_objects(converted, &mut count, hr, map_attrs.cs as f32);
|
||||
|
||||
let diff_objects = Self::create_difficulty_objects(
|
||||
&map_attrs,
|
||||
clock_rate,
|
||||
palpable_objects.iter().take(take),
|
||||
);
|
||||
|
||||
let hr = difficulty.get_mods().hr();
|
||||
let mut movement = Movement::new(clock_rate);
|
||||
|
||||
let palpable_objects = convert_objects(converted, &mut attrs, hr, map_attrs.cs as f32);
|
||||
let mut palpable_objects_iter = palpable_objects.iter().take(take);
|
||||
{
|
||||
let mut movement = Skill::new(&mut movement, &diff_objects);
|
||||
|
||||
let Some(mut last_object) = palpable_objects_iter.next() else {
|
||||
return Self {
|
||||
movement,
|
||||
attrs: attrs.into_inner(),
|
||||
};
|
||||
for curr in diff_objects.iter() {
|
||||
movement.process(curr);
|
||||
}
|
||||
}
|
||||
|
||||
attrs.set_object_count(&count.into_regular());
|
||||
|
||||
Self { movement, attrs }
|
||||
}
|
||||
|
||||
pub fn eval(attrs: &mut CatchDifficultyAttributes, movement_difficulty_value: f64) {
|
||||
attrs.stars = movement_difficulty_value.sqrt() * STAR_SCALING_FACTOR;
|
||||
}
|
||||
|
||||
pub fn create_difficulty_objects<'a>(
|
||||
map_attrs: &BeatmapAttributes,
|
||||
clock_rate: f64,
|
||||
mut palpable_objects: impl ExactSizeIterator<Item = &'a PalpableObject>,
|
||||
) -> Box<[CatchDifficultyObject]> {
|
||||
let Some(mut last_object) = palpable_objects.next() else {
|
||||
return Box::default();
|
||||
};
|
||||
|
||||
let mut half_catcher_width = Catcher::calculate_catch_width(map_attrs.cs as f32) * 0.5;
|
||||
@@ -74,7 +121,7 @@ impl DifficultyValues {
|
||||
let scaling_factor =
|
||||
CatchDifficultyObject::NORMALIZED_HITOBJECT_RADIUS / half_catcher_width;
|
||||
|
||||
let diff_objects: Vec<_> = palpable_objects_iter
|
||||
palpable_objects
|
||||
.enumerate()
|
||||
.map(|(i, hit_object)| {
|
||||
let diff_object = CatchDifficultyObject::new(
|
||||
@@ -88,19 +135,6 @@ impl DifficultyValues {
|
||||
|
||||
diff_object
|
||||
})
|
||||
.collect();
|
||||
|
||||
{
|
||||
let mut movement = Skill::new(&mut movement, &diff_objects);
|
||||
|
||||
for curr in diff_objects.iter() {
|
||||
movement.process(curr);
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
movement,
|
||||
attrs: attrs.into_inner(),
|
||||
}
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,17 @@ impl Movement {
|
||||
}
|
||||
|
||||
pub fn difficulty_value(self) -> f64 {
|
||||
self.inner.difficulty_value(DECAY_WEIGHT)
|
||||
Self::static_difficulty_value(self.inner)
|
||||
}
|
||||
|
||||
/// Use [`difficulty_value`] instead whenever possible because
|
||||
/// [`as_difficulty_value`] clones internally.
|
||||
pub fn as_difficulty_value(&self) -> f64 {
|
||||
Self::static_difficulty_value(self.inner.clone())
|
||||
}
|
||||
|
||||
fn static_difficulty_value(skill: StrainDecaySkill) -> f64 {
|
||||
skill.difficulty_value(DECAY_WEIGHT)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -11,7 +11,8 @@ use crate::{
|
||||
pub use self::{
|
||||
attributes::{CatchDifficultyAttributes, CatchPerformanceAttributes},
|
||||
convert::CatchBeatmap,
|
||||
performance::CatchPerformance,
|
||||
difficulty::gradual::CatchGradualDifficulty,
|
||||
performance::{gradual::CatchGradualPerformance, CatchPerformance},
|
||||
score_state::CatchScoreState,
|
||||
strains::CatchStrains,
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::catch::attributes::CatchDifficultyAttributesBuilder;
|
||||
use crate::catch::attributes::ObjectCountBuilder;
|
||||
|
||||
pub struct Fruit {
|
||||
pub x_offset: f32,
|
||||
}
|
||||
|
||||
impl Fruit {
|
||||
pub fn new(attrs: &mut CatchDifficultyAttributesBuilder) -> Self {
|
||||
attrs.inc_fruits();
|
||||
pub fn new(count: &mut ObjectCountBuilder) -> Self {
|
||||
count.record_fruit();
|
||||
|
||||
Self { x_offset: 0.0 }
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use rosu_map::section::hit_objects::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
catch::{attributes::CatchDifficultyAttributesBuilder, convert::CatchBeatmap, PLAYFIELD_WIDTH},
|
||||
catch::{attributes::ObjectCountBuilder, convert::CatchBeatmap, PLAYFIELD_WIDTH},
|
||||
model::{
|
||||
control_point::{DifficultyPoint, TimingPoint},
|
||||
hit_object::Slider,
|
||||
@@ -25,7 +25,7 @@ impl<'a> JuiceStream<'a> {
|
||||
start_time: f64,
|
||||
slider: &'a Slider,
|
||||
converted: &CatchBeatmap<'_>,
|
||||
attrs: &mut CatchDifficultyAttributesBuilder,
|
||||
count: &mut ObjectCountBuilder,
|
||||
bufs: &'a mut JuiceStreamBufs,
|
||||
) -> Self {
|
||||
let slider_multiplier = converted.map.slider_multiplier;
|
||||
@@ -69,7 +69,8 @@ impl<'a> JuiceStream<'a> {
|
||||
let mut last_event_time = None;
|
||||
|
||||
for e in events {
|
||||
if let Some(last_event_time) = last_event_time.filter(|_| attrs.take_more()) {
|
||||
if let Some(last_event_time) = last_event_time {
|
||||
let mut tiny_droplets = 0;
|
||||
let since_last_tick = e.time - last_event_time;
|
||||
|
||||
if since_last_tick > 80.0 {
|
||||
@@ -82,7 +83,7 @@ impl<'a> JuiceStream<'a> {
|
||||
let mut t = time_between_tiny;
|
||||
|
||||
while t < since_last_tick {
|
||||
attrs.inc_tiny_droplets();
|
||||
tiny_droplets += 1;
|
||||
|
||||
let nested = NestedJuiceStreamObject {
|
||||
pos: 0.0, // not important
|
||||
@@ -95,18 +96,20 @@ impl<'a> JuiceStream<'a> {
|
||||
t += time_between_tiny;
|
||||
}
|
||||
}
|
||||
|
||||
count.record_tiny_droplets(tiny_droplets);
|
||||
}
|
||||
|
||||
last_event_time = Some(e.time);
|
||||
|
||||
let kind = match e.kind {
|
||||
SliderEventType::Tick => {
|
||||
attrs.inc_droplets();
|
||||
count.record_droplet();
|
||||
|
||||
NestedJuiceStreamObjectKind::Droplet
|
||||
}
|
||||
SliderEventType::Head | SliderEventType::Repeat | SliderEventType::Tail => {
|
||||
attrs.inc_fruits();
|
||||
count.record_fruit();
|
||||
|
||||
NestedJuiceStreamObjectKind::Fruit
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
use crate::{
|
||||
catch::{CatchBeatmap, CatchGradualDifficulty, CatchPerformanceAttributes, CatchScoreState},
|
||||
ModeDifficulty,
|
||||
};
|
||||
|
||||
/// Gradually calculate the performance attributes of an osu!catch map.
|
||||
///
|
||||
/// After each hit object you can call [`next`] and it will return the resulting
|
||||
/// current [`CatchPerformanceAttributes`]. To process multiple objects at once,
|
||||
/// use [`nth`] instead.
|
||||
///
|
||||
/// Both methods require a [`CatchScoreState`] that contains the current
|
||||
/// hitresults as well as the maximum combo so far.
|
||||
///
|
||||
/// Note that neither hits nor misses of tiny droplets require to be processed.
|
||||
/// Only fruits and droplets do.
|
||||
///
|
||||
/// If you only want to calculate difficulty attributes use
|
||||
/// [`CatchGradualDifficulty`] instead.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use rosu_pp::{Beatmap, ModeDifficulty};
|
||||
/// use rosu_pp::catch::{Catch, CatchGradualPerformance, CatchScoreState};
|
||||
///
|
||||
/// let converted = Beatmap::from_path()
|
||||
/// .unwrap()
|
||||
/// .unchecked_into_converted::<Catch>();
|
||||
///
|
||||
/// let difficulty = ModeDifficulty::new().mods(64); // DT
|
||||
/// let mut gradual_perf = CatchGradualPerformance::new(&difficulty, &converted);
|
||||
/// let mut state = CatchScoreState::new(); // empty state, everything is on 0.
|
||||
///
|
||||
/// // The first 10 hitresults are only fruits
|
||||
/// for _ in 0..10 {
|
||||
/// state.n_fruits += 1;
|
||||
/// state.max_combo += 1;
|
||||
///
|
||||
/// let performance = gradual_perf.next(state.clone()).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
/// }
|
||||
///
|
||||
/// // Then comes a miss.
|
||||
/// // Note that state's max combo won't be incremented for
|
||||
/// // the next few objects because the combo is reset.
|
||||
/// state.n_misses += 1;
|
||||
/// let performance = gradual_perf.next(state.clone()).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
///
|
||||
/// // The next 10 objects will be a mixture of fruits and droplets.
|
||||
/// // Notice how tiny droplets from sliders do not count as hit objects
|
||||
/// // that require processing. Only fruits and droplets do.
|
||||
/// // Also notice how all 10 objects will be processed in one go.
|
||||
/// state.n_fruits += 4;
|
||||
/// state.n_droplets += 6;
|
||||
/// state.n_tiny_droplets += 12;
|
||||
/// // The `nth` method takes a zero-based value.
|
||||
/// let performance = gradual_perf.nth(state.clone(), 9).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
///
|
||||
/// // Now comes another fruit. Note that the max combo gets incremented again.
|
||||
/// state.n_fruits += 1;
|
||||
/// state.max_combo += 1;
|
||||
/// let performance = gradual_perf.next(state.clone()).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
///
|
||||
/// // Skip to the end
|
||||
/// # /*
|
||||
/// state.max_combo = ...
|
||||
/// state.n_fruits = ...
|
||||
/// state.n_droplets = ...
|
||||
/// state.n_tiny_droplets = ...
|
||||
/// state.n_tiny_droplet_misses = ...
|
||||
/// state.n_misses = ...
|
||||
/// # */
|
||||
/// let final_performance = gradual_perf.last(state.clone()).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
///
|
||||
/// // Once the final performance has been calculated,
|
||||
/// // attempting to process further objects will return `None`.
|
||||
/// assert!(gradual_perf.next(state).is_none());
|
||||
/// ```
|
||||
///
|
||||
/// [`next`]: CatchGradualPerformance::next
|
||||
/// [`nth`]: CatchGradualPerformance::nth
|
||||
pub struct CatchGradualPerformance {
|
||||
difficulty: CatchGradualDifficulty,
|
||||
}
|
||||
|
||||
impl CatchGradualPerformance {
|
||||
/// Create a new gradual performance calculator for osu!catch maps.
|
||||
pub fn new(difficulty: &ModeDifficulty, converted: &CatchBeatmap<'_>) -> Self {
|
||||
let difficulty = CatchGradualDifficulty::new(difficulty, converted);
|
||||
|
||||
Self { difficulty }
|
||||
}
|
||||
|
||||
/// Process the next hit object and calculate the performance attributes
|
||||
/// for the resulting score state.
|
||||
///
|
||||
/// Note that neither hits nor misses of tiny droplets require to be
|
||||
/// processed. Only fruits and droplets do.
|
||||
pub fn next(&mut self, state: CatchScoreState) -> Option<CatchPerformanceAttributes> {
|
||||
self.nth(state, 0)
|
||||
}
|
||||
|
||||
/// Process all remaining hit objects and calculate the final performance
|
||||
/// attributes.
|
||||
pub fn last(&mut self, state: CatchScoreState) -> Option<CatchPerformanceAttributes> {
|
||||
self.nth(state, usize::MAX)
|
||||
}
|
||||
|
||||
/// Process everything up the the next `n`th hit object and calculate the
|
||||
/// performance attributes for the resulting score state.
|
||||
///
|
||||
/// Note that the count is zero-indexed, so `n=0` will process 1 object,
|
||||
/// `n=1` will process 2, and so on.
|
||||
pub fn nth(&mut self, state: CatchScoreState, n: usize) -> Option<CatchPerformanceAttributes> {
|
||||
let performance = self
|
||||
.difficulty
|
||||
.nth(n)?
|
||||
.performance()
|
||||
.state(state)
|
||||
.mods(self.difficulty.mods)
|
||||
.clock_rate(self.difficulty.clock_rate)
|
||||
.passed_objects(self.difficulty.idx as u32)
|
||||
.calculate();
|
||||
|
||||
Some(performance)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{catch::CatchPerformance, Beatmap};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn next_and_nth() {
|
||||
let converted = Beatmap::from_path("./resources/2118524.osu")
|
||||
.unwrap()
|
||||
.unchecked_into_converted();
|
||||
|
||||
let mods = 88; // HDHRDT
|
||||
let difficulty = ModeDifficulty::new().mods(88);
|
||||
|
||||
let mut gradual = CatchGradualPerformance::new(&difficulty, &converted);
|
||||
let mut gradual_2nd = CatchGradualPerformance::new(&difficulty, &converted);
|
||||
let mut gradual_3rd = CatchGradualPerformance::new(&difficulty, &converted);
|
||||
|
||||
let mut state = CatchScoreState::default();
|
||||
|
||||
for i in 1.. {
|
||||
state.n_misses += 1;
|
||||
|
||||
let Some(next_gradual) = gradual.next(state.clone()) else {
|
||||
assert_eq!(i, 731);
|
||||
assert!(gradual_2nd.last(state.clone()).is_none()); // 730 % 2 == 0
|
||||
assert!(gradual_3rd.last(state.clone()).is_some()); // 730 % 3 == 1
|
||||
break;
|
||||
};
|
||||
|
||||
if i % 2 == 0 {
|
||||
let next_gradual_2nd = gradual_2nd.nth(state.clone(), 1).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_2nd);
|
||||
}
|
||||
|
||||
if i % 3 == 0 {
|
||||
let next_gradual_3rd = gradual_3rd.nth(state.clone(), 2).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_3rd);
|
||||
}
|
||||
|
||||
let regular_calc = CatchPerformance::new(converted.as_owned())
|
||||
.mods(mods)
|
||||
.passed_objects(i as u32)
|
||||
.state(state.clone());
|
||||
|
||||
let expected = regular_calc.calculate();
|
||||
|
||||
assert_eq!(next_gradual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ use super::{
|
||||
Catch,
|
||||
};
|
||||
|
||||
pub mod gradual;
|
||||
|
||||
/// Performance calculator on osu!catch maps.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[must_use]
|
||||
Reference in New Issue
Block a user