rotterna_lib/decoding/
decode.rs1use crate::structs::{Chart, Measure, Beat};
2use crate::structs::SmFile;
3use crate::utils::{parse_field, parse_pairs};
4use std::path::PathBuf;
5
6const ROWS_PER_BEAT: f64 = 48.0; const ROWS_PER_MEASURE: f64 = 192.0; impl 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_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_pairs(content, r"(?s)#BPMS:(.*?);", &mut self.bpms);
36
37 for (beat, _bpm) in &mut self.bpms {
40 *beat = *beat * ROWS_PER_BEAT;
41 }
42
43 self.bpms
45 .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
46
47 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_pairs(content, r"(?s)#STOPS:(.*?);", &mut self.stops);
57
58 for (beat, _duration) in &mut self.stops {
61 *beat = *beat * ROWS_PER_BEAT;
62 }
63
64 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 let section_end = notes_section.find("#NOTES:").unwrap_or(notes_section.len());
75 let section_content = ¬es_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 let mut idx = chart.parse_header(&lines);
92
93 let mut current_bpm = if bpms.is_empty() { 120.0 } else { bpms[0].1 };
96 let mut current_time_ms = 0.0; let mut current_row = 0.0; let mut bpm_index = 0;
99
100 while idx < lines.len() {
101 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 chart.measures.push(measure);
114
115 current_time_ms = new_time_ms;
116 current_row = new_row;
117 idx = next_idx;
118
119 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 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 while idx < lines.len() && lines[idx].is_empty() {
148 idx += 1;
149 }
150
151 if idx < lines.len() {
153 self.stepstype = lines[idx].to_string();
154 idx += 1;
155 }
156
157 while idx < lines.len() && (lines[idx].is_empty() || lines[idx] == ":") {
159 idx += 1;
160 }
161
162 if idx < lines.len() {
164 self.difficulty = lines[idx].to_string();
165 idx += 1;
166 }
167
168 while idx < lines.len() && lines[idx].is_empty() {
170 idx += 1;
171 }
172
173 if idx < lines.len() {
175 self.meter = lines[idx].parse().unwrap_or(0);
176 idx += 1;
177 }
178
179 while idx < lines.len() && lines[idx].is_empty() {
181 idx += 1;
182 }
183
184 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)], 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 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 let line_without_comment = if let Some(comment_pos) = line.find("//") {
223 &line[..comment_pos]
224 } else {
225 line
226 }
227 .trim();
228
229 if line_without_comment == "," || line_without_comment == ";" {
231 break;
232 } else if Beat::is_note_line(line_without_comment) {
233 note_lines.push(line_without_comment);
235 }
236
237 idx += 1;
238 }
239
240 let num_lines = note_lines.len();
243 let quantization = if num_lines > 0 {
244 if num_lines == ROWS_PER_MEASURE as usize {
246 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 found_quant = test_quant;
254 break;
255 }
256 }
257 found_quant
258 } else if num_lines > 0 {
259 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 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 let row_offset = if num_lines > 0 {
276 if ROWS_PER_MEASURE as usize % num_lines == 0 {
278 (line_idx * quantization) as f64
279 } else {
280 (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 while *bpm_index < bpms.len() {
291 let (bpm_row, new_bpm) = bpms[*bpm_index];
292 if bpm_row <= note_row {
293 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 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 let mut beat = Beat::parse(line);
319 beat.time = current_time;
320 measure.beats.push(beat);
321 }
322
323 let end_row = start_row + ROWS_PER_MEASURE;
325
326 while *bpm_index < bpms.len() {
328 let (bpm_row, new_bpm) = bpms[*bpm_index];
329 if bpm_row < end_row {
330 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 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, notes,
374 }
375 }
376}
377