Skip to main content

rgc_chart/models/osu/
hitobjects.rs

1use std::str::FromStr;
2use std::ops::{Index, IndexMut};
3use std::slice::SliceIndex;
4use crate::models::osu::sound::HitSample;
5use crate::models::common::{self, Key};
6use crate::models::generic::sound::{self, SoundBank};
7use crate::osu::OsuMode;
8
9#[derive(Debug, Clone, PartialEq)]
10pub struct HitObject {
11    pub x: i32,
12    
13    pub y: i32,
14    
15    pub time: f32,
16    
17    pub object_type: u8,
18    
19    pub hit_sound: u8,
20    
21    pub object_params: Vec<String>,
22    
23    pub hit_sample: HitSample,
24}
25
26impl FromStr for HitObject {
27    type Err = String;
28    
29    fn from_str(s: &str) -> Result<Self, Self::Err> {
30        Self::from_str_with_mode(s, &OsuMode::Standard)
31    }
32}
33
34impl Default for HitObject {
35    fn default() -> Self {
36        HitObject { x: 256, y: 192, time: 0.0, object_type: 1, hit_sound: 0, object_params: Vec::new(), hit_sample: HitSample::default() }
37    }
38}
39
40impl HitObject {
41    pub fn from_str_with_mode(s: &str, mode: &OsuMode) -> Result<Self, String> {
42        let parts: Vec<&str> = s.split(',').collect();
43        
44        if parts.len() < 5 {
45            return Err(format!("Expected at least 5 comma-separated values, found {}", parts.len()));
46        }
47        
48        let x = parts[0].parse::<i32>()
49            .map_err(|_| format!("Invalid x value: {}", parts[0]))?;
50            
51        let y = parts[1].parse::<i32>()
52            .map_err(|_| format!("Invalid y value: {}", parts[1]))?;
53            
54        let time = parts[2].parse::<f32>()
55            .map_err(|_| format!("Invalid time value: {}", parts[2]))?;
56            
57        let object_type = parts[3].parse::<u8>()
58            .map_err(|_| format!("Invalid type value: {}", parts[3]))?;
59            
60        let hit_sound = parts[4].parse::<u8>()
61            .map_err(|_| format!("Invalid hitSound value: {}", parts[4]))?;
62        
63        let mut object_params = Vec::new();
64        let mut hit_sample = HitSample::default();
65        
66        if parts.len() > 5 {
67            let is_mania_hold = matches!(mode, OsuMode::Mania) && (object_type & 128) != 0;
68            
69            if is_mania_hold {
70                if parts.len() >= 6 {
71                    let sixth_param = parts[5];
72                    if sixth_param.contains(':') {
73                        let param_parts: Vec<&str> = sixth_param.split(':').collect();
74                        if !param_parts.is_empty() {
75                            object_params.push(param_parts[0].to_string());
76                            
77                            if param_parts.len() > 1 {
78                                let hit_sample_part = param_parts[1..].join(":");
79                                hit_sample = HitSample::from_str(&hit_sample_part)?;
80                            }
81                        }
82                    } else {
83                        object_params.push(sixth_param.to_string());
84                    }
85                }
86            } else {
87                let last_part = parts[parts.len() - 1];
88                if last_part.contains(':') {
89                    hit_sample = HitSample::from_str(last_part)?;
90                    object_params = parts[5..parts.len()-1].iter()
91                        .map(|s| s.to_string())
92                        .collect();
93                } else {
94                    object_params = parts[5..].iter()
95                        .map(|s| s.to_string())
96                        .collect();
97                }
98            }
99        }
100        
101        Ok(HitObject {
102            x,
103            y,
104            time,
105            object_type,
106            hit_sound,
107            object_params,
108            hit_sample,
109        })
110    }
111
112    pub fn new(x: i32, y: i32, time: f32, object_type: u8, hit_sound: u8) -> Self {
113        Self {
114            x,
115            y,
116            time,
117            object_type,
118            hit_sound,
119            object_params: Vec::new(),
120            hit_sample: HitSample::default(),
121        }
122    }
123    
124    pub fn is_hit_circle(&self) -> bool {
125        (self.object_type & 1) != 0
126    }
127    
128    pub fn is_slider(&self) -> bool {
129        (self.object_type & 2) != 0
130    }
131    
132    pub fn is_new_combo(&self) -> bool {
133        (self.object_type & 4) != 0
134    }
135    
136    pub fn is_spinner(&self) -> bool {
137        (self.object_type & 8) != 0
138    }
139
140    pub fn is_normal(&self) -> bool {
141        (self.object_type & 1) == 1
142    }
143    
144    pub fn is_hold(&self) -> bool {
145        (self.object_type & 128) != 0
146    }
147    
148    pub fn has_normal(&self) -> bool {
149        (self.hit_sound & 1) != 0
150    }
151    
152    pub fn has_whistle(&self) -> bool {
153        (self.hit_sound & 2) != 0
154    }
155    
156    pub fn has_finish(&self) -> bool {
157        (self.hit_sound & 4) != 0
158    }
159    
160    pub fn has_clap(&self) -> bool {
161        (self.hit_sound & 8) != 0
162    }
163
164    pub fn combo_skip(&self) -> u8 {
165        (self.object_type >> 4) & 7
166    }
167    
168    pub fn end_time(&self) -> Option<i32> {
169        if self.is_spinner() || self.is_hold() {
170            if !self.object_params.is_empty() {
171                return self.object_params[0].parse::<i32>().ok();
172            }
173        }
174        None
175    }
176    
177    pub fn slider_params(&self) -> Option<(String, Vec<String>, i32, f32)> {
178        if self.is_slider() && self.object_params.len() >= 3 {
179            let curve_info = &self.object_params[0];
180            let curve_parts: Vec<&str> = curve_info.split('|').collect();
181            
182            if curve_parts.is_empty() {
183                return None;
184            }
185            
186            let curve_type = curve_parts[0].to_string();
187            let curve_points: Vec<String> = curve_parts[1..].iter()
188                .map(|s| s.to_string())
189                .collect();
190            
191            let slides = self.object_params[1].parse::<i32>().ok()?;
192            let length = self.object_params[2].parse::<f32>().ok()?;
193            
194            Some((curve_type, curve_points, slides, length))
195        } else {
196            None
197        }
198    }
199    
200    pub fn slider_edge_sounds(&self) -> Vec<i32> {
201        if self.is_slider() && self.object_params.len() >= 4 {
202            self.object_params[3]
203                .split('|')
204                .filter_map(|s| s.parse::<i32>().ok())
205                .collect()
206        } else {
207            Vec::new()
208        }
209    }
210    
211    pub fn slider_edge_sets(&self) -> Vec<(i32, i32)> {
212        if self.is_slider() && self.object_params.len() >= 5 {
213            self.object_params[4]
214                .split('|')
215                .filter_map(|s| {
216                    let parts: Vec<&str> = s.split(':').collect();
217                    if parts.len() >= 2 {
218                        let normal = parts[0].parse::<i32>().ok()?;
219                        let addition = parts[1].parse::<i32>().ok()?;
220                        Some((normal, addition))
221                    } else {
222                        None
223                    }
224                })
225                .collect()
226        } else {
227            Vec::new()
228        }
229    }
230    
231    pub fn mania_column(&self, column_count: u8) -> u8 {
232        let column = (self.x * (column_count as i32) / 512).max(0).min((column_count as i32) - 1);
233        column as u8
234    }
235    
236    pub fn to_taiko(&self) -> common::TaikoHitobject {
237        use common::TaikoHitobject;
238        
239        let is_bonus = self.has_finish();
240        let is_kat = self.has_whistle() || self.has_clap();
241        let is_slider = self.is_slider();
242        let is_spinner = self.is_spinner();
243        
244        match (is_slider, is_spinner, is_bonus, is_kat) {
245            (true, _, true, _) => TaikoHitobject::bonus_drum_roll(self.end_time().unwrap_or(0)),
246            (true, _, false, _) => TaikoHitobject::drum_roll(self.end_time().unwrap_or(0)),
247            (_, true, _, _) => TaikoHitobject::balloon(self.end_time().unwrap_or(0)),
248            (false, false, true, false) => TaikoHitobject::bonus_don(),
249            (false, false, true, true) => TaikoHitobject::bonus_kat(),
250            (false, false, false, false) => TaikoHitobject::don(),
251            (false, false, false, true) => TaikoHitobject::kat(),
252        }
253    }
254    
255    pub fn to_catch(&self) -> common::CatchHitobject {
256        use common::CatchHitobject;
257
258        if self.is_hit_circle() {
259            CatchHitobject::fruit(self.x)
260        } else if self.is_slider() {
261            CatchHitobject::juice(self.x)
262        } else if self.is_spinner() {
263            CatchHitobject::banana(self.x, self.end_time().unwrap_or(0))
264        } else {
265            CatchHitobject::empty()
266        }
267    }
268    
269    pub fn to_mania(&self, column_count: u8) -> (Key, u8) {
270        let column = self.mania_column(column_count);
271        let object_type = if self.is_normal() {
272            Key::normal()
273        } else if self.is_hold() {
274            Key::slider_start(Some(self.end_time().unwrap_or(0)))
275        } else {
276            Key::empty()
277        };
278        
279        (object_type, column)
280    }
281
282    pub fn get_generic_keysound(&self, soundbank: &mut SoundBank) -> sound::KeySound {
283        let hitsound_type = match self.hit_sound {
284            3 => sound::HitSoundType::Clap,
285            1 => sound::HitSoundType::Whistle,
286            2 => sound::HitSoundType::Finish,
287            _ => sound::HitSoundType::Normal,
288        };
289
290        if self.hit_sample.filename.trim().is_empty() {
291            sound::KeySound::of_type(100, hitsound_type)
292        } else {
293            let keysound_idx = soundbank.add_sound_sample(self.hit_sample.filename.clone());
294            sound::KeySound::with_custom(self.hit_sample.volume.clamp(0, 100), keysound_idx, Some(hitsound_type))
295        }
296    }
297    
298    pub fn to_osu_format(&self) -> String {
299        let mut parts = vec![
300            self.x.to_string(),
301            self.y.to_string(),
302            self.time.to_string(),
303            self.object_type.to_string(),
304            self.hit_sound.to_string(),
305        ];
306        
307        parts.extend(self.object_params.iter().cloned());
308        
309        let hit_sample_str = self.hit_sample.to_osu_format();
310        parts.push(hit_sample_str);
311        
312        parts.join(",")
313    }
314}
315
316impl HitObject {
317    pub fn to_osu_format_with_mode(&self, mode: &OsuMode, soundbank: Option<&mut SoundBank>) -> String {
318        match mode {
319            OsuMode::Standard => self.to_osu_format_standard(),
320            OsuMode::Taiko => self.to_osu_format_taiko(),
321            OsuMode::Catch => self.to_osu_format_catch(),
322            OsuMode::Mania => {
323                if let Some(sb) = soundbank {
324                    self.to_osu_format_mania(sb)
325                } else {
326                    self.to_osu_format_mania_no_soundbank()
327                }
328            },
329            OsuMode::Unknown => self.to_osu_format(),
330        }
331    }
332
333    fn to_osu_format_standard(&self) -> String {
334        let mut parts = vec![
335            self.x.to_string(),
336            self.y.to_string(),
337            self.time.to_string(),
338            self.object_type.to_string(),
339            self.hit_sound.to_string(),
340        ];
341        
342        if self.is_slider() {
343            parts.extend(self.object_params.iter().cloned());
344        } else if self.is_spinner() {
345            if let Some(end_time) = self.end_time() {
346                parts.push(end_time.to_string());
347            }
348        }
349        
350        let hit_sample_str = self.hit_sample.to_osu_format();
351        parts.push(hit_sample_str);
352        
353        parts.join(",")
354    }
355
356    fn to_osu_format_taiko(&self) -> String {
357        let taiko_obj = self.to_taiko();
358        let mut parts = vec![
359            "256".to_string(),
360            "192".to_string(),
361            self.time.to_string(),
362        ];
363
364        match taiko_obj.note_type {
365            common::TaikoHitobjectType::Don => {
366                parts.push("1".to_string());
367                parts.push("0".to_string());
368            },
369            common::TaikoHitobjectType::Kat => {
370                parts.push("1".to_string());
371                parts.push("2".to_string());
372            },
373            common::TaikoHitobjectType::BonusDon => {
374                parts.push("1".to_string());
375                parts.push("4".to_string());
376            },
377            common::TaikoHitobjectType::BonusKat => {
378                parts.push("1".to_string());
379                parts.push("6".to_string());
380            },
381            common::TaikoHitobjectType::DrumRoll => {
382                parts.push("2".to_string());
383                parts.push("0".to_string());
384                let end_time = taiko_obj.end_time.unwrap_or(self.time as i32);
385                parts.push(format!("L|256:192,1,{}", end_time - self.time as i32));
386            },
387            common::TaikoHitobjectType::BonusDrumRoll => {
388                parts.push("2".to_string());
389                parts.push("4".to_string());
390                let end_time = taiko_obj.end_time.unwrap_or(self.time as i32);
391                parts.push(format!("L|256:192,1,{}", end_time - self.time as i32));
392            },
393            common::TaikoHitobjectType::Balloon => {
394                parts.push("8".to_string());
395                parts.push("0".to_string());
396                let end_time = taiko_obj.end_time.unwrap_or(self.time as i32);
397                parts.push(end_time.to_string());
398            },
399            common::TaikoHitobjectType::Empty | common::TaikoHitobjectType::Unknown => {
400                return String::new();
401            }
402        }
403
404        let hit_sample_str = self.hit_sample.to_osu_format();
405        parts.push(hit_sample_str);
406
407        return parts.join(",");
408    }
409
410    fn to_osu_format_catch(&self) -> String {
411        let catch_obj = self.to_catch();
412        let mut parts = vec![
413            self.x.to_string(),
414            "192".to_string(),
415            self.time.to_string(),
416        ];
417
418        match catch_obj.object_type {
419            common::CatchHitobjectType::Fruit | common::CatchHitobjectType::Hyperfruit => {
420                parts.push("1".to_string());
421                parts.push(self.hit_sound.to_string());
422            },
423            common::CatchHitobjectType::Juice => {
424                parts.push("2".to_string());
425                parts.push(self.hit_sound.to_string());
426                if !self.object_params.is_empty() {
427                    parts.extend(self.object_params.iter().cloned());
428                }
429            },
430            common::CatchHitobjectType::Banana => {
431                parts.push("8".to_string());
432                parts.push(self.hit_sound.to_string());
433                parts.push(catch_obj.end_time().unwrap_or(0).to_string());
434            },
435            common::CatchHitobjectType::Empty | common::CatchHitobjectType::Unknown => {
436                return String::new();
437            },
438        }
439
440        let hit_sample_str = self.hit_sample.to_osu_format();
441        parts.push(hit_sample_str);
442
443        parts.join(",")
444    }
445
446    fn to_osu_format_mania(&self, soundbank: &mut SoundBank) -> String {
447        let keysound = self.get_generic_keysound(soundbank);
448        let mut parts = vec![
449            self.x.to_string(),
450            "192".to_string(),
451            self.time.to_string(),
452            self.object_type.to_string(),
453            self.hit_sound.to_string(),
454        ];
455
456        let custom_sample = if keysound.has_custom {
457            soundbank.get_sound_sample(keysound.sample.unwrap_or(0))
458                .unwrap_or_default()
459        } else {
460            String::new()
461        };
462
463        let volume = if keysound.volume >= 100 { 0 } else { keysound.volume };
464        let hit_sample = HitSample {
465            normal_set: 0,
466            addition_set: 0,
467            index: 0,
468            volume,
469            filename: custom_sample,
470        };
471
472        if self.is_hold() {
473            if let Some(end_time) = self.end_time() {
474                let hit_sample_str = hit_sample.to_osu_format();
475                parts.push(format!("{}:{}", end_time, hit_sample_str));
476            }
477        } else {
478            let hit_sample_str = hit_sample.to_osu_format();
479            parts.push(hit_sample_str);
480        }
481
482        parts.join(",")
483    }
484
485    fn to_osu_format_mania_no_soundbank(&self) -> String {
486        let mut parts = vec![
487            self.x.to_string(),
488            "192".to_string(),
489            self.time.to_string(),
490            self.object_type.to_string(),
491            self.hit_sound.to_string(),
492        ];
493
494        if self.is_hold() {
495            if let Some(end_time) = self.end_time() {
496                let hit_sample_str = self.hit_sample.to_osu_format();
497                parts.push(format!("{}:{}", end_time, hit_sample_str));
498            }
499        } else {
500            let hit_sample_str = self.hit_sample.to_osu_format();
501            parts.push(hit_sample_str);
502        }
503
504        parts.join(",")
505    }
506}
507
508#[derive(Debug, Clone, PartialEq)]
509pub struct HitObjects {
510    hit_objects: Vec<HitObject>,
511}
512
513impl Default for HitObjects {
514    fn default() -> Self {
515        Self {
516            hit_objects: Vec::new(),
517        }
518    }
519}
520
521impl FromStr for HitObjects {
522    type Err = String;
523    
524    fn from_str(s: &str) -> Result<Self, Self::Err> {
525        Self::from_str_with_mode(s, &OsuMode::Standard)
526    }
527}
528
529impl HitObjects {
530    pub fn from_str_with_mode(s: &str, mode: &OsuMode) -> Result<Self, String> {
531        let mut hit_objects = Vec::new();
532        
533        for line in s.lines() {
534            let line = line.trim();
535            if line.is_empty() || line.starts_with("//") {
536                continue;
537            }
538            
539            let hit_object = HitObject::from_str_with_mode(line, mode)?;
540            hit_objects.push(hit_object);
541        }
542        
543        Ok(HitObjects { hit_objects })
544    }
545
546    pub fn new() -> Self {
547        Self::default()
548    }
549    
550    pub fn add_hit_object(&mut self, hit_object: HitObject) {
551        self.hit_objects.push(hit_object);
552    }
553    
554    pub fn count(&self) -> usize {
555        self.hit_objects.len()
556    }
557    
558    pub fn hit_circle_count(&self) -> usize {
559        self.hit_objects.iter().filter(|obj| obj.is_hit_circle()).count()
560    }
561    
562    pub fn slider_count(&self) -> usize {
563        self.hit_objects.iter().filter(|obj| obj.is_slider()).count()
564    }
565
566    pub fn spinner_count(&self) -> usize {
567        self.hit_objects.iter().filter(|obj| obj.is_spinner()).count()
568    }
569    
570    pub fn start_time(&self) -> Option<f32> {
571        self.hit_objects.first().map(|obj| obj.time)
572    }
573    
574    pub fn end_time(&self) -> Option<i32> {
575        self.hit_objects.last().and_then(|obj| {
576            obj.end_time().or(Some(obj.time as i32))
577        })
578    }
579    
580    pub fn length(&self) -> Option<f32> {
581        if let (Some(start), Some(end)) = (self.start_time(), self.end_time()) {
582            Some(end as f32 - start)
583        } else {
584            None
585        }
586    }
587    
588    pub fn objects_in_time_range(&self, start_time: f32, end_time: f32) -> Vec<&HitObject> {
589        self.hit_objects
590            .iter()
591            .filter(|obj| obj.time >= start_time && obj.time <= (end_time as f32))
592            .collect()
593    }
594
595    pub fn sort_by_time(&mut self) {
596        self.hit_objects.sort_by(|a, b| {
597            match (a.time.is_nan(), b.time.is_nan()) {
598                (true, true) => std::cmp::Ordering::Equal,
599                (true, false) => std::cmp::Ordering::Greater,
600                (false, true) => std::cmp::Ordering::Less,
601                (false, false) => a.time.partial_cmp(&b.time).unwrap(),
602            }
603        });
604    }
605    
606    pub fn to_osu_format(&self) -> String {
607        self.hit_objects
608            .iter()
609            .map(|obj| obj.to_osu_format() )
610            .collect::<Vec<_>>()
611            .join("\n")
612    }
613}
614
615impl HitObjects {
616    pub fn to_osu_format_taiko(&self) -> String {
617        self.hit_objects
618            .iter()
619            .map(|obj| obj.to_osu_format_taiko() )
620            .collect::<Vec<_>>()
621            .join("\n")
622    }
623
624    pub fn to_osu_format_catch(&self) -> String {
625        self.hit_objects
626            .iter()
627            .map(|obj| obj.to_osu_format_catch() )
628            .collect::<Vec<_>>()
629            .join("\n")
630    }
631
632    pub fn to_osu_format_mania(&self, soundbank: &mut SoundBank) -> String {
633        self.hit_objects
634            .iter()
635            .map(|obj| obj.to_osu_format_mania(soundbank) )
636            .collect::<Vec<_>>()
637            .join("\n")
638    }
639
640    pub fn to_osu_format_mania_no_soundbank(&self) -> String {
641        self.hit_objects
642            .iter()
643            .map(|obj| obj.to_osu_format_mania_no_soundbank() )
644            .collect::<Vec<_>>()
645            .join("\n")
646    }
647
648    pub fn to_osu_format_standard(&self) -> String {
649        self.to_osu_format()
650    }
651}
652
653impl<I> Index<I> for HitObjects
654where
655    I: SliceIndex<[HitObject]>,
656{
657    type Output = I::Output;
658
659    fn index(&self, index: I) -> &Self::Output {
660        &self.hit_objects[index]
661    }
662}
663
664impl<I> IndexMut<I> for HitObjects
665where
666    I: SliceIndex<[HitObject], Output = [HitObject]>,
667{
668    fn index_mut(&mut self, index: I) -> &mut Self::Output {
669        &mut self.hit_objects[index]
670    }
671}
672
673impl IntoIterator for HitObjects {
674    type Item = HitObject;
675    type IntoIter = std::vec::IntoIter<HitObject>;
676
677    fn into_iter(self) -> Self::IntoIter {
678        self.hit_objects.into_iter()
679    }
680}
681
682impl<'a> IntoIterator for &'a HitObjects {
683    type Item = &'a HitObject;
684    type IntoIter = std::slice::Iter<'a, HitObject>;
685
686    fn into_iter(self) -> Self::IntoIter {
687        self.hit_objects.iter()
688    }
689}
690
691impl<'a> IntoIterator for &'a mut HitObjects {
692    type Item = &'a mut HitObject;
693    type IntoIter = std::slice::IterMut<'a, HitObject>;
694
695    fn into_iter(self) -> Self::IntoIter {
696        self.hit_objects.iter_mut()
697    }
698}
699
700impl HitObjects {
701    pub fn iter(&'_ self) -> std::slice::Iter<'_, HitObject> {
702        self.hit_objects.iter()
703    }
704    
705    pub fn iter_mut(&'_ mut self) -> std::slice::IterMut<'_, HitObject> {
706        self.hit_objects.iter_mut()
707    }
708    
709    pub fn get(&self, index: usize) -> Option<&HitObject> {
710        self.hit_objects.get(index)
711    }
712    
713    pub fn get_mut(&mut self, index: usize) -> Option<&mut HitObject> {
714        self.hit_objects.get_mut(index)
715    }
716    
717    pub fn get_slice<I>(&self, index: I) -> Option<&I::Output>
718    where
719        I: SliceIndex<[HitObject]>,
720    {
721        self.hit_objects.get(index)
722    }
723    
724    pub fn get_slice_mut<I>(&mut self, index: I) -> Option<&mut I::Output>
725    where
726        I: SliceIndex<[HitObject], Output = [HitObject]>,
727    {
728        self.hit_objects.get_mut(index)
729    }
730    
731    pub fn is_empty(&self) -> bool {
732        self.hit_objects.is_empty()
733    }
734    
735    pub fn first(&self) -> Option<&HitObject> {
736        self.hit_objects.first()
737    }
738    
739    pub fn last(&self) -> Option<&HitObject> {
740        self.hit_objects.last()
741    }
742    
743    pub fn first_mut(&mut self) -> Option<&mut HitObject> {
744        self.hit_objects.first_mut()
745    }
746    
747    pub fn last_mut(&mut self) -> Option<&mut HitObject> {
748        self.hit_objects.last_mut()
749    }
750}