use crate::codec::Encoder;
use crate::error::RoxResult;
use crate::model::RoxChart;
use super::types::{QuaChart, QuaHitObject, QuaSliderVelocity, QuaTimingPoint};
pub struct QuaEncoder;
impl Encoder for QuaEncoder {
fn encode(chart: &RoxChart) -> RoxResult<Vec<u8>> {
use compact_str::CompactString;
let mut qua = QuaChart {
audio_file: chart.metadata.audio_file.to_string(),
#[allow(clippy::cast_possible_truncation)]
preview_time: (chart.metadata.preview_time_us / 1000) as i32,
background_file: Some(chart
.metadata
.background_file
.as_ref()
.unwrap_or(&CompactString::new(""))
.to_string()),
map_id: if let Some(id) = chart.metadata.chart_id {
id as i32
} else {
-1
},
title: chart.metadata.title.to_string(),
artist: chart.metadata.artist.to_string(),
creator: chart.metadata.creator.to_string(),
difficulty_name: chart.metadata.difficulty_name.to_string(),
source: Some(chart.metadata.source.clone().unwrap_or_default().to_string()),
tags: Some(chart
.metadata
.tags
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(" ")),
description: None,
initial_scroll_velocity: 1.0,
bpm_does_not_affect_sv: true,
..Default::default()
};
for tp in &chart.timing_points {
#[allow(clippy::cast_precision_loss)]
let start_time = tp.time_us as f64 / 1000.0;
if tp.is_inherited {
qua.slider_velocities.push(QuaSliderVelocity {
start_time,
multiplier: f64::from(tp.scroll_speed),
});
} else {
qua.timing_points.push(QuaTimingPoint {
start_time,
bpm: tp.bpm,
signature: None,
});
}
}
for note in &chart.notes {
#[allow(clippy::cast_precision_loss)]
let start_time = note.time_us as f64 / 1000.0;
let lane = note.column + 1;
let end_time = match ¬e.note_type {
crate::model::NoteType::Hold { duration_us } => {
#[allow(clippy::cast_precision_loss)]
let end = (note.time_us + duration_us) as f64 / 1000.0;
Some(end)
}
_ => None,
};
qua.hit_objects.push(QuaHitObject {
start_time,
lane,
end_time,
key_sounds: Vec::new(),
});
}
let yaml = serde_yaml::to_string(&qua).map_err(|e| {
crate::error::RoxError::InvalidFormat(format!("YAML encoding error: {e}"))
})?;
Ok(yaml.into_bytes())
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_roundtrip() {
use super::*;
use crate::codec::Decoder;
use crate::codec::formats::qua::QuaDecoder;
let data = crate::test_utils::get_test_asset("quaver/4K.qua");
let chart1 = QuaDecoder::decode(&data).unwrap();
let encoded = QuaEncoder::encode(&chart1).unwrap();
let chart2 = QuaDecoder::decode(&encoded).unwrap();
assert_eq!(chart1.key_count(), chart2.key_count());
assert_eq!(chart1.notes.len(), chart2.notes.len());
for (n1, n2) in chart1.notes.iter().zip(chart2.notes.iter()) {
assert_eq!(n1.column, n2.column);
assert!(
(n1.time_us - n2.time_us).abs() <= 1000,
"Note time mismatch"
);
}
assert_eq!(chart1.timing_points.len(), chart2.timing_points.len());
for (tp1, tp2) in chart1.timing_points.iter().zip(chart2.timing_points.iter()) {
assert!(
(tp1.time_us - tp2.time_us).abs() <= 1000,
"Timing point time mismatch"
);
if !tp1.is_inherited {
assert!((tp1.bpm - tp2.bpm).abs() < 0.01, "BPM mismatch");
}
}
}
}