quaver-rs 0.1.0

A Rust library for parsing and analyzing Quaver rhythm game maps
Documentation
use std::fs;
use serde::{Deserialize, Serialize};
use crate::hit_object_info::{HitObjectInfo, KeySoundInfo};
use crate::enums::GameMode;

/// YAML structure for parsing .qua files
#[derive(Debug, Deserialize, Serialize)]
struct QuaYaml {
    #[serde(rename = "AudioFile", default)]
    audio_file: String,
    
    #[serde(rename = "SongPreviewTime", default)]
    song_preview_time: i32,
    
    #[serde(rename = "BackgroundFile", default)]
    background_file: String,
    
    #[serde(rename = "MapId", default)]
    map_id: i32,
    
    #[serde(rename = "MapSetId", default)]
    map_set_id: i32,
    
    #[serde(rename = "Mode", default)]
    mode: String,
    
    #[serde(rename = "Title", default)]
    title: String,
    
    #[serde(rename = "Artist", default)]
    artist: String,
    
    #[serde(rename = "Source", default)]
    source: String,
    
    #[serde(rename = "Tags", default)]
    tags: String,
    
    #[serde(rename = "Creator", default)]
    creator: String,
    
    #[serde(rename = "DifficultyName", default)]
    difficulty_name: String,
    
    #[serde(rename = "Description", default)]
    description: String,
    
    #[serde(rename = "LegacyLNRendering", default)]
    legacy_ln_rendering: bool,
    
    #[serde(rename = "BPMDoesNotAffectScrollVelocity", default)]
    bpm_does_not_affect_scroll_velocity: bool,
    
    #[serde(rename = "EditorLayers", default)]
    editor_layers: Vec<EditorLayerYaml>,
    
    #[serde(rename = "Bookmarks", default)]
    bookmarks: Vec<BookmarkYaml>,
    
    #[serde(rename = "CustomAudioSamples", default)]
    custom_audio_samples: Vec<CustomAudioSampleYaml>,
    
    #[serde(rename = "SoundEffects", default)]
    sound_effects: Vec<SoundEffectYaml>,
    
    #[serde(rename = "TimingPoints", default)]
    timing_points: Vec<TimingPointYaml>,
    
    #[serde(rename = "HitObjects", default)]
    hit_objects: Vec<HitObjectYaml>,
}

#[derive(Debug, Deserialize, Serialize)]
struct EditorLayerYaml {
    #[serde(rename = "Name", default)]
    name: String,
    
    #[serde(rename = "Color", default)]
    color: (u8, u8, u8, u8),
    
    #[serde(rename = "Hidden", default)]
    hidden: bool,
}

#[derive(Debug, Deserialize, Serialize)]
struct BookmarkYaml {
    #[serde(rename = "StartTime", default)]
    start_time: f32,
    
    #[serde(rename = "Note", default)]
    note: String,
}

#[derive(Debug, Deserialize, Serialize)]
struct CustomAudioSampleYaml {
    #[serde(rename = "Name", default)]
    name: String,
    
    #[serde(rename = "Path", default)]
    path: String,
    
    #[serde(rename = "Volume", default)]
    volume: f32,
}

#[derive(Debug, Deserialize, Serialize)]
struct SoundEffectYaml {
    #[serde(rename = "StartTime", default)]
    start_time: f32,
    
    #[serde(rename = "EffectType", default)]
    effect_type: String,
    
    #[serde(rename = "Parameters", default)]
    parameters: Vec<f32>,
}

#[derive(Debug, Deserialize, Serialize)]
struct TimingPointYaml {
    #[serde(rename = "StartTime", default)]
    start_time: f32, // Changed to f32 to handle floating point values
    
    #[serde(rename = "Bpm", default)]
    bpm: f32,
    
    #[serde(rename = "Signature", default)]
    signature: i32,
    
    #[serde(rename = "Hidden", default)]
    hidden: bool,
}

#[derive(Debug, Deserialize, Serialize)]
struct HitObjectYaml {
    #[serde(rename = "StartTime", default)]
    start_time: f32,
    
    #[serde(rename = "Lane", default)]
    lane: i32,
    
    #[serde(rename = "EndTime", default)]
    end_time: f32,
    
    #[serde(rename = "HitSound", default)]
    hit_sound: i32,
    
    #[serde(rename = "KeySounds", default)]
    key_sounds: Vec<KeySoundYaml>,
    
    #[serde(rename = "EditorLayer", default)]
    editor_layer: i32,
    
    #[serde(rename = "TimingGroup", default)]
    timing_group: String,
}

#[derive(Debug, Deserialize, Serialize)]
struct KeySoundYaml {
    #[serde(rename = "SampleSet", default)]
    sample_set: String,
    
    #[serde(rename = "AdditionSet", default)]
    addition_set: String,
    
    #[serde(rename = "CustomIndex", default)]
    custom_index: i32,
    
    #[serde(rename = "SampleVolume", default)]
    sample_volume: i32,
    
