use crate::structs::{Chart, Measure, Beat};
use crate::structs::SmFile;
use crate::utils::{parse_field, parse_pairs};
use std::path::PathBuf;
impl SmFile {
pub fn from_file(path: PathBuf) -> Result<SmFile, String> {
let content = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
SmFile::parse(&content)
}
pub fn from_string(content: &str) -> Result<SmFile, String> {
SmFile::parse(content)
}
fn parse(content: &str) -> Result<SmFile, String> {
let mut sm = SmFile::new();
sm.metadata.parse(content);
sm.parse_bpms(content);
sm.parse_stops(content);
parse_field(content, r"#OFFSET:([-\d.]+);", &mut sm.offset);
sm.offset = sm.offset.abs() * 1000.0;
sm.parse_charts(content).map_err(|e| e.to_string())?;
return Ok(sm);
}
fn parse_bpms(&mut self, content: &str) {
parse_pairs(content, r"(?s)#BPMS:(.*?);", &mut self.bpms);
self.bpms
.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
if self.bpms.is_empty() {
self.bpms.push((0.0, 120.0));
}
}
fn parse_stops(&mut self, content: &str) {
parse_pairs(content, r"(?s)#STOPS:(.*?);", &mut self.stops);
self.stops
.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
}
fn parse_charts(&mut self, content: &str) -> Result<(), String> {
let notes_sections: Vec<&str> = content.split("#NOTES:").skip(1).collect();
for notes_section in notes_sections {
let section_end = notes_section.find("#NOTES:").unwrap_or(notes_section.len());
let section_content = ¬es_section[..section_end];
let chart = Chart::parse(section_content, &self.bpms).map_err(|e| e.to_string())?;
self.charts.push(chart);
}
Ok(())
}
}
impl Chart {
fn parse(content: &str, bpms: &[(f64, f64)]) -> Result<Chart, String> {
let lines: Vec<&str> = content.lines().map(|l| l.trim()).collect();
let mut chart = Chart::new();
let mut idx = chart.parse_header(&lines);
let mut current_bpm = if bpms.is_empty() { 120.0 } else { bpms[0].1 };
let mut current_time_ms = 0.0; let mut current_beat = 0.0; let mut bpm_index = 0;
while idx < lines.len() {
update_bpm_if_needed(&mut current_bpm, current_beat, &mut bpm_index, bpms);
let (measure, next_idx, new_time_ms, new_beat) = Measure::parse(
&lines,
idx,
current_bpm,
current_time_ms,
current_beat
);
chart.measures.push(measure);
current_time_ms = new_time_ms;
current_beat = new_beat;
idx = next_idx;
if idx > 0 && idx <= lines.len() {
let prev_line = lines[idx - 1].trim();
let line_without_comment = if let Some(comment_pos) = prev_line.find("//") {
&prev_line[..comment_pos]
} else {
prev_line
}
.trim();
if line_without_comment == ";" {
break;
}
}
if idx >= lines.len() {
break;
}
}
Ok(chart)
}
fn parse_header(&mut self, lines: &[&str]) -> usize {
let mut idx = 0;
while idx < lines.len() && lines[idx].is_empty() {
idx += 1;
}
if idx < lines.len() {
self.stepstype = lines[idx].to_string();
idx += 1;
}
while idx < lines.len() && (lines[idx].is_empty() || lines[idx] == ":") {
idx += 1;
}
if idx < lines.len() {
self.difficulty = lines[idx].to_string();
idx += 1;
}
while idx < lines.len() && lines[idx].is_empty() {
idx += 1;
}
if idx < lines.len() {
self.meter = lines[idx].parse().unwrap_or(0);
idx += 1;
}
while idx < lines.len() && lines[idx].is_empty() {
idx += 1;
}
if idx < lines.len() {
for val in lines[idx].split(',') {
if let Ok(v) = val.trim().parse::<f64>() {
self.radar_values.push(v);
}
}
idx += 1;
}
idx
}
}
impl Measure {
fn parse(
lines: &[&str],
start_idx: usize,
bpm: f64,
start_time_ms: f64,
start_beat: f64,
) -> (Measure, usize, f64, f64) {
let mut measure = Measure::new();
let mut idx = start_idx;
while idx < lines.len() {
let line = lines[idx].trim();
if line.is_empty() {
idx += 1;
continue;
}
let line_without_comment = if let Some(comment_pos) = line.find("//") {
&line[..comment_pos]
} else {
line
}
.trim();
if line_without_comment == "," || line_without_comment == ";" {
let beats_in_measure = measure.beats.len();
let actual_beats = if beats_in_measure == 0 { 4 } else { beats_in_measure };
measure.start_time = start_time_ms;
let beats_in_measure_f64 = actual_beats as f64;
let measure_duration_ms = (60000.0 / bpm) * 4.0; let time_per_beat_ms = measure_duration_ms / beats_in_measure_f64;
let mut current_time = start_time_ms;
let mut current_beat = start_beat;
for beat in measure.beats.iter_mut() {
beat.time = current_time;
current_time += time_per_beat_ms;
current_beat += 1.0;
}
let new_time = start_time_ms + (time_per_beat_ms * actual_beats as f64);
let new_beat = start_beat + actual_beats as f64;
return (measure, idx + 1, new_time, new_beat);
} else if Beat::is_note_line(line_without_comment) {
let beat = Beat::parse(line_without_comment);
measure.beats.push(beat);
}
idx += 1;
}
let beats_in_measure = measure.beats.len();
let actual_beats = if beats_in_measure == 0 { 4 } else { beats_in_measure };
measure.start_time = start_time_ms;
let beats_in_measure_f64 = actual_beats as f64;
let measure_duration_ms = (60000.0 / bpm) * 4.0; let time_per_beat_ms = measure_duration_ms / beats_in_measure_f64;
let mut current_time = start_time_ms;
let mut current_beat = start_beat;
for beat in measure.beats.iter_mut() {
beat.time = current_time;
current_time += time_per_beat_ms;
current_beat += 1.0;
}
let new_time = start_time_ms + (time_per_beat_ms * actual_beats as f64);
let new_beat = start_beat + actual_beats as f64;
(measure, idx, new_time, new_beat)
}
}
impl Beat {
pub fn is_note_line(line: &str) -> bool {
line.chars()
.all(|c| matches!(c, '0' | '1' | '2' | '3' | '4' | 'M'))
}
pub fn parse(line: &str) -> Beat {
let notes = line.chars().map(|c| c != '0').collect();
Beat {
time: 0.0, notes,
}
}
}
fn update_bpm_if_needed(
current_bpm: &mut f64,
current_beat: f64,
bpm_index: &mut usize,
bpms: &[(f64, f64)],
) {
while *bpm_index < bpms.len() {
let (bpm_beat, new_bpm) = bpms[*bpm_index];
if bpm_beat <= current_beat {
*current_bpm = new_bpm;
*bpm_index += 1;
} else {
break;
}
}
}