#![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 {
key_count: chart.column_count,
title: sm.metadata.title.clone().into(),
artist: sm.metadata.artist.clone().into(),
creator: sm.metadata.credit.clone().into(),
difficulty_name: chart.difficulty.clone().into(),
#[allow(clippy::cast_precision_loss)]
difficulty_value: Some(chart.meter as f32),
audio_file: sm.metadata.music.clone().into(),
background_file: if sm.metadata.background.is_empty() {
None
} else {
Some(sm.metadata.background.clone().into())
},
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: Some(sm.metadata.banner.clone().into()),
genre: None,
language: None,
tags: Vec::new(),
is_coop: false,
..Default::default()
};
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())
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codec::Decoder;
const BASIC_SM: &str = r#"
#TITLE:Test Song;
#ARTIST:Test Artist;
#CREDIT:Test Mapper;
#MUSIC:song.ogg;
#OFFSET:0;
#BPMS:0=120;
#STOPS:;
#NOTES:
dance-single:
:
Beginner:
1:
0,0,0,0,0:
0000
1000
0100
0010
,
0001
0000
0000
0000
;
"#;
#[test]
fn test_decode_basic_sm() {
let chart = <SmDecoder as Decoder>::decode(BASIC_SM.as_bytes()).expect("Failed to decode");
assert_eq!(chart.key_count(), 4);
assert_eq!(chart.metadata.title, "Test Song");
assert_eq!(chart.metadata.artist, "Test Artist");
assert_eq!(chart.metadata.creator, "Test Mapper");
assert_eq!(chart.metadata.difficulty_name, "Beginner");
assert!(!chart.notes.is_empty());
}
#[test]
fn test_sm_note_count() {
let chart = <SmDecoder as Decoder>::decode(BASIC_SM.as_bytes()).expect("Failed to decode");
assert_eq!(chart.notes.len(), 4);
}
#[test]
fn test_sm_timing_points() {
let chart = <SmDecoder as Decoder>::decode(BASIC_SM.as_bytes()).expect("Failed to decode");
assert!(!chart.timing_points.is_empty());
assert_eq!(chart.timing_points[0].bpm, 120.0);
}
#[test]
fn test_decode_asset_4k() {
let data = crate::test_utils::get_test_asset("stepmania/4k.sm");
let chart = <SmDecoder as Decoder>::decode(&data).expect("Failed to decode 4k.sm");
assert_eq!(chart.key_count(), 4);
}
}