#![allow(clippy::doc_markdown)]
use crate::codec::Decoder;
use crate::error::RoxResult;
use crate::model::{Metadata, Note, RoxChart, TimingPoint};
use super::parser;
use super::types::{SmChart, SmFile, SmNoteType};
pub struct SmDecoder;
impl SmDecoder {
#[must_use]
pub fn from_file(sm: &SmFile) -> Option<RoxChart> {
sm.charts.first().map(|chart| Self::from_chart(sm, chart))
}
#[must_use]
pub fn from_chart(sm: &SmFile, chart: &SmChart) -> RoxChart {
let mut rox = RoxChart::new(chart.column_count);
rox.metadata = Metadata {
title: sm.metadata.title.clone(),
artist: sm.metadata.artist.clone(),
creator: sm.metadata.credit.clone(),
difficulty_name: chart.difficulty.clone(),
#[allow(clippy::cast_precision_loss)]
difficulty_value: Some(chart.meter as f32),
audio_file: sm.metadata.music.clone(),
background_file: if sm.metadata.background.is_empty() {
None
} else {
Some(sm.metadata.background.clone())
},
audio_offset_us: -sm.offset_us, #[allow(clippy::cast_possible_truncation)]
preview_time_us: (sm.metadata.sample_start * 1_000_000.0) as i64,
#[allow(clippy::cast_possible_truncation)]
preview_duration_us: (sm.metadata.sample_length * 1_000_000.0) as i64,
source: None,
genre: None,
language: None,
tags: Vec::new(),
is_coop: false,
};
for (time_us, bpm) in &sm.bpms {
rox.timing_points.push(TimingPoint::bpm(*time_us, *bpm));
}
let mut pending_holds: Vec<(i64, u8)> = Vec::new(); let mut pending_rolls: Vec<(i64, u8)> = Vec::new();
let mut sorted_notes = chart.notes.clone();
sorted_notes.sort_by(|a, b| a.time_us.cmp(&b.time_us).then(a.column.cmp(&b.column)));
for note in &sorted_notes {
match note.note_type {
SmNoteType::Tap => {
rox.notes.push(Note::tap(note.time_us, note.column));
}
SmNoteType::HoldHead => {
pending_holds.push((note.time_us, note.column));
}
SmNoteType::RollHead => {
pending_rolls.push((note.time_us, note.column));
}
SmNoteType::Tail => {
if let Some(idx) = pending_holds
.iter()
.position(|(_, col)| *col == note.column)
{
let (start_time, column) = pending_holds.remove(idx);
let duration = note.time_us - start_time;
rox.notes.push(Note::hold(start_time, duration, column));
} else if let Some(idx) = pending_rolls
.iter()
.position(|(_, col)| *col == note.column)
{
let (start_time, column) = pending_rolls.remove(idx);
let duration = note.time_us - start_time;
rox.notes.push(Note::burst(start_time, duration, column));
}
}
SmNoteType::Mine => {
rox.notes.push(Note::mine(note.time_us, note.column));
}
SmNoteType::Lift => {
rox.notes.push(Note::tap(note.time_us, note.column));
}
SmNoteType::Empty | SmNoteType::Fake => {
}
}
}
rox.notes.sort_by_key(|n| n.time_us);
rox
}
#[must_use]
pub fn decode_all(sm: &SmFile) -> Vec<RoxChart> {
sm.charts
.iter()
.map(|chart| Self::from_chart(sm, chart))
.collect()
}
}
impl Decoder for SmDecoder {
fn decode(data: &[u8]) -> RoxResult<RoxChart> {
let sm = parser::parse(data)?;
sm.charts
.first()
.map(|chart| Self::from_chart(&sm, chart))
.ok_or_else(|| {
crate::error::RoxError::InvalidFormat("No charts found in SM file".into())
})
}
}