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}