Skip to main content

rgc_chart/models/fluxis/
chart.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use crate::models::fluxis::*;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum DualMode {
6    Disabled = 0,
7    Enabled = 1,
8    Separate = 2,
9}
10
11impl Serialize for DualMode {
12    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
13    where
14        S: Serializer,
15    {
16        match self {
17            DualMode::Disabled => serializer.serialize_i32(0),
18            DualMode::Enabled => serializer.serialize_i32(1),
19            DualMode::Separate => serializer.serialize_i32(2),
20        }
21    }
22}
23
24impl<'de> Deserialize<'de> for DualMode {
25    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
26    where
27        D: Deserializer<'de>,
28    {
29        let value = i32::deserialize(deserializer)?;
30        match value {
31            0 => Ok(DualMode::Disabled),
32            1 => Ok(DualMode::Enabled),
33            2 => Ok(DualMode::Separate),
34            _ => Err(serde::de::Error::custom(format!("Invalid DualMode value: {}", value))),
35        }
36    }
37}
38
39#[derive(Debug, Serialize, Deserialize)]
40pub struct FscFile {
41    #[serde(rename = "AudioFile")]
42    pub audio_file: String,
43    
44    #[serde(rename = "BackgroundFile")]
45    pub background_file: String,
46    
47    #[serde(rename = "CoverFile")]
48    pub cover_file: String,
49    
50    #[serde(rename = "VideoFile")]
51    pub video_file: String,
52    
53    #[serde(rename = "EffectFile")]
54    pub effect_file: String,
55    
56    #[serde(rename = "StoryboardFile")]
57    pub storyboard_file: String,
58    
59    #[serde(alias = "Metadata", alias = "MetaData", alias = "METADATA")]
60    pub metadata: metadata::Metadata,
61
62    pub colors: metadata::Colors,
63    
64    #[serde(rename = "HitObjects")]
65    pub hit_objects: Vec<hitobjects::HitObject>,
66    
67    #[serde(rename = "TimingPoints")]
68    pub timing_points: Vec<timing_points::TimingPoint>,
69    
70    #[serde(rename = "ScrollVelocities")]
71    pub scroll_velocities: Vec<timing_points::ScrollVelocity>,
72    
73    #[serde(rename = "HitSoundFades")]
74    pub hit_sound_fades: Vec<sound::HitSoundFade>,
75    
76    #[serde(rename = "AccuracyDifficulty", skip_serializing_if = "Option::is_none")]
77    pub accuracy_difficulty: Option<f32>,
78    
79    #[serde(rename = "HealthDifficulty", skip_serializing_if = "Option::is_none")]
80    pub health_difficulty: Option<f32>,
81    
82    #[serde(rename = "dual", skip_serializing_if = "Option::is_none")]
83    pub dual_mode: Option<DualMode>,
84    
85    #[serde(rename = "ls-v2", skip_serializing_if = "Option::is_none")]
86    pub new_lane_switch_layout: Option<bool>,
87    
88    #[serde(rename = "editor-time", skip_serializing_if = "Option::is_none")]
89    pub time_in_editor: Option<i32>,
90    
91    #[serde(rename = "extra-playfields", skip_serializing_if = "Option::is_none")]
92    pub extra_playfields: Option<i32>,
93}
94
95impl FscFile {
96    pub fn from_str(json: &str) -> Result<Self, serde_json::Error> {
97        serde_json::from_str(json)
98    }
99}
100
101impl FscFile {
102    pub const MIN_KEYMODE: isize = 1;
103    pub const MAX_KEYMODE: isize = -1;
104    
105    pub fn new() -> Self {
106        Self {
107            audio_file: String::new(),
108            background_file: String::new(),
109            cover_file: String::new(),
110            video_file: String::new(),
111            effect_file: String::new(),
112            storyboard_file: String::new(),
113            metadata: metadata::Metadata::default(),
114            colors: metadata::Colors::default(),
115            hit_objects: Vec::new(),
116            timing_points: vec![timing_points::TimingPoint::new(0.0, 120.0, 4)],
117            scroll_velocities: Vec::new(),
118            hit_sound_fades: Vec::new(),
119            accuracy_difficulty: Some(8.0),
120            health_difficulty: Some(8.0),
121            dual_mode: Some(DualMode::Disabled),
122            new_lane_switch_layout: Some(false),
123            time_in_editor: Some(0),
124            extra_playfields: Some(0),
125        }
126    }
127    
128    pub fn to_str(&self) -> Result<String, serde_json::Error> {
129        serde_json::to_string(self)
130    }
131    
132    pub fn is_dual(&self) -> bool {
133        self.dual_mode.unwrap_or(DualMode::Disabled) as i32 > DualMode::Disabled as i32
134    }
135    
136    pub fn is_split(&self) -> bool {
137        self.dual_mode.unwrap_or(DualMode::Disabled) == DualMode::Separate
138    }
139    
140    pub fn start_time(&self) -> f32 {
141        self.hit_objects.first().map(|obj| obj.time).unwrap_or(0.0)
142    }
143    
144    pub fn end_time(&self) -> f32 {
145        self.hit_objects
146            .iter()
147            .map(|obj| obj.end_time())
148            .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
149            .unwrap_or(0.0)
150    }
151    
152    pub fn max_combo(&self) -> i32 {
153        self.hit_objects.iter().fold(0, |acc, obj| {
154            acc + 1 + if obj.is_ln() { 1 } else { 0 }
155        })
156    }
157    
158    pub fn key_count(&self) -> isize {
159        self.hit_objects
160            .iter()
161            .map(|obj| obj.lane)
162            .max()
163            .unwrap_or(4)
164    }
165    
166    pub fn validate(&self) -> Result<(), String> {
167        if self.hit_objects.is_empty() {
168            return Err("Map has no hit objects.".to_string());
169        }
170        
171        if self.timing_points.is_empty() {
172            return Err("Map has no timing points.".to_string());
173        }
174        
175        for timing_point in &self.timing_points {
176            if timing_point.bpm <= 0.0 {
177                return Err("A timing point has an invalid BPM.".to_string());
178            }
179            
180            if timing_point.signature <= 0 {
181                return Err("A timing point has an invalid signature.".to_string());
182            }
183        }
184        
185        if self.hit_objects.iter().any(|obj| obj.lane < 1) {
186            return Err("A hit object in this map is in a lane below 1.".to_string());
187        }
188        
189        let mode = self.key_count();
190        
191        if mode < Self::MIN_KEYMODE || mode > Self::MAX_KEYMODE {
192            return Err(format!("Map has an invalid keymode: {}. Must be between {} and {}.", 
193                mode, Self::MIN_KEYMODE, Self::MAX_KEYMODE));
194        }
195        
196        Ok(())
197    }
198    
199    pub fn get_normal_notes(&self) -> Vec<&hitobjects::HitObject> {
200        self.hit_objects.iter().filter(|obj| obj.is_normal()).collect()
201    }
202
203    pub fn get_long_notes(&self) -> Vec<&hitobjects::HitObject> {
204        self.hit_objects.iter().filter(|obj| obj.is_ln()).collect()
205    }
206    
207    pub fn get_tick_notes(&self) -> Vec<&hitobjects::HitObject> {
208        self.hit_objects.iter().filter(|obj| obj.is_tick()).collect()
209    }
210}
211
212impl Default for FscFile {
213    fn default() -> Self {
214        Self::new()
215    }
216}