rgchart 0.0.12

A library for parsing and writing rhythm game charts.
Documentation
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::models::fluxis::*;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DualMode {
    Disabled = 0,
    Enabled = 1,
    Separate = 2,
}

impl Serialize for DualMode {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match self {
            DualMode::Disabled => serializer.serialize_i32(0),
            DualMode::Enabled => serializer.serialize_i32(1),
            DualMode::Separate => serializer.serialize_i32(2),
        }
    }
}

impl<'de> Deserialize<'de> for DualMode {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let value = i32::deserialize(deserializer)?;
        match value {
            0 => Ok(DualMode::Disabled),
            1 => Ok(DualMode::Enabled),
            2 => Ok(DualMode::Separate),
            _ => Err(serde::de::Error::custom(format!("Invalid DualMode value: {}", value))),
        }
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct FscFile {
    #[serde(rename = "AudioFile")]
    pub audio_file: String,
    
    #[serde(rename = "BackgroundFile")]
    pub background_file: String,
    
    #[serde(rename = "CoverFile")]
    pub cover_file: String,
    
    #[serde(rename = "VideoFile")]
    pub video_file: String,
    
    #[serde(rename = "EffectFile")]
    pub effect_file: String,
    
    #[serde(rename = "StoryboardFile")]
    pub storyboard_file: String,
    
    #[serde(alias = "Metadata", alias = "MetaData", alias = "METADATA")]
    pub metadata: metadata::Metadata,

    pub colors: metadata::Colors,
    
    #[serde(rename = "HitObjects")]
    pub hit_objects: Vec<hitobjects::HitObject>,
    
    #[serde(rename = "TimingPoints")]
    pub timing_points: Vec<timing_points::TimingPoint>,
    
    #[serde(rename = "ScrollVelocities")]
    pub scroll_velocities: Vec<timing_points::ScrollVelocity>,
    
    #[serde(rename = "HitSoundFades")]
    pub hit_sound_fades: Vec<sound::HitSoundFade>,
    
    #[serde(rename = "AccuracyDifficulty", skip_serializing_if = "Option::is_none")]
    pub accuracy_difficulty: Option<f32>,
    
    #[serde(rename = "HealthDifficulty", skip_serializing_if = "Option::is_none")]
    pub health_difficulty: Option<f32>,
    
    #[serde(rename = "dual", skip_serializing_if = "Option::is_none")]
    pub dual_mode: Option<DualMode>,
    
    #[serde(rename = "ls-v2", skip_serializing_if = "Option::is_none")]
    pub new_lane_switch_layout: Option<bool>,
    
    #[serde(rename = "editor-time", skip_serializing_if = "Option::is_none")]
    pub time_in_editor: Option<i32>,
    
    #[serde(rename = "extra-playfields", skip_serializing_if = "Option::is_none")]
    pub extra_playfields: Option<i32>,
}

impl FscFile {
    pub fn from_str(json: &str) -> Result<Self, serde_json::Error> {
        serde_json::from_str(json)
    }
}

impl FscFile {
    pub const MIN_KEYMODE: isize = 1;
    pub const MAX_KEYMODE: isize = -1;
    
    pub fn new() -> Self {
        Self {
            audio_file: String::new(),
            background_file: String::new(),
            cover_file: String::new(),
            video_file: String::new(),
            effect_file: String::new(),
            storyboard_file: String::new(),
            metadata: metadata::Metadata::default(),
            colors: metadata::Colors::default(),
            hit_objects: Vec::new(),
            timing_points: vec![timing_points::TimingPoint::new(0.0, 120.0, 4)],
            scroll_velocities: Vec::new(),
            hit_sound_fades: Vec::new(),
            accuracy_difficulty: Some(8.0),
            health_difficulty: Some(8.0),
            dual_mode: Some(DualMode::Disabled),
            new_lane_switch_layout: Some(false),
            time_in_editor: Some(0),
            extra_playfields: Some(0),
        }
    }
    
    pub fn to_str(&self) -> Result<String, serde_json::Error> {
        serde_json::to_string(self)
    }
    
    pub fn is_dual(&self) -> bool {
        self.dual_mode.unwrap_or(DualMode::Disabled) as i32 > DualMode::Disabled as i32
    }
    
    pub fn is_split(&self) -> bool {
        self.dual_mode.unwrap_or(DualMode::Disabled) == DualMode::Separate
    }
    
    pub fn start_time(&self) -> f32 {
        self.hit_objects.first().map(|obj| obj.time).unwrap_or(0.0)
    }
    
    pub fn end_time(&self) -> f32 {
        self.hit_objects
            .iter()
            .map(|obj| obj.end_time())
            .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
            .unwrap_or(0.0)
    }
    
    pub fn max_combo(&self) -> i32 {
        self.hit_objects.iter().fold(0, |acc, obj| {
            acc + 1 + if obj.is_ln() { 1 } else { 0 }
        })
    }
    
    pub fn key_count(&self) -> isize {
        self.hit_objects
            .iter()
            .map(|obj| obj.lane)
            .max()
            .unwrap_or(4)
    }
    
    pub fn validate(&self) -> Result<(), String> {
        if self.hit_objects.is_empty() {
            return Err("Map has no hit objects.".to_string());
        }
        
        if self.timing_points.is_empty() {
            return Err("Map has no timing points.".to_string());
        }
        
        for timing_point in &self.timing_points {
            if timing_point.bpm <= 0.0 {
                return Err("A timing point has an invalid BPM.".to_string());
            }
            
            if timing_point.signature <= 0 {
                return Err("A timing point has an invalid signature.".to_string());
            }
        }
        
        if self.hit_objects.iter().any(|obj| obj.lane < 1) {
            return Err("A hit object in this map is in a lane below 1.".to_string());
        }
        
        let mode = self.key_count();
        
        if mode < Self::MIN_KEYMODE || mode > Self::MAX_KEYMODE {
            return Err(format!("Map has an invalid keymode: {}. Must be between {} and {}.", 
                mode, Self::MIN_KEYMODE, Self::MAX_KEYMODE));
        }
        
        Ok(())
    }
    
    pub fn get_normal_notes(&self) -> Vec<&hitobjects::HitObject> {
        self.hit_objects.iter().filter(|obj| obj.is_normal()).collect()
    }

    pub fn get_long_notes(&self) -> Vec<&hitobjects::HitObject> {
        self.hit_objects.iter().filter(|obj| obj.is_ln()).collect()
    }
    
    pub fn get_tick_notes(&self) -> Vec<&hitobjects::HitObject> {
        self.hit_objects.iter().filter(|obj| obj.is_tick()).collect()
    }
}

impl Default for FscFile {
    fn default() -> Self {
        Self::new()
    }
}