rgchart 0.0.13

A library for parsing and writing rhythm game charts.
Documentation
use crate::errors;
use crate::models::common::*;
use crate::models::generic::{
    self,
    GenericManiaChart,
    ChartInfo,
    HitObjects,
    KeySound,
    Metadata,
    SoundBank,
    SoundEffect,
    TimingPoints
};
use crate::models::quaver::{self, QuaFile};
use crate::models::timeline::{TimelineOps, TimelineTimingPoint, TimingPointTimeline};
use crate::utils::quaver::get_keycount_from_str;
use crate::utils::rhythm::calculate_beat_from_time;

fn process_timing_points(
    timing_points: Vec<quaver::TimingPoint>,
    chartinfo: &mut ChartInfo,
    timeline: &mut TimingPointTimeline,
) -> Result<(), Box<dyn std::error::Error>> {
    for timing_point in timing_points {
        timeline.add_sorted(TimelineTimingPoint {
            time: timing_point.start_time as i32,
            value: timing_point.bpm,
            change_type: TimingChangeType::Bpm,
        });
    }

    let start_time = if timeline.is_empty() {
        0
    } else {
        timeline[0].time
    };

    chartinfo.audio_offset = start_time;
    Ok(())
}

fn process_sv(
    slider_velocities: Vec<quaver::SliderVelocity>,
    timeline: &mut TimingPointTimeline,
) -> Result<(), Box<dyn std::error::Error>> {
    for sv in slider_velocities {
        timeline.add_sorted(TimelineTimingPoint {
            time: sv.start_time as i32,
            value: sv.multiplier.unwrap_or(1.0),
            change_type: TimingChangeType::Sv,
        });
    }
    Ok(())
}

fn process_soundeffects(
    sound_effects: Vec<quaver::SoundEffect>,
    soundbank: &mut SoundBank,
) -> Result<(), Box<dyn std::error::Error>> {
    for sound_effect in sound_effects {
        soundbank.add_sound_effect(SoundEffect {
            time: sound_effect.start_time as i32,
            volume: sound_effect.volume,
            sample: sound_effect.sample,
        });
    }
    Ok(())
}

fn process_samples(
    samples: Vec<quaver::AudioSample>,
    soundbank: &mut SoundBank,
) -> Result<(), Box<dyn std::error::Error>> {
    for sample in samples {
        soundbank.add_sound_sample(sample.path);
    }
    Ok(())
}

fn process_notes(
    quaver_hitobjects: Vec<quaver::HitObject>,
    hitobjects: &mut HitObjects,
    chartinfo: &mut ChartInfo,
    offset: i32,
    bpm_times: &Vec<i32>,
    bpms: &Vec<f32>,
    has_scratch: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut key_count = chartinfo.key_count as usize;

    if has_scratch {
        key_count += 1;
    }

    for hitobject in quaver_hitobjects {
        let lane = hitobject.lane();
        let key_sound = hitobject.get_generic_keysound();
        let time = hitobject.start_time() as i32;

        let beat = calculate_beat_from_time(time, offset, (bpm_times, bpms));

        if hitobject.is_ln() {
            let slider = generic::HitObject {
                time,
                beat,
                lane,
                key: Key::slider_start(Some(hitobject.end_time().unwrap_or(0))),
                keysound: key_sound,
                group: hitobject.timing_group().map(|s| s.to_string()),
            };

            let slider_end_time = hitobject.end_time().unwrap_or(0);
            let end_time_beat = calculate_beat_from_time(slider_end_time, offset, (bpm_times, bpms));

            let slider_end = generic::HitObject {
                time: slider_end_time,
                beat: end_time_beat,
                lane,
                key: Key::slider_end(),
                keysound: KeySound::default(),
                group: hitobject.timing_group().map(|s| s.to_string()),
            };

            hitobjects.add_hitobject_sorted(slider);
            hitobjects.add_hitobject_sorted(slider_end);
        } else {
            hitobjects.add_hitobject_sorted(generic::HitObject {
                time,
                beat,
                lane,
                key: Key::normal(),
                keysound: key_sound,
                group: hitobject.timing_group().map(|s| s.to_string()),
            });
        }
    }

    chartinfo.key_count = key_count as u8;

    Ok(())
}

pub(crate) fn from_qua_generic(
    raw_chart: &str,
) -> Result<GenericManiaChart, Box<dyn std::error::Error>> {
    let quaver_file = QuaFile::from_str(&raw_chart)?;

    let key_count = get_keycount_from_str(quaver_file.mode.as_str())
    .ok_or_else(|| Box::new(errors::ParseError::<GameMode>::InvalidChart(
        "Quaver only supports Keys1 up to Keys10 for Mode".to_string(),
    )))?;

    let metadata = Metadata {
        title: quaver_file.title,
        artist: quaver_file.artist,
        source: quaver_file.source,
        tags: quaver_file
            .tags
            .split(" ")
            .map(|s| s.to_string())
            .collect(),
        creator: quaver_file.creator,
        ..Metadata::empty()
    };

    let mut chartinfo = ChartInfo {
        song_path: quaver_file.audio_file,
        preview_time: quaver_file.song_preview_time as i32,
        bg_path: quaver_file.background_file,
        key_count,
        difficulty_name: quaver_file.difficulty_name,
        bpm_affects_sv: !quaver_file.bpm_does_not_affect_scroll_velocity.unwrap_or(true),
        ..ChartInfo::empty()
    };

    let mut timing_points = TimingPoints::with_capacity(64);
    let mut hitobjects = HitObjects::with_capacity(2048);

    let mut soundbank = SoundBank::new();
    let mut timeline = TimingPointTimeline::with_capacity(64);

    process_samples(quaver_file.custom_audio_samples, &mut soundbank)?;
    process_soundeffects(quaver_file.sound_effects, &mut soundbank)?;
    process_timing_points(quaver_file.timing_points, &mut chartinfo, &mut timeline)?;
    process_sv(quaver_file.slider_velocities, &mut timeline)?;

    let offset = chartinfo.audio_offset;
    timeline.to_timing_points(&mut timing_points, chartinfo.audio_offset);
    process_notes(
        quaver_file.hitobjects,
        &mut hitobjects,
        &mut chartinfo,
        offset,
        &timing_points.bpms_times(),
        &timing_points.bpms(),
        quaver_file.has_scratch_key,
    )?;

    Ok(GenericManiaChart::new(
        metadata,
        chartinfo,
        timing_points,
        hitobjects,
        Some(soundbank),
    ))
}