rotterna_lib/decoding/
decode.rs

1use crate::structs::{Chart, Measure, Beat};
2use crate::structs::SmFile;
3use crate::utils::{parse_field, parse_pairs};
4use std::path::PathBuf;
5
6impl SmFile {
7    pub fn from_file(path: PathBuf) -> Result<SmFile, String> {
8        let content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
9        SmFile::parse(&content)
10    }
11
12    pub fn from_string(content: &str) -> Result<SmFile, String> {
13        SmFile::parse(content)
14    }
15
16    fn parse(content: &str) -> Result<SmFile, String> {
17        let mut sm = SmFile::new();
18        sm.metadata.parse(content);
19        sm.parse_bpms(content);
20        sm.parse_stops(content);
21        // Parse offset
22        parse_field(content, r"#OFFSET:([-\d.]+);", &mut sm.offset);
23        sm.offset = sm.offset.abs() * 1000.0;
24        sm.parse_charts(content).map_err(|e| e.to_string())?;
25        return Ok(sm);
26    }
27
28    fn parse_bpms(&mut self, content: &str) {
29        // 1. Utilisation de la fonction générique pour remplir le vecteur
30        parse_pairs(content, r"(?s)#BPMS:(.*?);", &mut self.bpms);
31
32        // 2. Tri (Sort) par beat (le premier élément du tuple)
33        self.bpms
34            .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
35
36        // 3. Gestion de la valeur par défaut si vide
37        if self.bpms.is_empty() {
38            self.bpms.push((0.0, 120.0));
39        }
40    }
41
42    fn parse_stops(&mut self, content: &str) {
43        parse_pairs(content, r"(?s)#STOPS:(.*?);", &mut self.stops);
44
45        self.stops
46            .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
47    }
48
49    fn parse_charts(&mut self, content: &str) -> Result<(), String> {
50        let notes_sections: Vec<&str> = content.split("#NOTES:").skip(1).collect();
51
52        for notes_section in notes_sections {
53            // Find end of section (next #NOTES: or end)
54            let section_end = notes_section.find("#NOTES:").unwrap_or(notes_section.len());
55            let section_content = &notes_section[..section_end];
56
57            let chart = Chart::parse(section_content, &self.bpms).map_err(|e| e.to_string())?;
58            self.charts.push(chart);
59        }
60        Ok(())
61    }
62}
63
64impl Chart {
65    fn parse(content: &str, bpms: &[(f64, f64)]) -> Result<Chart, String> {
66        let lines: Vec<&str> = content.lines().map(|l| l.trim()).collect();
67
68        let mut chart = Chart::new();
69    
70        // Parse chart header
71        let mut idx = chart.parse_header(&lines);
72    
73        // Parse measures
74        // Timing state
75        let mut current_bpm = if bpms.is_empty() { 120.0 } else { bpms[0].1 };
76        let mut current_time_ms = 0.0; // Time in MILLISECONDS
77        let mut current_beat = 0.0; // Position in beats
78        let mut bpm_index = 0;
79
80        while idx < lines.len() {
81            // Check if BPM changes before this measure
82            update_bpm_if_needed(&mut current_bpm, current_beat, &mut bpm_index, bpms);
83
84            // Parse next measure (it will calculate timings internally)
85            let (measure, next_idx, new_time_ms, new_beat) = Measure::parse(
86                &lines, 
87                idx, 
88                current_bpm, 
89                current_time_ms, 
90                current_beat
91            );
92            
93            // Always add measure, even if empty (empty measures represent time)
94            chart.measures.push(measure);
95
96            current_time_ms = new_time_ms;
97            current_beat = new_beat;
98            idx = next_idx;
99
100            // Check if we hit a semicolon (end of chart)
101            if idx > 0 && idx <= lines.len() {
102                let prev_line = lines[idx - 1].trim();
103                let line_without_comment = if let Some(comment_pos) = prev_line.find("//") {
104                    &prev_line[..comment_pos]
105                } else {
106                    prev_line
107                }
108                .trim();
109                
110                if line_without_comment == ";" {
111                    break;
112                }
113            }
114
115            // If we're at EOF, stop
116            if idx >= lines.len() {
117                break;
118            }
119        }
120    
121        Ok(chart)
122    }
123
124    fn parse_header(&mut self, lines: &[&str]) -> usize {
125        let mut idx = 0;
126    
127        // Skip empty lines
128        while idx < lines.len() && lines[idx].is_empty() {
129            idx += 1;
130        }
131    
132        // Stepstype
133        if idx < lines.len() {
134            self.stepstype = lines[idx].to_string();
135            idx += 1;
136        }
137    
138        // Skip description (empty or ":")
139        while idx < lines.len() && (lines[idx].is_empty() || lines[idx] == ":") {
140            idx += 1;
141        }
142    
143        // Difficulty
144        if idx < lines.len() {
145            self.difficulty = lines[idx].to_string();
146            idx += 1;
147        }
148    
149        // Skip empty lines
150        while idx < lines.len() && lines[idx].is_empty() {
151            idx += 1;
152        }
153    
154        // Meter
155        if idx < lines.len() {
156            self.meter = lines[idx].parse().unwrap_or(0);
157            idx += 1;
158        }
159    
160        // Skip empty lines
161        while idx < lines.len() && lines[idx].is_empty() {
162            idx += 1;
163        }
164    
165        // Radar values
166        if idx < lines.len() {
167            for val in lines[idx].split(',') {
168                if let Ok(v) = val.trim().parse::<f64>() {
169                    self.radar_values.push(v);
170                }
171            }
172            idx += 1;
173        }
174    
175        idx
176    }
177}
178
179impl Measure {
180    fn parse(
181        lines: &[&str], 
182        start_idx: usize, 
183        bpm: f64, 
184        start_time_ms: f64, 
185        start_beat: f64,
186    ) -> (Measure, usize, f64, f64) {
187        let mut measure = Measure::new();
188        let mut idx = start_idx;
189
190        // Parse lines until we hit a comma or semicolon
191        while idx < lines.len() {
192            let line = lines[idx].trim();
193
194            if line.is_empty() {
195                idx += 1;
196                continue;
197            }
198
199            // Remove comments from line
200            let line_without_comment = if let Some(comment_pos) = line.find("//") {
201                &line[..comment_pos]
202            } else {
203                line
204            }
205            .trim();
206
207            // Check if line is a measure separator (comma or semicolon)
208            if line_without_comment == "," || line_without_comment == ";" {
209                // End of measure - calculate timings for all beats
210                let beats_in_measure = measure.beats.len();
211                
212                // If measure is empty, assume it has 4 beats (standard measure length)
213                let actual_beats = if beats_in_measure == 0 { 4 } else { beats_in_measure };
214                
215                measure.start_time = start_time_ms;
216                
217                // Calculate time per beat in MILLISECONDS
218                // A measure always represents 4 beats of music in StepMania
219                // Formula: time_per_beat_ms = (60000 / BPM * 4) / beats_in_measure
220                // This divides the total measure time (4 beats of music) by the number of lines
221                let beats_in_measure_f64 = actual_beats as f64;
222                let measure_duration_ms = (60000.0 / bpm) * 4.0; // 4 beats of music per measure
223                let time_per_beat_ms = measure_duration_ms / beats_in_measure_f64;
224
225                // Update each beat with timing (if any)
226                let mut current_time = start_time_ms;
227                let mut current_beat = start_beat;
228                for beat in measure.beats.iter_mut() {
229                    beat.time = current_time;
230                    current_time += time_per_beat_ms;
231                    current_beat += 1.0;
232                }
233                
234                // Advance time even if measure is empty (empty measures still take time)
235                let new_time = start_time_ms + (time_per_beat_ms * actual_beats as f64);
236                let new_beat = start_beat + actual_beats as f64;
237
238                return (measure, idx + 1, new_time, new_beat);
239            } else if Beat::is_note_line(line_without_comment) {
240                // Parse note line using Beat::parse()
241                let beat = Beat::parse(line_without_comment);
242                measure.beats.push(beat);
243            }
244
245            idx += 1;
246        }
247
248        // End of file - calculate timings for all beats
249        let beats_in_measure = measure.beats.len();
250        
251        // If measure is empty, assume it has 4 beats (standard measure length)
252        let actual_beats = if beats_in_measure == 0 { 4 } else { beats_in_measure };
253        
254        measure.start_time = start_time_ms;
255        
256        // Calculate time per beat in MILLISECONDS
257        // A measure always represents 4 beats of music in StepMania
258        // Formula: time_per_beat_ms = (60000 / BPM * 4) / beats_in_measure
259        let beats_in_measure_f64 = actual_beats as f64;
260        let measure_duration_ms = (60000.0 / bpm) * 4.0; // 4 beats of music per measure
261        let time_per_beat_ms = measure_duration_ms / beats_in_measure_f64;
262
263        // Update each beat with timing (if any)
264        let mut current_time = start_time_ms;
265        let mut current_beat = start_beat;
266        for beat in measure.beats.iter_mut() {
267            beat.time = current_time;
268            current_time += time_per_beat_ms;
269            current_beat += 1.0;
270        }
271        
272        // Advance time even if measure is empty (empty measures still take time)
273        let new_time = start_time_ms + (time_per_beat_ms * actual_beats as f64);
274        let new_beat = start_beat + actual_beats as f64;
275
276        (measure, idx, new_time, new_beat)
277    }
278}
279
280impl Beat {
281    pub fn is_note_line(line: &str) -> bool {
282        line.chars()
283            .all(|c| matches!(c, '0' | '1' | '2' | '3' | '4' | 'M'))
284    }
285
286    pub fn parse(line: &str) -> Beat {
287        let notes = line.chars().map(|c| c != '0').collect();
288        Beat {
289            time: 0.0, // Will be calculated when measure ends
290            notes,
291        }
292    }
293}
294
295fn update_bpm_if_needed(
296    current_bpm: &mut f64,
297    current_beat: f64,
298    bpm_index: &mut usize,
299    bpms: &[(f64, f64)],
300) {
301    while *bpm_index < bpms.len() {
302        let (bpm_beat, new_bpm) = bpms[*bpm_index];
303        if bpm_beat <= current_beat {
304            *current_bpm = new_bpm;
305            *bpm_index += 1;
306        } else {
307            break;
308        }
309    }
310}