minacalc_rs/
wrapper.rs

1use crate::{NoteInfo, Ssr, MsdForAllRates as BindingsMsdForAllRates, CalcHandle, create_calc, calc_version, calc_msd, calc_ssr, destroy_calc};
2use crate::error::{MinaCalcError, MinaCalcResult};
3
4/// Represents a note in the rhythm game
5#[derive(Debug, Clone, Copy)]
6pub struct Note {
7    /// Number of notes at this time position
8    pub notes: u32,
9    /// Row time (in seconds)
10    pub row_time: f32,
11}
12
13impl Note {
14    /// Validates note data
15    pub fn validate(&self) -> MinaCalcResult<()> {
16        if self.notes == 0 {
17            return Err(MinaCalcError::InvalidNoteData("Note must have at least one column".to_string()));
18        }
19        if self.notes > 0b1111 {
20            return Err(MinaCalcError::InvalidNoteData("Note bitflags exceed 4K limit".to_string()));
21        }
22        if self.row_time < 0.0 {
23            return Err(MinaCalcError::InvalidNoteData("Row time cannot be negative".to_string()));
24        }
25        Ok(())
26    }
27}
28
29impl From<Note> for NoteInfo {
30    fn from(note: Note) -> Self {
31        NoteInfo {
32            notes: note.notes,
33            rowTime: note.row_time,
34        }
35    }
36}
37
38impl From<NoteInfo> for Note {
39    fn from(note_info: NoteInfo) -> Self {
40        Note {
41            notes: note_info.notes,
42            row_time: note_info.rowTime,
43        }
44    }
45}
46
47/// Represents difficulty scores for different skillsets
48#[derive(Debug, Clone, Copy)]
49pub struct SkillsetScores {
50    pub overall: f32,
51    pub stream: f32,
52    pub jumpstream: f32,
53    pub handstream: f32,
54    pub stamina: f32,
55    pub jackspeed: f32,
56    pub chordjack: f32,
57    pub technical: f32,
58}
59
60impl SkillsetScores {
61    /// Validates scores are within reasonable bounds
62    pub fn validate(&self) -> MinaCalcResult<()> {
63        let scores = [
64            self.overall, self.stream, self.jumpstream, self.handstream,
65            self.stamina, self.jackspeed, self.chordjack, self.technical
66        ];
67        
68        for score in scores {
69            if score < 0.0 || score > 1000.0 {
70                return Err(MinaCalcError::InvalidNoteData(format!("Score {} is out of reasonable bounds", score)));
71            }
72        }
73        Ok(())
74    }
75}
76
77impl From<Ssr> for SkillsetScores {
78    fn from(ssr: Ssr) -> Self {
79        SkillsetScores {
80            overall: ssr.overall,
81            stream: ssr.stream,
82            jumpstream: ssr.jumpstream,
83            handstream: ssr.handstream,
84            stamina: ssr.stamina,
85            jackspeed: ssr.jackspeed,
86            chordjack: ssr.chordjack,
87            technical: ssr.technical,
88        }
89    }
90}
91
92impl From<SkillsetScores> for Ssr {
93    fn from(scores: SkillsetScores) -> Self {
94        Ssr {
95            overall: scores.overall,
96            stream: scores.stream,
97            jumpstream: scores.jumpstream,
98            handstream: scores.handstream,
99            stamina: scores.stamina,
100            jackspeed: scores.jackspeed,
101            chordjack: scores.chordjack,
102            technical: scores.technical,
103        }
104    }
105}
106
107/// Represents MSD scores for all music rates (0.7x to 2.0x)
108#[derive(Debug, Clone)]
109pub struct AllRates {
110    pub msds: [SkillsetScores; 14],
111}
112
113impl AllRates {
114    /// Validates all MSD scores
115    pub fn validate(&self) -> MinaCalcResult<()> {
116        for (i, scores) in self.msds.iter().enumerate() {
117            scores.validate()
118                .map_err(|e| MinaCalcError::InvalidNoteData(format!("Rate {}: {}", (i as f32) / 10.0 + 0.7, e)))?;
119        }
120        Ok(())
121    }
122}
123
124impl From<AllRates> for super::MsdForAllRates {
125    fn from(msd: AllRates) -> Self {
126        let mut bindings_msd = super::MsdForAllRates {
127            msds: [Ssr {
128                overall: 0.0,
129                stream: 0.0,
130                jumpstream: 0.0,
131                handstream: 0.0,
132                stamina: 0.0,
133                jackspeed: 0.0,
134                chordjack: 0.0,
135                technical: 0.0,
136            }; 14],
137        };
138        
139        for (i, scores) in msd.msds.iter().enumerate() {
140            bindings_msd.msds[i] = (*scores).into();
141        }
142        
143        bindings_msd
144    }
145}
146
147impl From<BindingsMsdForAllRates> for AllRates {
148    fn from(bindings_msd: BindingsMsdForAllRates) -> Self {
149        let mut msds = [SkillsetScores {
150            overall: 0.0,
151            stream: 0.0,
152            jumpstream: 0.0,
153            handstream: 0.0,
154            stamina: 0.0,
155            jackspeed: 0.0,
156            chordjack: 0.0,
157            technical: 0.0,
158        }; 14];
159        
160        for (i, ssr) in bindings_msd.msds.iter().enumerate() {
161            msds[i] = (*ssr).into();
162        }
163        
164        AllRates { msds }
165    }
166}
167
168/// Main handler for difficulty calculations
169#[derive(Clone)]
170pub struct Calc {
171    handle: *mut CalcHandle,
172}
173
174impl Calc {
175    /// Creates a new calculator instance
176    pub fn new() -> MinaCalcResult<Self> {
177        let handle = unsafe { create_calc() };
178        if handle.is_null() {
179            return Err(MinaCalcError::CalculatorCreationFailed);
180        }
181        Ok(Calc { handle })
182    }
183    
184    /// Gets the calculator version
185    pub fn version() -> i32 {
186        unsafe { calc_version() }
187    }
188    
189    /// Calculates MSD scores for all music rates
190    pub fn calc_msd(&self, notes: &[Note]) -> MinaCalcResult<AllRates> {
191        if notes.is_empty() {
192            return Err(MinaCalcError::NoNotesProvided);
193        }
194        
195        // Validate all notes
196        for note in notes {
197            note.validate()?;
198        }
199        
200        // Convert notes to C format
201        let note_infos: Vec<NoteInfo> = notes.iter().map(|&note| note.into()).collect();
202        
203        let result = unsafe {
204            calc_msd(self.handle, note_infos.as_ptr(), note_infos.len())
205        };
206        
207        let msd: AllRates = result.into();
208        msd.validate()?;
209        Ok(msd)
210    }
211    
212    /// Calculates SSR scores for a specific music rate and score goal
213    pub fn calc_ssr(
214        &self,
215        notes: &[Note],
216        music_rate: f32,
217        score_goal: f32,
218    ) -> MinaCalcResult<SkillsetScores> {
219        if notes.is_empty() {
220            return Err(MinaCalcError::NoNotesProvided);
221        }
222        
223        if music_rate <= 0.0 {
224            return Err(MinaCalcError::InvalidMusicRate(music_rate));
225        }
226        
227        if score_goal <= 0.0 || score_goal > 100.0 {
228            return Err(MinaCalcError::InvalidScoreGoal(score_goal));
229        }
230        
231        // Validate all notes
232        for note in notes {
233            note.validate()?;
234        }
235        
236        // Convert notes to C format
237        let mut note_infos: Vec<NoteInfo> = notes.iter().map(|&note| note.into()).collect();
238        
239        let result = unsafe {
240            calc_ssr(self.handle, note_infos.as_mut_ptr(), note_infos.len(), music_rate, score_goal)
241        };
242        
243        let scores: SkillsetScores = result.into();
244        scores.validate()?;
245        Ok(scores)
246    }
247    
248    /// Validates the calculator handle is still valid
249    pub fn is_valid(&self) -> bool {
250        !self.handle.is_null()
251    }
252}
253
254impl Drop for Calc {
255    fn drop(&mut self) {
256        if !self.handle.is_null() {
257            unsafe {
258                destroy_calc(self.handle);
259            }
260        }
261    }
262}
263
264impl Default for Calc {
265    fn default() -> Self {
266        Self::new().expect("Failed to create default calculator")
267    }
268}
269
270// Unit tests
271#[cfg(test)]
272mod tests {
273    use super::*;
274    
275    #[test]
276    fn test_calc_version() {
277        let version = Calc::version();
278        assert!(version > 0);
279    }
280    
281    #[test]
282    fn test_calc_creation() {
283        let calc = Calc::new();
284        assert!(calc.is_ok());
285    }
286    
287    #[test]
288    fn test_note_conversion() {
289        let note = Note {
290            notes: 4,
291            row_time: 1.5,
292        };
293        
294        let note_info: NoteInfo = note.into();
295        let converted_note: Note = note_info.into();
296        
297        assert_eq!(note.notes, converted_note.notes);
298        assert_eq!(note.row_time, converted_note.row_time);
299    }
300    
301    #[test]
302    fn test_skillset_scores_conversion() {
303        let scores = SkillsetScores {
304            overall: 10.5,
305            stream: 8.2,
306            jumpstream: 12.1,
307            handstream: 9.3,
308            stamina: 7.8,
309            jackspeed: 11.4,
310            chordjack: 6.9,
311            technical: 13.2,
312        };
313        
314        let ssr: Ssr = scores.into();
315        let converted_scores: SkillsetScores = ssr.into();
316        
317        assert_eq!(scores.overall, converted_scores.overall);
318        assert_eq!(scores.stream, converted_scores.stream);
319        assert_eq!(scores.jumpstream, converted_scores.jumpstream);
320        assert_eq!(scores.handstream, converted_scores.handstream);
321        assert_eq!(scores.stamina, converted_scores.stamina);
322        assert_eq!(scores.jackspeed, converted_scores.jackspeed);
323        assert_eq!(scores.chordjack, converted_scores.chordjack);
324        assert_eq!(scores.technical, converted_scores.technical);
325    }
326}