rhythm_open_exchange/codec/formats/qua/
decoder.rs1use crate::codec::Decoder;
4use crate::error::RoxResult;
5use crate::model::{Metadata, Note, RoxChart, TimingPoint};
6
7use super::parser;
8use super::types::QuaChart;
9
10pub struct QuaDecoder;
12
13impl QuaDecoder {
14 #[must_use]
16 pub fn from_qua(qua: &QuaChart) -> RoxChart {
17 let key_count = qua.mode.key_count();
18 let mut chart = RoxChart::new(key_count);
19
20 chart.metadata = Metadata {
22 chart_id: if qua.map_id > 0 {
24 #[allow(clippy::cast_sign_loss)]
25 Some(qua.map_id as u64)
26 } else {
27 None
28 },
29 chartset_id: if qua.map_set_id > 0 {
30 #[allow(clippy::cast_sign_loss)]
31 Some(qua.map_set_id as u64)
32 } else {
33 None
34 },
35 key_count,
36 title: qua.title.clone().into(),
37 artist: qua.artist.clone().into(),
38 creator: qua.creator.clone().into(),
39 difficulty_name: qua.difficulty_name.clone().into(),
40 audio_file: qua.audio_file.clone().into(),
41 background_file: qua.background_file.clone().map(|s| s.into()),
42 preview_time_us: i64::from(qua.preview_time) * 1000,
43 source: qua.source.clone().map(|s| s.into()),
44 tags: qua
46 .tags
47 .as_deref()
48 .unwrap_or("")
49 .split_whitespace()
50 .map(|s| s.into())
51 .collect(),
52 ..Default::default()
53 };
54
55 for tp in &qua.timing_points {
57 #[allow(clippy::cast_possible_truncation)]
59 let time_us = (tp.start_time * 1000.0) as i64;
60 let mut timing = TimingPoint::bpm(time_us, tp.bpm);
61 timing.signature = tp
62 .signature
63 .as_ref()
64 .map_or(4, super::types::TimeSignature::beats);
65 chart.timing_points.push(timing);
66 }
67
68 for sv in &qua.slider_velocities {
70 #[allow(clippy::cast_possible_truncation)]
71 let time_us = (sv.start_time * 1000.0) as i64;
72 #[allow(clippy::cast_possible_truncation)]
73 let multiplier = sv.multiplier as f32;
74 chart
75 .timing_points
76 .push(TimingPoint::sv(time_us, multiplier));
77 }
78
79 chart.timing_points.sort_by_key(|tp| tp.time_us);
81
82 for ho in &qua.hit_objects {
84 #[allow(clippy::cast_possible_truncation)]
85 let time_us = (ho.start_time * 1000.0) as i64;
86 let column = ho.lane.saturating_sub(1);
88
89 let note = if let Some(end_time) = ho.end_time {
90 #[allow(clippy::cast_possible_truncation)]
91 let end_us = (end_time * 1000.0) as i64;
92 let duration_us = end_us - time_us;
93 Note::hold(time_us, duration_us, column)
94 } else {
95 Note::tap(time_us, column)
96 };
97
98 chart.notes.push(note);
99 }
100
101 chart.notes.sort_by_key(|n| n.time_us);
103
104 chart
105 }
106}
107
108impl Decoder for QuaDecoder {
109 fn decode(data: &[u8]) -> RoxResult<RoxChart> {
110 let qua = parser::parse(data)?;
111 Ok(Self::from_qua(&qua))
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::codec::Decoder;
119
120 #[test]
121 fn test_decode_asset_4k() {
122 let data = crate::test_utils::get_test_asset("quaver/4K.qua");
123 let chart = <QuaDecoder as Decoder>::decode(&data).expect("Failed to decode 4K.qua");
124
125 assert_eq!(chart.key_count(), 4);
127 assert!(!chart.notes.is_empty());
128 assert!(!chart.timing_points.is_empty());
129 }
130}