    #[serde(rename = "Filename", default)]
    filename: String,
}

/// Parse a .qua file from a file path
pub fn parse_from_file(path: &str) -> Result<crate::qua::Qua, Box<dyn std::error::Error>> {
    let content = fs::read_to_string(path)?;
    parse_from_str(&content)
}

/// Parse a .qua file from a string
pub fn parse_from_str(content: &str) -> Result<crate::qua::Qua, Box<dyn std::error::Error>> {
        let qua_yaml: QuaYaml = serde_yaml::from_str(content)?;
        
        // Convert YAML to Qua struct
        let metadata = crate::qua::metadata::MapMetadata {
            audio_file: qua_yaml.audio_file,
            song_preview_time: qua_yaml.song_preview_time,
            background_file: qua_yaml.background_file,
            banner_file: String::new(), // Not in this YAML structure
            map_id: qua_yaml.map_id,
            map_set_id: qua_yaml.map_set_id,
            mode: parse_game_mode(&qua_yaml.mode),
            title: qua_yaml.title,
            artist: qua_yaml.artist,
            source: qua_yaml.source,
            tags: qua_yaml.tags,
            creator: qua_yaml.creator,
            difficulty_name: qua_yaml.difficulty_name,
            description: qua_yaml.description,
            genre: String::new(), // Not in this YAML structure
            legacy_ln_rendering: qua_yaml.legacy_ln_rendering,
            bpm_does_not_affect_scroll_velocity: qua_yaml.bpm_does_not_affect_scroll_velocity,
            has_scratch_key: false, // Will be determined from mode
            randomize_modifier_seed: -1,
        };
        
        let timing_points = qua_yaml.timing_points
            .into_iter()
            .map(|tp| crate::qua::TimingPointInfo {
                start_time: tp.start_time, // Keep as f32
                bpm: tp.bpm,
                signature: tp.signature,
            })
            .collect();
        
        let hit_objects = qua_yaml.hit_objects
            .into_iter()
            .map(|ho| HitObjectInfo {
                start_time: ho.start_time,
                lane: ho.lane,
                end_time: ho.end_time,
                hit_sound: parse_hit_sounds(ho.hit_sound),
                key_sounds: ho.key_sounds
                    .into_iter()
                    .map(|ks| KeySoundInfo {
                        sample_set: ks.sample_set,
                        addition_set: ks.addition_set,
                        custom_index: ks.custom_index,
                        sample_volume: ks.sample_volume,
                        filename: ks.filename,
                    })
                    .collect(),
                editor_layer: ho.editor_layer,
                timing_group: ho.timing_group,
                is_editable_in_lua_script: true,
            })
            .collect();
        
        let editor_layers = qua_yaml.editor_layers
            .into_iter()
            .map(|el| crate::qua::EditorLayerInfo {
                name: el.name,
                color: el.color,
                hidden: el.hidden,
            })
            .collect();
        
        let bookmarks = qua_yaml.bookmarks
            .into_iter()
            .map(|bm| crate::qua::BookmarkInfo {
                start_time: bm.start_time,
                note: bm.note,
            })
            .collect();
        
        let custom_audio_samples = qua_yaml.custom_audio_samples
            .into_iter()
            .map(|cas| crate::qua::CustomAudioSampleInfo {
                name: cas.name,
                path: cas.path,
                volume: cas.volume,
            })
            .collect();
        
        let sound_effects = qua_yaml.sound_effects
            .into_iter()
            .map(|se| crate::qua::SoundEffectInfo {
                start_time: se.start_time,
                effect_type: se.effect_type,
                parameters: se.parameters,
            })
            .collect();
        
    Ok(crate::qua::Qua {
        metadata,
        timing_points,
        hit_objects,
        editor_layers,
        bookmarks,
        custom_audio_samples,
        sound_effects,
        timing_groups: std::collections::HashMap::new(),
    })
}

fn parse_game_mode(mode_str: &str) -> GameMode {
    match mode_str {
        "Keys1" => GameMode::Keys1,
        "Keys2" => GameMode::Keys2,
        "Keys3" => GameMode::Keys3,
        "Keys4" => GameMode::Keys4,
        "Keys5" => GameMode::Keys5,
        "Keys6" => GameMode::Keys6,
        "Keys7" => GameMode::Keys7,
        "Keys8" => GameMode::Keys8,
        "Keys9" => GameMode::Keys9,
        "Keys10" => GameMode::Keys10,
        _ => GameMode::Keys4, // Default fallback
    }
}

fn parse_hit_sounds(hit_sound: i32) -> crate::hit_object_info::HitSounds {
    match hit_sound {
        0 => crate::hit_object_info::HitSounds::None,
        1 => crate::hit_object_info::HitSounds::Normal,
        2 => crate::hit_object_info::HitSounds::Whistle,
        4 => crate::hit_object_info::HitSounds::Finish,
        8 => crate::hit_object_info::HitSounds::Clap,
        _ => crate::hit_object_info::HitSounds::None,
    }
}