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
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_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 parse_pairs(content, r"(?s)#BPMS:(.*?);", &mut self.bpms);
31
32 self.bpms
34 .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
35
36 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 let section_end = notes_section.find("#NOTES:").unwrap_or(notes_section.len());
55 let section_content = ¬es_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 let mut idx = chart.parse_header(&lines);
72
73 let mut current_bpm = if bpms.is_empty() { 120.0 } else { bpms[0].1 };
76 let mut current_time_ms = 0.0; let mut current_beat = 0.0; let mut bpm_index = 0;
79
80 while idx < lines.len() {
81 update_bpm_if_needed(&mut current_bpm, current_beat, &mut bpm_index, bpms);
83
84 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 chart.measures.push(measure);
95
96 current_time_ms = new_time_ms;
97 current_beat = new_beat;
98 idx = next_idx;
99
100 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 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 while idx < lines.len() && lines[idx].is_empty() {
129 idx += 1;
130 }
131
132 if idx < lines.len() {
134 self.stepstype = lines[idx].to_string();
135 idx += 1;
136 }
137
138 while idx < lines.len() && (lines[idx].is_empty() || lines[idx] == ":") {
140 idx += 1;
141 }
142
143 if idx < lines.len() {
145 self.difficulty = lines[idx].to_string();
146 idx += 1;
147 }
148
149 while idx < lines.len() && lines[idx].is_empty() {
151 idx += 1;
152 }
153
154 if idx < lines.len() {
156 self.meter = lines[idx].parse().unwrap_or(0);
157 idx += 1;
158 }
159
160 while idx < lines.len() && lines[idx].is_empty() {
162 idx += 1;
163 }
164
165 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 while idx < lines.len() {
192 let line = lines[idx].trim();
193
194 if line.is_empty() {
195 idx += 1;
196 continue;
197 }
198
199 let line_without_comment = if let Some(comment_pos) = line.find("//") {
201 &line[..comment_pos]
202 } else {
203 line
204 }
205 .trim();
206
207 if line_without_comment == "," || line_without_comment == ";" {
209 let beats_in_measure = measure.beats.len();
211
212 let actual_beats = if beats_in_measure == 0 { 4 } else { beats_in_measure };
214
215 measure.start_time = start_time_ms;
216
217 let beats_in_measure_f64 = actual_beats as f64;
222 let measure_duration_ms = (60000.0 / bpm) * 4.0; let time_per_beat_ms = measure_duration_ms / beats_in_measure_f64;
224
225 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 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 let beat = Beat::parse(line_without_comment);
242 measure.beats.push(beat);
243 }
244
245 idx += 1;
246 }
247
248 let beats_in_measure = measure.beats.len();
250
251 let actual_beats = if beats_in_measure == 0 { 4 } else { beats_in_measure };
253
254 measure.start_time = start_time_ms;
255
256 let beats_in_measure_f64 = actual_beats as f64;
260 let measure_duration_ms = (60000.0 / bpm) * 4.0; let time_per_beat_ms = measure_duration_ms / beats_in_measure_f64;
262
263 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 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, 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}