rgchart 0.0.13

A library for parsing and writing rhythm game charts.
Documentation
use crate::models::common::*;
use crate::models::generic::GenericManiaChart;
use crate::models::timeline::HitObjectTimeline;
use crate::utils::math::approx_eq;
use crate::utils::rhythm::{MeasureRange, snap_to_nearest_note_type_normed};
use crate::utils::string::add_key_value_template;
use crate::utils::time::to_seconds;

#[allow(unused)]
use crate::errors;

#[inline]
fn sm_row_to_str(row: &[Key]) -> String {
    let mut result = String::with_capacity(row.len());
    for key in row {
        result.push(match key.key_type {
            KeyType::Empty => '0',
            KeyType::Normal => '1',
            KeyType::SliderStart => '2',
            KeyType::SliderEnd => '3',
            KeyType::Mine => 'M',
            KeyType::Fake => 'F',
            KeyType::Unknown => '0',
        });
    }
    result
}

#[inline]
fn pad_measure(rows: &[HitObjectRow], range: &MeasureRange) -> Measure {
    let key_count = rows.first().map_or(0, |row| row.keys.len());

    if range.is_empty() {
        return vec![HitObjectRow::empty(0, 0.0, key_count); 4];
    }

    let measure: Vec<&HitObjectRow> = rows
        .iter()
        .skip(range.0)
        .take(range.1 - range.0)
        .collect();

    if measure.is_empty() {
        return vec![HitObjectRow::empty(0, 0.0, key_count); 4];
    }

    let measure_start_beat = measure[0].beat;
    let measure_start_time = measure[0].time;
    let normalized_measure: Vec<_> = measure
        .iter()
        .map(|row| (row.time, row.beat - measure_start_beat, &row.keys))
        .collect();

    let mut minimal_beat_diff = 4.0;
    for i in 1..normalized_measure.len() {
        let diff = normalized_measure[i].1 - normalized_measure[i - 1].1;
        if diff < minimal_beat_diff && diff > 1e-5 {
            minimal_beat_diff = diff;
        }
    }

    let snapped_beat_diff = snap_to_nearest_note_type_normed(minimal_beat_diff);
    let row_count = (4.0 / snapped_beat_diff).round() as usize;

    let expected_beats: Vec<f32> = (0..row_count)
        .map(|k| k as f32 * snapped_beat_diff)
        .collect();

    let mut padded_measure = Vec::with_capacity(row_count);
    let mut measure_iter = normalized_measure.iter().peekable();

    for &expected_beat in &expected_beats {
        if let Some(row) = measure_iter.peek() {
            if approx_eq(row.1, expected_beat, 0.15) {
                padded_measure.push(HitObjectRow {
                    time: row.0,
                    beat: measure_start_beat + expected_beat,
                    keys: row.2.clone(),
                });
                measure_iter.next();
                continue;
            }
        }
        let empty_time = measure_start_time + ((expected_beat / 4.0) * 1000.0) as i32;
        padded_measure.push(HitObjectRow::empty(
            empty_time,
            measure_start_beat + expected_beat,
            key_count,
        ));
    }

    padded_measure
}

