Skip to main content

rhythm_open_exchange/codec/formats/qua/
decoder.rs

1//! Decoder for converting .qua to `RoxChart`.
2
3use crate::codec::Decoder;
4use crate::error::RoxResult;
5use crate::model::{Metadata, Note, RoxChart, TimingPoint};
6
7use super::parser;
8use super::types::QuaChart;
9
10/// Decoder for Quaver beatmaps.
11pub struct QuaDecoder;
12
13impl QuaDecoder {
14    /// Convert a `QuaChart` to `RoxChart`.
15    #[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        // Map metadata
21        chart.metadata = Metadata {
22            // Map Quaver IDs (i32 -> Option<u64>)
23            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            // Quaver tags are space-separated in a single string
45            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        // Convert timing points (BPM)
56        for tp in &qua.timing_points {
57            // Safe: time in ms fits in i64 after multiplying by 1000
58            #[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        // Convert slider velocities to SV timing points
69        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        // Sort timing points by time
80        chart.timing_points.sort_by_key(|tp| tp.time_us);
81
82        // Convert hit objects
83        for ho in &qua.hit_objects {
84            #[allow(clippy::cast_possible_truncation)]
85            let time_us = (ho.start_time * 1000.0) as i64;
86            // Quaver lanes are 1-indexed
87            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        // Sort notes by time
102        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        // Basic validation
126        assert_eq!(chart.key_count(), 4);
127        assert!(!chart.notes.is_empty());
128        assert!(!chart.timing_points.is_empty());
129    }
130}