rgc_chart/models/fluxis/
chart.rs1use 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}