rgchart 0.0.13

A library for parsing and writing rhythm game charts.
Documentation
use std::str::FromStr;
use std::collections::HashMap;

use crate::models::common::GameMode;
use crate::models::osu::sound::SampleSet;
use crate::models::osu::chart::OsuMode;

#[derive(Debug, Clone, PartialEq)]
pub enum OverlayPosition {
    NoChange,
    Below,
    Above,
}

impl FromStr for OverlayPosition {
    type Err = String;
    
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "NoChange" => Ok(OverlayPosition::NoChange),
            "Below" => Ok(OverlayPosition::Below),
            "Above" => Ok(OverlayPosition::Above),
            _ => Err(format!("Invalid OverlayPosition: {}", s)),
        }
    }
}

impl ToString for OverlayPosition {
    fn to_string(&self) -> String {
        match self {
            OverlayPosition::NoChange => "NoChange".to_string(),
            OverlayPosition::Below => "Below".to_string(),
            OverlayPosition::Above => "Above".to_string(),
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct General {
    pub audio_filename: String,
    pub audio_lead_in: i32,
    pub preview_time: i32,
    pub countdown: i32,
    pub sample_set: SampleSet,
    pub stack_leniency: f32,
    pub mode: u8,
    pub letterbox_in_breaks: bool,
    pub special_style: bool,
    pub widescreen_storyboard: bool,
    
    pub audio_hash: Option<String>,
    pub story_fire_in_front: Option<bool>,
    pub use_skin_sprites: Option<bool>,
    pub always_show_playfield: Option<bool>,
    pub overlay_position: Option<OverlayPosition>,
    pub skin_preference: Option<String>,
    pub epilepsy_warning: Option<bool>,
    pub countdown_offset: Option<i32>,
    pub samples_match_playback_rate: Option<bool>,
}

impl Default for General {
    fn default() -> Self {
        Self {
            audio_filename: String::new(),
            audio_lead_in: 0,
            preview_time: -1,
            countdown: 1,
            sample_set: SampleSet::Normal,
            stack_leniency: 0.7,
            mode: 0,
            letterbox_in_breaks: false,
            special_style: false,
            widescreen_storyboard: false,
            
            audio_hash: None,
            story_fire_in_front: None,
            use_skin_sprites: None,
            always_show_playfield: None,
            overlay_position: None,
            skin_preference: None,
            epilepsy_warning: None,
            countdown_offset: None,
            samples_match_playback_rate: None,
        }
    }
}

impl FromStr for General {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut general = General::default();
        
        let mut key_values = HashMap::new();
        for line in s.lines() {
            let line = line.trim();
            if line.is_empty() || line.starts_with("//") {
                continue;
            }
            
            if let Some((key, value)) = line.split_once(':') {
                let key = key.trim();
                let value = value.trim();
                key_values.insert(key.to_string(), value.to_string());
            }
        }
        
        if let Some(value) = key_values.get("AudioFilename") {
            general.audio_filename = value.clone();
        }
        
        if let Some(value) = key_values.get("AudioLeadIn") {
            general.audio_lead_in = value.parse::<i32>()
                .map_err(|_| format!("Invalid AudioLeadIn value: {}", value))?;
        }
        
        if let Some(value) = key_values.get("PreviewTime") {
            general.preview_time = value.parse::<i32>()
                .map_err(|_| format!("Invalid PreviewTime value: {}", value))?;
        }
        
        if let Some(value) = key_values.get("Countdown") {
            general.countdown = value.parse::<i32>()
                .map_err(|_| format!("Invalid Countdown value: {}", value))?;
        }
        
        if let Some(value) = key_values.get("SampleSet") {
            general.sample_set = SampleSet::from_str(value)?;
        }
        
        if let Some(value) = key_values.get("StackLeniency") {
            general.stack_leniency = value.parse::<f32>()
                .map_err(|_| format!("Invalid StackLeniency value: {}", value))?;
        }
        
        if let Some(value) = key_values.get("Mode") {
            general.mode = value.parse::<u8>()
                .map_err(|_| format!("Invalid Mode value: {}", value))?;
        }
        
        if let Some(value) = key_values.get("LetterboxInBreaks") {
            general.letterbox_in_breaks = value.parse::<i32>()
                .map_err(|_| format!("Invalid LetterboxInBreaks value: {}", value))? != 0;
        }
        
        if let Some(value) = key_values.get("SpecialStyle") {
            general.special_style = value.parse::<i32>()
                .map_err(|_| format!("Invalid SpecialStyle value: {}", value))? != 0;
        }
        
        if let Some(value) = key_values.get("WidescreenStoryboard") {
            general.widescreen_storyboard = value.parse::<i32>()
                .map_err(|_| format!("Invalid WidescreenStoryboard value: {}", value))? != 0;
        }
        
        if let Some(value) = key_values.get("AudioHash") {
            general.audio_hash = Some(value.clone());
        }
        
        if let Some(value) = key_values.get("StoryFireInFront") {
            general.story_fire_in_front = Some(value.parse::<i32>()
                .map_err(|_| format!("Invalid StoryFireInFront value: {}", value))? != 0);
        }
        
        if let Some(value) = key_values.get("UseSkinSprites") {
            general.use_skin_sprites = Some(value.parse::<i32>()
                .map_err(|_| format!("Invalid UseSkinSprites value: {}", value))? != 0);
        }
        
        if let Some(value) = key_values.get("AlwaysShowPlayfield") {
            general.always_show_playfield = Some(value.parse::<i32>()
                .map_err(|_| format!("Invalid AlwaysShowPlayfield value: {}", value))? != 0);
        }
        
        if let Some(value) = key_values.get("OverlayPosition") {
            general.overlay_position = Some(OverlayPosition::from_str(value)?);
        }
        
        if let Some(value) = key_values.get("SkinPreference") {
            general.skin_preference = Some(value.clone());
        }
        
        if let Some(value) = key_values.get("EpilepsyWarning") {
            general.epilepsy_warning = Some(value.parse::<i32>()
                .map_err(|_| format!("Invalid EpilepsyWarning value: {}", value))? != 0);
        }
        
        if let Some(value) = key_values.get("CountdownOffset") {
            general.countdown_offset = Some(value.parse::<i32>()
                .map_err(|_| format!("Invalid CountdownOffset value: {}", value))?);
        }
        
        if let Some(value) = key_values.get("SamplesMatchPlaybackRate") {
            general.samples_match_playback_rate = Some(value.parse::<i32>()
                .map_err(|_| format!("Invalid SamplesMatchPlaybackRate value: {}", value))? != 0);
        }
        
        Ok(general)
    }
}

impl General {
    pub fn get_mode_str(&self) -> &'static str {
        match self.mode {
            0 => "osu!",
            1 => "osu!taiko",
            2 => "osu!catch",
            3 => "osu!mania",
            _ => "unknown",
        }
    }

    pub fn get_mode_osu(&self) -> OsuMode {
        match self.mode {
            0 => OsuMode::Standard,
            1 => OsuMode::Taiko,
            2 => OsuMode::Catch,
            3 => OsuMode::Mania,
            _ => OsuMode::Unknown,
        }
    }

    pub fn get_mode(&self) -> GameMode {
        match self.mode {
            0 => GameMode::OsuStandard,
            1 => GameMode::Taiko,
            2 => GameMode::Catch,
            3 => GameMode::Mania,
            _ => GameMode::OsuStandard,
        }
    }

    pub fn is_osu_standard(&self) -> bool {
        self.mode == 0
    }

    pub fn is_taiko(&self) -> bool {
        self.mode == 1
    }

    pub fn is_catch(&self) -> bool {
        self.mode == 2
    }

    pub fn is_mania(&self) -> bool {
        self.mode == 3
    }

    pub fn to_str(&self) -> String {
        let mut lines = Vec::new();
        
        lines.push(format!("AudioFilename: {}", self.audio_filename));
        lines.push(format!("AudioLeadIn: {}", self.audio_lead_in));
        lines.push(format!("PreviewTime: {}", self.preview_time));
        lines.push(format!("Countdown: {}", self.countdown));
        lines.push(format!("SampleSet: {}", self.sample_set.to_string()));
        lines.push(format!("StackLeniency: {}", self.stack_leniency));
        lines.push(format!("Mode: {}", self.mode));
        lines.push(format!("LetterboxInBreaks: {}", if self.letterbox_in_breaks { 1 } else { 0 }));
        lines.push(format!("SpecialStyle: {}", if self.special_style { 1 } else { 0 }));
        lines.push(format!("WidescreenStoryboard: {}", if self.widescreen_storyboard { 1 } else { 0 }));
        
        if let Some(ref hash) = self.audio_hash {
            lines.push(format!("AudioHash: {}", hash));
        }
        
        if let Some(story_fire) = self.story_fire_in_front {
            lines.push(format!("StoryFireInFront: {}", if story_fire { 1 } else { 0 }));
        }
        
        if let Some(skin_sprites) = self.use_skin_sprites {
            lines.push(format!("UseSkinSprites: {}", if skin_sprites { 1 } else { 0 }));
        }
        
        if let Some(always_show) = self.always_show_playfield {
            lines.push(format!("AlwaysShowPlayfield: {}", if always_show { 1 } else { 0 }));
        }
        
        if let Some(ref overlay) = self.overlay_position {
            lines.push(format!("OverlayPosition: {}", overlay.to_string()));
        }
        
        if let Some(ref skin) = self.skin_preference {
            lines.push(format!("SkinPreference: {}", skin));
        }
        
        if let Some(epilepsy) = self.epilepsy_warning {
            lines.push(format!("EpilepsyWarning: {}", if epilepsy { 1 } else { 0 }));
        }
        
        if let Some(offset) = self.countdown_offset {
            lines.push(format!("CountdownOffset: {}", offset));
        }
        
        if let Some(samples_match) = self.samples_match_playback_rate {
            lines.push(format!("SamplesMatchPlaybackRate: {}", if samples_match { 1 } else { 0 }));
        }
        
        lines.join("\n")
    }
}