Skip to main content

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
6// StepMania row system constants
7const ROWS_PER_BEAT: f64 = 48.0;  // 1 beat = 48 rows (for 4/4 time)
8const ROWS_PER_MEASURE: f64 = 192.0;  // 1 measure = 192 rows (4 beats * 48)
9
10impl SmFile {
11    pub fn from_file(path: PathBuf) -> Result<SmFile, String> {
12        let content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
13        SmFile::parse(&content)
14    }
15
16    pub fn from_string(content: &str) -> Result<SmFile, String> {
17        SmFile::parse(content)
18    }
19
20    fn parse(content: &str) -> Result<SmFile, String> {
21        let mut sm = SmFile::new();
22        sm.metadata.parse(content);
23        sm.parse_bpms(content);
24        sm.parse_stops(content);
25        // Parse offset
26        parse_field(content, r"#OFFSET:([-\d.]+);", &mut sm.offset);
27        sm.offset = sm.offset.abs() * 1000.0;
28        sm.parse_charts(content).map_err(|e| e.to_string())?;
29        return Ok(sm);
30    }
31
32    fn parse_bpms(&mut self, content: &str) {
33        // Parse BPM pairs from the file
34        // Format: #BPMS:beat1=bpm1,beat2=bpm2,...;
35        parse_pairs(content, r"(?s)#BPMS:(.*?);", &mut self.bpms);
36
37        // Convert beats to rows (1 beat = 48 rows in StepMania)
38        // Store as (row, bpm) instead of (beat, bpm)
39        for (beat, _bpm) in &mut self.bpms {
40            *beat = *beat * ROWS_PER_BEAT;
41        }
42
43        // Sort by row (first element of tuple)
44        self.bpms
45            .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
46
47        // Ensure we have at least one BPM change at row 0
48        if self.bpms.is_empty() || self.bpms[0].0 > 0.0 {
49            self.bpms.insert(0, (0.0, 120.0));
50        }
51    }
52
53    fn parse_stops(&mut self, content: &str) {
54        // Parse stop pairs from the file
55        // Format: #STOPS:beat1=duration1,beat2=duration2,...;
56        parse_pairs(content, r"(?s)#STOPS:(.*?);", &mut self.stops);
57
58        // Convert beats to rows (1 beat = 48 rows in StepMania)
59        // Store as (row, duration) instead of (beat, duration)
60        for (beat, _duration) in &mut self.stops {
61            *beat = *beat * ROWS_PER_BEAT;
62        }
63
64        // Sort by row (first element of tuple)
65        self.stops
66            .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
67    }
68
69    fn parse_charts(&mut self, content: &str) -> Result<(), String> {
70        let notes_sections: Vec<&str> = content.split("#NOTES:").skip(1).collect();
71
72        for notes_section in notes_sections {
73            // Find end of section (next #NOTES: or end)
74            let section_end = notes_section.find("#NOTES:").unwrap_or(notes_section.len());
75            let section_content = &notes_section[..section_end];
76
77            let chart = Chart::parse(section_content, &self.bpms).map_err(|e| e.to_string())?;
78            self.charts.push(chart);
79        }
80        Ok(())
81    }
82}
83
84impl Chart {
85    fn parse(content: &str, bpms: &[(f64, f64)]) -> Result<Chart, String> {
86        let lines: Vec<&str> = content.lines().map(|l| l.trim()).collect();
87
88        let mut chart = Chart::new();
89    
90        // Parse chart header
91        let mut idx = chart.parse_header(&lines);
92    
93        // Parse measures
94        // Timing state - using row-based system
95        let mut current_bpm = if bpms.is_empty() { 120.0 } else { bpms[0].1 };
96        let mut current_time_ms = 0.0; // Time in MILLISECONDS
97        let mut current_row = 0.0; // Position in rows (not beats!)
98        let mut bpm_index = 0;
99
100        while idx < lines.len() {
101            // Parse next measure (it will handle BPM changes internally)
102            let (measure, next_idx, new_time_ms, new_row) = Measure::parse(
103                &lines, 
104                idx, 
105                bpms,
106                &mut current_bpm,
107                &mut bpm_index,
108                current_time_ms, 
109                current_row
110            );
111            
112            // Always add measure, even if empty (empty measures represent time)
113            chart.measures.push(measure);
114
115            current_time_ms = new_time_ms;
116            current_row = new_row;
117            idx = next_idx;
118
119            // Check if we hit a semicolon (end of chart)
120            if idx > 0 && idx <= lines.len() {
121                let prev_line = lines[idx - 1].trim();
122                let line_without_comment = if let Some(comment_pos) = prev_line.find("//") {
123                    &prev_line[..comment_pos]
124                } else {
125                    prev_line
126                }
127                .trim();
128                
129                if line_without_comment == ";" {
130                    break;
131                }
132            }
133
134            // If we're at EOF, stop
135            if idx >= lines.len() {
136                break;
137            }
138        }
139    
140        Ok(chart)
141    }
142
143    fn parse_header(&mut self, lines: &[&str]) -> usize {
144        let mut idx = 0;
145    
146        // Skip empty lines
147        while idx < lines.len() && lines[idx].is_empty() {
148            idx += 1;
149        }
150    
151        // Stepstype
152        if idx < lines.len() {
153            self.stepstype = lines[idx].to_string();
154            idx += 1;
155        }
156    
157        // Skip description (empty or ":")
158        while idx < lines.len() && (lines[idx].is_empty() || lines[idx] == ":") {
159            idx += 1;
160        }
161    
162        // Difficulty
163        if idx < lines.len() {
164            self.difficulty = lines[idx].to_string();
165            idx += 1;
166        }
167    
168        // Skip empty lines
169        while idx < lines.len() && lines[idx].is_empty() {
170            idx += 1;
171        }
172    
173        // Meter
174        if idx < lines.len() {
175            self.meter = lines[idx].parse().unwrap_or(0);
176            idx += 1;
177        }
178    
179        // Skip empty lines
180        while idx < lines.len() && lines[idx].is_empty() {
181            idx += 1;
182        }
183    
184        // Radar values
185        if idx < lines.len() {
186            for val in lines[idx].split(',') {
187                if let Ok(v) = val.trim().parse::<f64>() {
188                    self.radar_values.push(v);
189                }
190            }
191            idx += 1;
192        }
193    
194        idx
195    }
196}
197
198impl Measure {
199    fn parse(
200        lines: &[&str], 
201        start_idx: usize, 
202        bpms: &[(f64, f64)],  // (row, bpm) pairs
203        current_bpm: &mut f64,
204        bpm_index: &mut usize,
205        start_time_ms: f64, 
206        start_row: f64,
207    ) -> (Measure, usize, f64, f64) {
208        let mut measure = Measure::new();
209        let mut idx = start_idx;
210
211        // Parse lines until we hit a comma or semicolon
212        let mut note_lines = Vec::new();
213        while idx < lines.len() {
214            let line = lines[idx].trim();
215
216            if line.is_empty() {
217                idx += 1;
218                continue;
219            }
220
221            // Remove comments from line
222            let line_without_comment = if let Some(comment_pos) = line.find("//") {
223                &line[..comment_pos]
224            } else {
225                line
226            }
227            .trim();
228
229            // Check if line is a measure separator (comma or semicolon)
230            if line_without_comment == "," || line_without_comment == ";" {
231                break;
232            } else if Beat::is_note_line(line_without_comment) {
233                // Store note lines for later processing
234                note_lines.push(line_without_comment);
235            }
236
237            idx += 1;
238        }
239
240        // Calculate quantization (rows per note line)
241        // A measure has 192 rows total
242        let num_lines = note_lines.len();
243        let quantization = if num_lines > 0 {
244            // Determine quantization by checking if measure uses full 192 rows
245            if num_lines == ROWS_PER_MEASURE as usize {
246                // Full 192-row measure - check for custom quantization
247                // Try to find the smallest valid quantization
248                let mut found_quant = ROWS_PER_MEASURE as usize;
249                for test_quant in [4, 8, 12, 16, 24, 32, 48, 64, 96] {
250                    if ROWS_PER_MEASURE as usize % test_quant == 0 {
251                        // For now, accept the first valid quantization
252                        // A more sophisticated check would verify that compressed rows are empty
253                        found_quant = test_quant;
254                        break;
255                    }
256                }
257                found_quant
258            } else if num_lines > 0 {
259                // Calculate quantization from number of lines
260                ROWS_PER_MEASURE as usize / num_lines
261            } else {
262                ROWS_PER_MEASURE as usize
263            }
264        } else {
265            ROWS_PER_MEASURE as usize
266        };
267
268        // Process note lines and calculate timings
269        measure.start_time = start_time_ms;
270        let mut current_time = start_time_ms;
271        let mut current_row = start_row;
272
273        for (line_idx, line) in note_lines.iter().enumerate() {
274            // Calculate row position for this note line
275            let row_offset = if num_lines > 0 {
276                // Handle quantization
277                if ROWS_PER_MEASURE as usize % num_lines == 0 {
278                    (line_idx * quantization) as f64
279                } else {
280                    // Non-uniform spacing
281                    (ROWS_PER_MEASURE as f64 / num_lines as f64) * line_idx as f64
282                }
283            } else {
284                0.0
285            };
286
287            let note_row = start_row + row_offset;
288
289            // Check for BPM changes up to this row
290            while *bpm_index < bpms.len() {
291                let (bpm_row, new_bpm) = bpms[*bpm_index];
292                if bpm_row <= note_row {
293                    // Calculate time elapsed from previous position to this BPM change
294                    if bpm_row > current_row {
295                        let rows_elapsed = bpm_row - current_row;
296                        let beats_elapsed = rows_elapsed / ROWS_PER_BEAT;
297                        let time_elapsed_ms = (beats_elapsed / *current_bpm) * 60000.0;
298                        current_time += time_elapsed_ms;
299                        current_row = bpm_row;
300                    }
301                    *current_bpm = new_bpm;
302                    *bpm_index += 1;
303                } else {
304                    break;
305                }
306            }
307
308            // Calculate time for this note line
309            if note_row > current_row {
310                let rows_elapsed = note_row - current_row;
311                let beats_elapsed = rows_elapsed / ROWS_PER_BEAT;
312                let time_elapsed_ms = (beats_elapsed / *current_bpm) * 60000.0;
313                current_time += time_elapsed_ms;
314                current_row = note_row;
315            }
316
317            // Parse and store the beat
318            let mut beat = Beat::parse(line);
319            beat.time = current_time;
320            measure.beats.push(beat);
321        }
322
323        // Calculate final row and time for the end of the measure
324        let end_row = start_row + ROWS_PER_MEASURE;
325        
326        // Check for any remaining BPM changes before end of measure
327        while *bpm_index < bpms.len() {
328            let (bpm_row, new_bpm) = bpms[*bpm_index];
329            if bpm_row < end_row {
330                // Calculate time elapsed to this BPM change
331                if bpm_row > current_row {
332                    let rows_elapsed = bpm_row - current_row;
333                    let beats_elapsed = rows_elapsed / ROWS_PER_BEAT;
334                    let time_elapsed_ms = (beats_elapsed / *current_bpm) * 60000.0;
335                    current_time += time_elapsed_ms;
336                    current_row = bpm_row;
337                }
338                *current_bpm = new_bpm;
339                *bpm_index += 1;
340            } else {
341                break;
342            }
343        }
344
345        // Calculate time from current position to end of measure
346        if end_row > current_row {
347            let rows_elapsed = end_row - current_row;
348            let beats_elapsed = rows_elapsed / ROWS_PER_BEAT;
349            let time_elapsed_ms = (beats_elapsed / *current_bpm) * 60000.0;
350            current_time += time_elapsed_ms;
351        }
352
353        let next_idx = if idx < lines.len() && (lines[idx].trim() == "," || lines[idx].trim() == ";") {
354            idx + 1
355        } else {
356            idx
357        };
358
359        (measure, next_idx, current_time, end_row)
360    }
361}
362
363impl Beat {
364    pub fn is_note_line(line: &str) -> bool {
365        line.chars()
366            .all(|c| matches!(c, '0' | '1' | '2' | '3' | '4' | 'M'))
367    }
368
369    pub fn parse(line: &str) -> Beat {
370        let notes = line.chars().map(|c| c != '0').collect();
371        Beat {
372            time: 0.0, // Will be calculated when measure ends
373            notes,
374        }
375    }
376}
377