pub(crate) fn to_sm_generic(chart: &GenericManiaChart) -> Result<String, Box<dyn std::error::Error>> {
    let mut template = String::new();
    let mut bpm_template = String::new();
    let mut notes_template = String::new();

    let mut prev_beat = 0.0;
    let mut prev_measure = 0;
    let mut prev_measure_beat = 0.0;
    let mut current_measure_index = 0;

    let scale_factor = 24.0;
    let beats_per_measure = 4.0;
    let beats_per_measure_scaled = scale_factor * beats_per_measure;

    let key_count = chart
        .hitobjects
        .iter()
        .map(|obj| obj.lane as usize + 1)
        .max()
        .unwrap_or(4);
    let rows = HitObjectTimeline::to_rows(&chart.hitobjects.objects, key_count);

    let mut measure_indices: Vec<MeasureRange> = Vec::with_capacity(85);
    let mut padded_measures: Vec<Measure> = Vec::with_capacity(rows.len() * 2);
    let bpms: Vec<_> = chart.timing_points.bpm_changes().collect();

    // get measures
    for (row_index, row) in rows.iter().enumerate() {
        let beat = row.beat;
        let beat_scaled = (beat * scale_factor).round();
        let measure = (beat_scaled / beats_per_measure_scaled) as u32;

        if measure != prev_measure {
            measure_indices.push(MeasureRange(current_measure_index, row_index, false));

            if beat - prev_measure_beat > 5.0 {
                let empty_measure_count = ((beat - prev_beat) / 4.0).floor() as usize;

                for _ in 0..empty_measure_count {
                    measure_indices.push(MeasureRange(current_measure_index, row_index, true));
                    current_measure_index = row_index;
                }
            }

            current_measure_index = row_index;
            prev_measure = measure;
            prev_measure_beat = beat;
        }
        prev_beat = beat;
    }
    measure_indices.push(MeasureRange(current_measure_index, rows.len(), false));

    for measure_range in measure_indices {
        padded_measures.push(pad_measure(&rows, &measure_range));
    }

    // process bpms
    let last_bpm_beat = bpms.last().unwrap().beat;
    for bpm in bpms {
        if bpm.beat < last_bpm_beat {
            add_key_value_template(
                &mut bpm_template,
                &bpm.beat.to_string(),
                "=",
                &bpm.change.value.to_string(),
                ",\n",
            );
        } else {
            add_key_value_template(
                &mut bpm_template,
                &bpm.beat.to_string(),
                "=",
                &bpm.change.value.to_string(),
                "\n",
            );
        }
    }

    // process notes
    let last_measure_index = padded_measures.len() - 1;
    notes_template.push('\n');
    notes_template.push_str("   ");
    notes_template.push_str("dance-single:\n"); // TODO: support for multikey later
    notes_template.push_str("   ");
    notes_template.push_str(&chart.metadata.creator);
    notes_template.push_str(":\n");
    notes_template.push_str("   ");
    notes_template.push_str("Edit"); // TODO: maybe process difficulty value and choose an sm difficulty??
    notes_template.push_str(":\n");
    notes_template.push_str("   ");
    notes_template.push_str("1:\n");
    notes_template.push_str("   ");
    notes_template.push_str("0.000,0.000,0.000,0.000,0.000:\n");
    for (measure_index, measure) in padded_measures.iter().enumerate() {
        notes_template.push_str("// Measure ");
        notes_template.push_str(&(measure_index + 1).to_string());
        notes_template.push('\n');
        for row in measure {
            notes_template.push_str(&sm_row_to_str(&row.keys));
            notes_template.push('\n');
        }
        if measure_index != last_measure_index {
            notes_template.push_str(", ");
        }
    }

    // process template
    add_key_value_template(&mut template, "#TITLE", ":", &chart.metadata.title, ";\n");
    let subtitle = if chart.metadata.source == ChartDefaults::SOURCE {
        ""
    } else {
        &chart.metadata.source
    };
    add_key_value_template(&mut template, "#SUBTITLE", ":", subtitle, ";\n");
    add_key_value_template(&mut template, "#ARTIST", ":", &chart.metadata.artist, ";\n");
    add_key_value_template(
        &mut template,
        "#TITLETRANSLIT",
        ":",
        &chart.metadata.alt_title,
        ";\n",
    );
    add_key_value_template(&mut template, "#SUBTITLETRANSLIT", ":", "", ";\n");
    add_key_value_template(
        &mut template,
        "#ARTISTTRANSLIT",
        ":",
        &chart.metadata.alt_artist,
        ";\n",
    );
    add_key_value_template(&mut template, "#GENRE", ":", &chart.metadata.genre, ";\n");
    add_key_value_template(&mut template, "#CREDIT", ":", &chart.metadata.creator, ";\n");
    add_key_value_template(&mut template, "#BANNER", ":", &chart.chartinfo.bg_path, ";\n");
    add_key_value_template(
        &mut template,
        "#BACKGROUND",
        ":",
        &chart.chartinfo.bg_path,
        ";\n",
    );
    add_key_value_template(&mut template, "#LYRICSPATH", ":", "", ";\n");
    add_key_value_template(&mut template, "#CDTITLE", ":", "", ";\n");
    add_key_value_template(&mut template, "#MUSIC", ":", &chart.chartinfo.song_path, ";\n");
    add_key_value_template(
        &mut template,
        "#OFFSET",
        ":",
        &to_seconds(-chart.chartinfo.audio_offset as f32).to_string(),
        ";\n",
    );
    add_key_value_template(
        &mut template,
        "#SAMPLESTART",
        ":",
        &to_seconds(chart.chartinfo.preview_time as f32).to_string(),
        ";\n",
    );
    add_key_value_template(&mut template, "#SAMPLELENGTH", ":", "12.000", ";\n"); // TODO: maybe add chart length in chart info
    add_key_value_template(&mut template, "#SELECTABLE", ":", "YES", ";\n");
    add_key_value_template(&mut template, "#BPMS", ":", &bpm_template, ";\n");
    add_key_value_template(&mut template, "#STOPS", ":", "", ";\n");
    add_key_value_template(&mut template, "#BGCHANGES", ":", "", ";\n");
    add_key_value_template(&mut template, "#KEYSOUNDS", ":", "", ";\n");
    add_key_value_template(&mut template, "#NOTES", ":", &notes_template, ";\n");

    Ok(template)
}

#[allow(unused)]
pub(crate) fn to_sma(chart: &GenericManiaChart) -> Result<String, Box<dyn std::error::Error>> {
    unimplemented!();
}

#[allow(unused)]
pub(crate) fn to_ssc(chart: &GenericManiaChart) -> Result<String, Box<dyn std::error::Error>> {
    unimplemented!();
}