quaver-rs 0.1.0

A Rust library for parsing and analyzing Quaver rhythm game maps
Documentation
use crate::rulesets::structs::{StrainSolverData, Hand};

/// Calculation functions for difficulty processing

/// Used to calculate Coefficient for Strain Difficulty
pub fn get_coefficient_value(
    duration: f32,
    x_min: f32,
    x_max: f32,
    strain_max: f32,
    exp: f32,
    average_note_density: f32,
) -> f32 {
    const LOWEST_DIFFICULTY: f32 = 1.0;
    const DENSITY_MULTIPLIER: f32 = 0.266;
    const DENSITY_DIFFICULTY_MIN: f32 = 0.4;

    // Calculate ratio between min and max value
    let ratio = (1.0 - (duration - x_min) / (x_max - x_min)).max(0.0);

    // If ratio is too big and map isn't a beginner map (nps > 4) scale based on nps instead
    if ratio == 0.0 && average_note_density < 4.0 {
        // If note density is too low don't bother calculating for density either
        if average_note_density < 1.0 {
            return DENSITY_DIFFICULTY_MIN;
        }
        return average_note_density * DENSITY_MULTIPLIER + 0.134;
    }

    // Compute for difficulty
    LOWEST_DIFFICULTY + (strain_max - LOWEST_DIFFICULTY) * ratio.powf(exp)
}

/// Calculate overall difficulty of a map
pub fn calculate_overall_difficulty(
    strain_solver_data: &mut [StrainSolverData],
    map_start: f32,
    map_end: f32,
    use_fallback: bool,
) -> f32 {
    // When the map has only scratch key notes, StrainSolverData would be empty, so we return 0
    if strain_solver_data.is_empty() {
        return 0.0;
    }

    // Solve strain value of every data point
    for data in strain_solver_data.iter_mut() {
        data.calculate_strain_value();
    }

    let calculated_diff = strain_solver_data
        .iter()
        .filter(|s| matches!(s.hand, Hand::Left | Hand::Right))
        .map(|s| s.total_strain_value)
        .sum::<f32>()
        / strain_solver_data
            .iter()
            .filter(|s| matches!(s.hand, Hand::Left | Hand::Right))
            .count() as f32;

    let bins = create_difficulty_bins(strain_solver_data, map_start, map_end, use_fallback);

    if !bins.iter().any(|&strain| strain > 0.0) {
        return 0.0;
    }

    let (continuity_adjustment, continuity) = calculate_continuity_adjustment(&bins);
    let short_map_adjustment = calculate_short_map_adjustment(&bins, continuity);

    calculated_diff * continuity_adjustment * short_map_adjustment
}

/// Create difficulty bins for analysis
fn create_difficulty_bins(
    strain_solver_data: &[StrainSolverData],
    map_start: f32,
    map_end: f32,
    use_fallback: bool,
) -> Vec<f32> {
    let mut bins = Vec::new();
    const BIN_SIZE: f32 = 1000.0;

    let mut left_index = 0;
    let mut right_index = 0;

    // Find starting index
    while left_index < strain_solver_data.len() && strain_solver_data[left_index].start_time < map_start {
        left_index += 1;
    }

    for i in (map_start as i32..map_end as i32).step_by(BIN_SIZE as usize) {
        let bin_start = i as f32;
        let bin_end = bin_start + BIN_SIZE;

        let values_in_bin: Vec<&StrainSolverData> = if use_fallback {
            strain_solver_data
                .iter()
                .filter(|s| s.start_time >= bin_start && s.start_time < bin_end)
                .collect()
        } else {
            while right_index < strain_solver_data.len() - 1 
                && strain_solver_data[right_index + 1].start_time < bin_end {
                right_index += 1;
            }

            if left_index >= strain_solver_data.len() {
                bins.push(0.0);
                continue;
            }

            strain_solver_data[left_index..=right_index].iter().collect()
        };

        let average_rating = if values_in_bin.is_empty() {
            0.0
        } else {
            values_in_bin.iter().map(|s| s.total_strain_value).sum::<f32>() / values_in_bin.len() as f32
        };

        bins.push(average_rating);
        left_index = right_index + 1;
    }

    bins
}

/// Calculate continuity adjustment for difficulty
pub fn calculate_continuity_adjustment(bins: &[f32]) -> (f32, f32) {
    // Average of the hardest 40% of the map
    let cutoff_pos = (bins.len() as f32 * 0.4).floor() as usize;
    let mut sorted_bins = bins.to_vec();
    sorted_bins.sort_by(|a, b| b.partial_cmp(a).unwrap());
    
    let top_40 = &sorted_bins[..cutoff_pos];
    let easy_rating_cutoff = if top_40.is_empty() {
        0.0
    } else {
        top_40.iter().sum::<f32>() / top_40.len() as f32
    };

    // Calculate continuity - this should match the C# implementation exactly
    let continuity = if easy_rating_cutoff > 0.0 {
        let non_zero_bins: Vec<f32> = bins.iter()
            .filter(|&&strain| strain > 0.0)
            .map(|&strain| (strain / easy_rating_cutoff).sqrt())
            .collect();
        
        if !non_zero_bins.is_empty() {
            non_zero_bins.iter().sum::<f32>() / non_zero_bins.len() as f32
        } else {
            0.0
        }
    } else {
        0.0
    };

    // Apply continuity adjustment
    const MAX_CONTINUITY: f32 = 1.00;
    const AVG_CONTINUITY: f32 = 0.85;
    const MIN_CONTINUITY: f32 = 0.60;

    const MAX_ADJUSTMENT: f32 = 1.05;
    const AVG_ADJUSTMENT: f32 = 1.00;
    const MIN_ADJUSTMENT: f32 = 0.90;

    let continuity_adjustment = if continuity > AVG_CONTINUITY {
        let continuity_factor = 1.0 - (continuity - AVG_CONTINUITY) / (MAX_CONTINUITY - AVG_CONTINUITY);
        (continuity_factor * (AVG_ADJUSTMENT - MIN_ADJUSTMENT) + MIN_ADJUSTMENT)
            .min(AVG_ADJUSTMENT)
            .max(MIN_ADJUSTMENT)
    } else {
        let continuity_factor = 1.0 - (continuity - MIN_CONTINUITY) / (AVG_CONTINUITY - MIN_CONTINUITY);
        (continuity_factor * (MAX_ADJUSTMENT - AVG_ADJUSTMENT) + AVG_ADJUSTMENT)
            .min(MAX_ADJUSTMENT)
            .max(AVG_ADJUSTMENT)
    };

    (continuity_adjustment, continuity)
}

/// Calculate short map adjustment for difficulty
pub fn calculate_short_map_adjustment(bins: &[f32], continuity: f32) -> f32 {
    const MAX_SHORT_MAP_ADJUSTMENT: f32 = 0.75;
    const SHORT_MAP_THRESHOLD: f32 = 60.0 * 1000.0; // 60 seconds in milliseconds
    const BIN_SIZE: f32 = 1000.0;

    // Use the continuity value passed from the continuity adjustment calculation
    let true_drain_time = bins.len() as f32 * continuity * BIN_SIZE;
    let short_map_adjustment = (0.25 * (true_drain_time / SHORT_MAP_THRESHOLD).sqrt() + 0.75)
        .min(1.0)
        .max(MAX_SHORT_MAP_ADJUSTMENT);

    short_map_adjustment
}