rosu-pp 4.0.1

Difficulty and performance calculation for osu!
Documentation
use std::cmp;

use rosu_map::section::general::GameMode;

use crate::{
    Beatmap,
    any::{
        CalculateError,
        difficulty::{Difficulty, skills::StrainSkill},
    },
    mania::{
        convert::prepare_map,
        difficulty::{object::ManiaDifficultyObject, skills::strain::Strain},
        object::{ManiaObject, ObjectParams},
    },
    model::mode::ConvertError,
    util::sync::RefCount,
};

use super::attributes::ManiaDifficultyAttributes;

mod evaluators;
pub mod gradual;
mod object;
mod skills;

const DIFFICULTY_MULTIPLIER: f64 = 0.018;

pub fn difficulty(
    difficulty: &Difficulty,
    map: &Beatmap,
) -> Result<ManiaDifficultyAttributes, ConvertError> {
    let map = prepare_map(difficulty, map)?;

    Ok(calculate_difficulty(difficulty, &map))
}

pub fn checked_difficulty(
    difficulty: &Difficulty,
    map: &Beatmap,
) -> Result<ManiaDifficultyAttributes, CalculateError> {
    let map = prepare_map(difficulty, map)?;
    map.check_suspicion()?;

    Ok(calculate_difficulty(difficulty, &map))
}

fn calculate_difficulty(difficulty: &Difficulty, map: &Beatmap) -> ManiaDifficultyAttributes {
    debug_assert_eq!(map.mode, GameMode::Mania);

    let n_objects = cmp::min(difficulty.get_passed_objects(), map.hit_objects.len()) as u32;

    let values = DifficultyValues::calculate(difficulty, map);

    ManiaDifficultyAttributes {
        stars: values.strain.into_difficulty_value() * DIFFICULTY_MULTIPLIER,
        max_combo: values.max_combo,
        n_objects,
        n_hold_notes: values.n_hold_notes,
        is_convert: map.is_convert,
    }
}

pub struct DifficultyValues {
    pub strain: Strain,
    pub max_combo: u32,
    pub n_hold_notes: u32,
}

impl DifficultyValues {
    pub fn calculate(difficulty: &Difficulty, map: &Beatmap) -> Self {
        let take = difficulty.get_passed_objects();
        let total_columns = map.cs.round_ties_even().max(1.0);
        let clock_rate = difficulty.get_clock_rate();
        let mut params = ObjectParams::new(map);

        let mania_objects = map
            .hit_objects
            .iter()
            .map(|h| ManiaObject::new(h, total_columns, &mut params))
            .take(take);

        let diff_objects =
            Self::create_difficulty_objects(clock_rate, total_columns as usize, mania_objects);

        let mut strain = Strain::new(total_columns as usize);

        for curr in diff_objects.iter() {
            strain.process(&curr.get(), &diff_objects);
        }

        Self {
            strain,
            max_combo: params.max_combo(),
            n_hold_notes: params.n_hold_notes(),
        }
    }

    pub fn create_difficulty_objects(
        clock_rate: f64,
        total_columns: usize,
        mut mania_objects: impl ExactSizeIterator<Item = ManiaObject>,
    ) -> Box<[RefCount<ManiaDifficultyObject>]> {
        let Some(mut prev) = mania_objects.next() else {
            return Box::default();
        };

        let n_diff_objects = mania_objects.len();

        let mut objects = Vec::with_capacity(n_diff_objects);
        let mut per_column_objects = vec![Vec::new(); total_columns].into_boxed_slice();

        for curr in mania_objects {
            let curr_obj =
                ManiaDifficultyObject::new(&curr, &prev, clock_rate, &objects, &per_column_objects);

            let column = curr_obj.column;
            let curr_obj = RefCount::new(curr_obj);
            per_column_objects[column].push(curr_obj.downgrade());
            objects.push(curr_obj);

            prev = curr;
        }

        debug_assert_eq!(n_diff_objects, objects.len());

        objects.into_boxed_slice()
    }
}