rosu_map/
encode.rs

1use std::{
2    fs::File,
3    io::{BufWriter, Error as IoError, ErrorKind, Result as IoResult, Write},
4    path::Path,
5    slice,
6};
7
8use crate::{
9    beatmap::Beatmap,
10    section::{
11        difficulty::DifficultyKey,
12        editor::EditorKey,
13        events::EventType,
14        general::{GameMode, GeneralKey},
15        hit_objects::{
16            hit_samples::{HitSampleInfo, HitSampleInfoName, HitSoundType},
17            CurveBuffers, HitObjectKind, HitObjectSlider, HitObjectType, PathType, SliderEvent,
18            SliderEventType, SliderEventsIter, SplineType, BASE_SCORING_DIST,
19        },
20        metadata::MetadataKey,
21        timing_points::{
22            ControlPoints, DifficultyPoint, EffectFlags, EffectPoint, SamplePoint, TimingPoint,
23        },
24    },
25    util::Pos,
26};
27
28impl Beatmap {
29    /// Encode a [`Beatmap`] into content of a `.osu` file and store it at the
30    /// given path.
31    ///
32    /// # Example
33    ///
34    /// ```no_run
35    /// # use rosu_map::Beatmap;
36    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
37    /// let mut map: Beatmap = /* ... */
38    /// # Beatmap::default();
39    /// let path = "./maps/123456.osu";
40    /// map.encode_to_path(path)?;
41    /// # Ok(()) }
42    /// ```
43    pub fn encode_to_path<P: AsRef<Path>>(&mut self, path: P) -> IoResult<()> {
44        let file = File::create(path)?;
45        let writer = BufWriter::new(file);
46
47        self.encode(writer)
48    }
49
50    /// Encode a [`Beatmap`] into content of a `.osu` file and store it into a
51    /// [`String`].
52    ///
53    /// # Example
54    ///
55    /// ```
56    /// # use rosu_map::Beatmap;
57    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
58    /// let mut map: Beatmap = /* ... */
59    /// # Beatmap::default();
60    /// let content: String = map.encode_to_string()?;
61    /// # Ok(()) }
62    /// ```
63    pub fn encode_to_string(&mut self) -> IoResult<String> {
64        let mut writer = Vec::with_capacity(4096);
65        self.encode(&mut writer)?;
66
67        String::from_utf8(writer).map_err(|e| IoError::new(ErrorKind::Other, e))
68    }
69
70    /// Encode a [`Beatmap`] into content of a `.osu` file.
71    ///
72    /// # Example
73    ///
74    /// In case of writing directly to a file, it is recommended to pass the
75    /// file wrapped in a [`BufWriter`] or just use
76    /// [`encode_to_path`].
77    ///
78    /// ```no_run
79    /// use std::{fs::File, io::BufWriter};
80    /// # use rosu_map::Beatmap;
81    ///
82    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
83    /// let mut map: Beatmap = /* ... */
84    /// # Beatmap::default();
85    /// let path = "./maps/123456.osu";
86    /// let file = File::create(path)?;
87    /// let writer = BufWriter::new(file);
88    ///
89    /// map.encode(writer)?;
90    /// # Ok(()) }
91    /// ```
92    ///
93    /// Encoding into a [`Vec<u8>`] can be done by passing a mutable reference.
94    ///
95    /// ```
96    /// # use rosu_map::Beatmap;
97    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
98    /// let mut map: Beatmap = /* ... */
99    /// # Beatmap::default();
100    /// let mut bytes: Vec<u8> = Vec::with_capacity(2048);
101    ///
102    /// map.encode(&mut bytes)?;
103    ///
104    /// // Or just use `Beatmap::encode_to_string`
105    /// let content = String::from_utf8(bytes)?;
106    ///
107    /// # Ok(()) }
108    /// ```
109    ///
110    /// [`encode_to_path`]: Beatmap::encode_to_path
111    pub fn encode<W: Write>(&mut self, mut writer: W) -> IoResult<()> {
112        writeln!(writer, "osu file format v{}", self.format_version)?;
113
114        writer.write_all(b"\n")?;
115        self.encode_general(&mut writer)?;
116
117        writer.write_all(b"\n")?;
118        self.encode_editor(&mut writer)?;
119
120        writer.write_all(b"\n")?;
121        self.encode_metadata(&mut writer)?;
122
123        writer.write_all(b"\n")?;
124        self.encode_difficulty(&mut writer)?;
125
126        writer.write_all(b"\n")?;
127        self.encode_events(&mut writer)?;
128
129        writer.write_all(b"\n")?;
130        self.encode_timing_points(&mut writer)?;
131
132        writer.write_all(b"\n")?;
133        self.encode_colors(&mut writer)?;
134
135        writer.write_all(b"\n")?;
136        self.encode_hit_objects(&mut writer)?;
137
138        writer.flush()
139    }
140
141    fn encode_general<W: Write>(&self, writer: &mut W) -> IoResult<()> {
142        writeln!(
143            writer,
144            "[General]
145{}: {}
146{}: {}
147{}: {}
148{}: {}",
149            GeneralKey::AudioFilename,
150            self.audio_file,
151            GeneralKey::AudioLeadIn,
152            self.audio_lead_in,
153            GeneralKey::PreviewTime,
154            self.preview_time,
155            GeneralKey::Countdown,
156            self.countdown as i32
157        )?;
158
159        let sample_set = self
160            .control_points
161            .sample_points
162            .first()
163            .map_or(SamplePoint::DEFAULT_SAMPLE_BANK, |sample_point| {
164                sample_point.sample_bank
165            });
166
167        writeln!(
168            writer,
169            "{}: {}
170{}: {}
171{}: {}
172{}: {}",
173            GeneralKey::SampleSet,
174            sample_set as i32,
175            GeneralKey::StackLeniency,
176            self.stack_leniency,
177            GeneralKey::Mode,
178            self.mode as i32,
179            GeneralKey::LetterboxInBreaks,
180            i32::from(self.letterbox_in_breaks),
181        )?;
182
183        if self.epilepsy_warning {
184            writeln!(writer, "{}: {}", GeneralKey::EpilepsyWarning, 1)?;
185        }
186
187        if self.countdown_offset > 0 {
188            writeln!(
189                writer,
190                "{}: {}",
191                GeneralKey::CountdownOffset,
192                self.countdown_offset
193            )?;
194        }
195
196        if self.mode == GameMode::Mania {
197            writeln!(
198                writer,
199                "{}: {}",
200                GeneralKey::SpecialStyle,
201                i32::from(self.special_style)
202            )?;
203        }
204
205        writeln!(
206            writer,
207            "{}: {}",
208            GeneralKey::WidescreenStoryboard,
209            i32::from(self.widescreen_storyboard)
210        )?;
211
212        if self.samples_match_playback_rate {
213            writeln!(writer, "{}: {}", GeneralKey::SamplesMatchPlaybackRate, 1)?;
214        }
215
216        Ok(())
217    }
218
219    fn encode_editor<W: Write>(&self, writer: &mut W) -> IoResult<()> {
220        writer.write_all(b"[Editor]\n")?;
221
222        let mut bookmarks = self.bookmarks.iter();
223
224        if let Some(bookmark) = bookmarks.next() {
225            write!(writer, "Bookmarks: {bookmark}")?;
226
227            for bookmark in bookmarks {
228                write!(writer, ",{bookmark}")?;
229            }
230
231            writer.write_all(b"\n")?;
232        }
233
234        writeln!(
235            writer,
236            "{}: {}
237{}: {}
238{}: {}
239{}: {}",
240            EditorKey::DistanceSpacing,
241            self.distance_spacing,
242            EditorKey::BeatDivisor,
243            self.beat_divisor,
244            EditorKey::GridSize,
245            self.grid_size,
246            EditorKey::TimelineZoom,
247            self.timeline_zoom
248        )
249    }
250
251    fn encode_metadata<W: Write>(&self, writer: &mut W) -> IoResult<()> {
252        writer.write_all(b"[Metadata]\n")?;
253
254        writeln!(writer, "{}: {}", MetadataKey::Title, &self.title)?;
255
256        if !self.title_unicode.is_empty() {
257            writeln!(
258                writer,
259                "{}: {}",
260                MetadataKey::TitleUnicode,
261                &self.title_unicode
262            )?;
263        }
264
265        writeln!(writer, "{}: {}", MetadataKey::Artist, self.artist)?;
266
267        if !self.artist_unicode.is_empty() {
268            writeln!(
269                writer,
270                "{}: {}",
271                MetadataKey::ArtistUnicode,
272                &self.artist_unicode
273            )?;
274        }
275
276        writeln!(writer, "{}: {}", MetadataKey::Creator, &self.creator)?;
277        writeln!(writer, "{}: {}", MetadataKey::Version, &self.version)?;
278
279        if !self.source.is_empty() {
280            writeln!(writer, "{}: {}", MetadataKey::Source, &self.source)?;
281        }
282
283        if !self.tags.is_empty() {
284            writeln!(writer, "{}: {}", MetadataKey::Tags, &self.tags)?;
285        }
286
287        Ok(())
288    }
289
290    fn encode_difficulty<W: Write>(&self, writer: &mut W) -> IoResult<()> {
291        writeln!(
292            writer,
293            "[Difficulty]
294{}: {}
295{}: {}
296{}: {}
297{}: {}
298{}: {}
299{}: {}",
300            DifficultyKey::HPDrainRate,
301            self.hp_drain_rate,
302            DifficultyKey::CircleSize,
303            self.circle_size,
304            DifficultyKey::OverallDifficulty,
305            self.overall_difficulty,
306            DifficultyKey::ApproachRate,
307            self.approach_rate,
308            DifficultyKey::SliderMultiplier,
309            self.slider_multiplier,
310            DifficultyKey::SliderTickRate,
311            self.slider_tick_rate
312        )
313    }
314
315    fn encode_events<W: Write>(&self, writer: &mut W) -> IoResult<()> {
316        writer.write_all(b"[Events]\n")?;
317
318        if !self.background_file.is_empty() {
319            writeln!(
320                writer,
321                "{},0,\"{}\",0,0",
322                EventType::Background as i32,
323                self.background_file
324            )?;
325        }
326
327        for b in self.breaks.iter() {
328            writeln!(
329                writer,
330                "{},{},{}",
331                EventType::Break as i32,
332                b.start_time,
333                b.end_time
334            )?;
335        }
336
337        Ok(())
338    }
339
340    fn encode_timing_points<W: Write>(&mut self, writer: &mut W) -> IoResult<()> {
341        fn output_control_point_at<W: Write>(
342            writer: &mut W,
343            props: &ControlPointProperties,
344            is_timing: bool,
345        ) -> IoResult<()> {
346            writeln!(
347                writer,
348                "{},{},{},{},{},{}",
349                props.timing_signature,
350                props.sample_bank,
351                props.custom_sample_bank,
352                props.sample_volume,
353                if is_timing { "1" } else { "0" },
354                props.effect_flags
355            )
356        }
357
358        let mut control_points = self.control_points.clone();
359        collect_samples(self, &mut control_points);
360
361        let mut groups: Vec<_> = control_points
362            .timing_points
363            .iter()
364            .map(ControlPointGroup::from)
365            .collect();
366
367        groups.sort_unstable_by(|a, b| a.time.total_cmp(&b.time));
368
369        let times = control_points
370            .difficulty_points
371            .iter()
372            .map(|point| point.time)
373            .chain(control_points.effect_points.iter().map(|point| point.time))
374            .chain(control_points.sample_points.iter().map(|point| point.time));
375
376        for time in times {
377            if let Err(i) = groups.binary_search_by(|probe| probe.time.total_cmp(&time)) {
378                groups.insert(i, ControlPointGroup::new(time));
379            }
380        }
381
382        writer.write_all(b"[TimingPoints]\n")?;
383        let mut last_props = ControlPointProperties::default();
384
385        for group in groups {
386            let props = ControlPointProperties::new(
387                group.time,
388                &control_points,
389                &last_props,
390                group.timing.is_some(),
391            );
392
393            if let Some(timing) = group.timing {
394                write!(writer, "{},{},", timing.time, timing.beat_len)?;
395                output_control_point_at(writer, &props, true)?;
396                last_props = ControlPointProperties {
397                    slider_velocity: 1.0,
398                    ..props
399                };
400            }
401
402            if props.is_redundant(&last_props) {
403                continue;
404            }
405
406            write!(writer, "{},{},", group.time, -100.0 / props.slider_velocity)?;
407            output_control_point_at(writer, &props, false)?;
408            last_props = props;
409        }
410
411        Ok(())
412    }
413
414    fn encode_colors<W: Write>(&self, writer: &mut W) -> IoResult<()> {
415        writer.write_all(b"[Colours]\n")?;
416
417        for (color, i) in self.custom_combo_colors.iter().zip(1..) {
418            writeln!(
419                writer,
420                "Combo{i}: {},{},{},{}",
421                color.red(),
422                color.green(),
423                color.blue(),
424                color.alpha(),
425            )?;
426        }
427
428        for custom in self.custom_colors.iter() {
429            writeln!(
430                writer,
431                "{}: {},{},{},{}",
432                custom.name,
433                custom.color.red(),
434                custom.color.green(),
435                custom.color.blue(),
436                custom.color.alpha(),
437            )?;
438        }
439
440        Ok(())
441    }
442
443    fn encode_hit_objects<W: Write>(&mut self, writer: &mut W) -> IoResult<()> {
444        writer.write_all(b"[HitObjects]\n")?;
445        let mut bufs = CurveBuffers::default();
446
447        for hit_object in self.hit_objects.iter_mut() {
448            let pos = match hit_object.kind {
449                HitObjectKind::Circle(ref h) => h.pos,
450                HitObjectKind::Slider(ref h) => h.pos,
451                HitObjectKind::Spinner(ref h) => h.pos,
452                HitObjectKind::Hold(ref h) => Pos::new(h.pos_x, 192.0),
453            };
454
455            write!(
456                writer,
457                "{x},{y},{start_time},{kind},{sound},",
458                x = pos.x,
459                y = pos.y,
460                start_time = hit_object.start_time,
461                kind = i32::from(HitObjectType::from(&*hit_object)),
462                sound = u8::from(HitSoundType::from(hit_object.samples.as_slice())),
463            )?;
464
465            match hit_object.kind {
466                HitObjectKind::Circle(_) => {}
467                HitObjectKind::Slider(ref mut h) => {
468                    add_path_data(writer, h, pos, self.mode, &mut bufs)?;
469                }
470                HitObjectKind::Spinner(ref h) => {
471                    write!(writer, "{},", hit_object.start_time + h.duration)?;
472                }
473                HitObjectKind::Hold(ref h) => {
474                    write!(writer, "{}:", hit_object.start_time + h.duration)?;
475                }
476            }
477
478            get_sample_bank(writer, &hit_object.samples, false, self.mode)?;
479
480            writer.write_all(b"\n")?;
481        }
482
483        Ok(())
484    }
485}
486
487#[derive(Clone, Default)]
488struct ControlPointProperties {
489    slider_velocity: f64,
490    timing_signature: u32,
491    sample_bank: i32,
492    custom_sample_bank: i32,
493    sample_volume: i32,
494    effect_flags: i32,
495}
496
497impl ControlPointProperties {
498    fn new(
499        time: f64,
500        control_points: &ControlPoints,
501        last_props: &Self,
502        update_sample_bank: bool,
503    ) -> Self {
504        let timing = control_points.timing_point_at(time);
505        let difficulty = control_points.difficulty_point_at(time);
506        let sample = control_points
507            .sample_point_at(time)
508            .map_or_else(SamplePoint::default, SamplePoint::clone);
509        let effect = control_points.effect_point_at(time);
510
511        let mut tmp_hit_sample = HitSampleInfo::new(HitSampleInfo::HIT_NORMAL, None, 0, 0);
512        sample.apply(&mut tmp_hit_sample);
513
514        let mut effect_flags = EffectFlags::NONE;
515
516        if effect.map_or(EffectPoint::DEFAULT_KIAI, |point| point.kiai) {
517            effect_flags |= EffectFlags::KIAI;
518        }
519
520        if timing.map_or(TimingPoint::DEFAULT_OMIT_FIRST_BAR_LINE, |point| {
521            point.omit_first_bar_line
522        }) {
523            effect_flags |= EffectFlags::OMIT_FIRST_BAR_LINE;
524        }
525
526        Self {
527            slider_velocity: difficulty.map_or(DifficultyPoint::DEFAULT_SLIDER_VELOCITY, |point| {
528                point.slider_velocity
529            }),
530            timing_signature: timing
531                .map_or(TimingPoint::DEFAULT_TIME_SIGNATURE, |point| {
532                    point.time_signature
533                })
534                .numerator
535                .get(),
536            sample_bank: if update_sample_bank {
537                tmp_hit_sample.bank as i32
538            } else {
539                last_props.sample_bank
540            },
541            custom_sample_bank: if tmp_hit_sample.custom_sample_bank >= 0 {
542                tmp_hit_sample.custom_sample_bank
543            } else {
544                last_props.custom_sample_bank
545            },
546            sample_volume: tmp_hit_sample.volume,
547            effect_flags,
548        }
549    }
550
551    fn is_redundant(&self, other: &Self) -> bool {
552        (self.slider_velocity - other.slider_velocity).abs() < f64::EPSILON
553            && self.timing_signature == other.timing_signature
554            && self.sample_bank == other.sample_bank
555            && self.custom_sample_bank == other.custom_sample_bank
556            && self.sample_volume == other.sample_volume
557            && self.effect_flags == other.effect_flags
558    }
559}
560
561struct ControlPointGroup<'a> {
562    time: f64,
563    timing: Option<&'a TimingPoint>,
564}
565
566impl<'a> ControlPointGroup<'a> {
567    const fn new(time: f64) -> Self {
568        Self { time, timing: None }
569    }
570}
571
572impl<'a> From<&'a TimingPoint> for ControlPointGroup<'a> {
573    fn from(point: &'a TimingPoint) -> Self {
574        Self {
575            time: point.time,
576            timing: Some(point),
577        }
578    }
579}
580
581fn add_path_data<W: Write>(
582    writer: &mut W,
583    slider: &mut HitObjectSlider,
584    pos: Pos,
585    mode: GameMode,
586    bufs: &mut CurveBuffers,
587) -> IoResult<()> {
588    let mut last_type = None;
589    let control_points = slider.path.control_points();
590
591    let separator = |i: usize| {
592        if i == control_points.len() - 1 {
593            b','
594        } else {
595            b'|'
596        }
597    };
598
599    for i in 0..control_points.len() {
600        let point = control_points[i];
601
602        if let Some(path_type) = point.path_type {
603            let mut needs_explicit_segment =
604                point.path_type != last_type || point.path_type == Some(PathType::PERFECT_CURVE);
605
606            if i > 1 {
607                let p1 = pos + control_points[i - 1].pos;
608                let p2 = pos + control_points[i - 2].pos;
609
610                if p1.x as i32 == p2.x as i32 && p1.y as i32 == p2.y as i32 {
611                    needs_explicit_segment = true;
612                }
613            }
614
615            if needs_explicit_segment {
616                match path_type.kind {
617                    SplineType::BSpline => {
618                        if let Some(degree) = path_type.degree {
619                            write!(writer, "B{degree}")?;
620                        } else {
621                            writer.write_all(b"B")?;
622                        }
623                    }
624                    SplineType::Catmull => writer.write_all(b"C")?,
625                    SplineType::PerfectCurve => writer.write_all(b"P")?,
626                    SplineType::Linear => writer.write_all(b"L")?,
627                }
628
629                // Beatmaps such as /b/1027526 have no control points so the
630                // path type needs to be followed by `,` instead of `|`.
631                writer.write_all(slice::from_ref(&separator(i)))?;
632
633                last_type = Some(path_type);
634            } else {
635                write!(
636                    writer,
637                    "{x}:{y}|",
638                    x = pos.x + point.pos.x,
639                    y = pos.y + point.pos.y
640                )?;
641            }
642        }
643
644        if i != 0 {
645            write!(
646                writer,
647                "{x}:{y}{count}",
648                x = pos.x + point.pos.x,
649                y = pos.y + point.pos.y,
650                count = separator(i) as char,
651            )?;
652        }
653    }
654
655    let dist = slider
656        .path
657        .expected_dist()
658        .unwrap_or_else(|| slider.path.curve_with_bufs(bufs).dist());
659
660    write!(
661        writer,
662        "{span_count},{dist},",
663        span_count = slider.span_count(),
664    )?;
665
666    for i in 0..=slider.span_count() as usize {
667        write!(
668            writer,
669            "{sound_type}{suffix}",
670            sound_type = if i < slider.node_samples.len() {
671                u8::from(HitSoundType::from(slider.node_samples[i].as_slice()))
672            } else {
673                0
674            },
675            suffix = if i == slider.span_count() as usize {
676                ','
677            } else {
678                '|'
679            }
680        )?;
681    }
682
683    for i in 0..=slider.span_count() as usize {
684        if i < slider.node_samples.len() {
685            get_sample_bank(writer, &slider.node_samples[i], true, mode)?;
686        } else {
687            writer.write_all(b"0:0")?;
688        }
689
690        let suffix = if i == slider.span_count() as usize {
691            b","
692        } else {
693            b"|"
694        };
695
696        writer.write_all(suffix)?;
697    }
698
699    Ok(())
700}
701
702fn get_sample_bank<W: Write>(
703    writer: &mut W,
704    samples: &[HitSampleInfo],
705    banks_only: bool,
706    mode: GameMode,
707) -> IoResult<()> {
708    // osu!lazer throws an error if multiple samples match the filter but
709    // we'll just take the first and assume it's the only one.
710    let normal_bank = samples
711        .iter()
712        .find(|sample| sample.name == HitSampleInfo::HIT_NORMAL)
713        .map(|sample| sample.bank)
714        .unwrap_or_default();
715
716    let add_bank = samples
717        .iter()
718        .find(|sample| {
719            !matches!(
720                sample.name,
721                HitSampleInfo::HIT_NORMAL | HitSampleInfoName::File(_)
722            )
723        })
724        .map(|sample| sample.bank)
725        .unwrap_or_default();
726
727    write!(writer, "{}:{}", normal_bank as i32, add_bank as i32)?;
728
729    if banks_only {
730        return Ok(());
731    }
732
733    let mut custom_sample_bank = samples
734        .iter()
735        .find(|sample| matches!(sample.name, HitSampleInfoName::Default(_)))
736        .map_or(0, |sample| sample.custom_sample_bank);
737
738    let sample_filename = samples
739        .iter()
740        .find(|sample| matches!(sample.name, HitSampleInfoName::File(ref filename) if !filename.is_empty()))
741        .map(HitSampleInfo::lookup_name);
742
743    let mut volume = samples.first().map_or(100, |sample| sample.volume);
744
745    if mode != GameMode::Mania {
746        custom_sample_bank = 0;
747        volume = 0;
748    }
749
750    write!(writer, ":{custom_sample_bank}:{volume}:")?;
751
752    if let Some(filename) = sample_filename {
753        write!(writer, "{filename}")?;
754    }
755
756    Ok(())
757}
758
759fn collect_samples(map: &mut Beatmap, control_points: &mut ControlPoints) {
760    let mut ticks = Vec::new();
761    let mut curve_bufs = CurveBuffers::default();
762    let mut collected_samples = Vec::with_capacity(map.hit_objects.len() * 2);
763
764    for h in map.hit_objects.iter_mut() {
765        let end_time = h.end_time_with_bufs(&mut curve_bufs);
766        collect_sample(&mut collected_samples, &h.samples, end_time);
767
768        // Emitting samples for nested objects
769        match h.kind {
770            HitObjectKind::Circle(_) | HitObjectKind::Spinner(_) => {}
771            HitObjectKind::Slider(ref mut slider) => match map.mode {
772                GameMode::Osu => {
773                    let events = slider_events(
774                        h.start_time,
775                        slider,
776                        map.format_version,
777                        map.slider_tick_rate,
778                        &map.control_points,
779                        &mut curve_bufs,
780                        &mut ticks,
781                    );
782
783                    for event in events {
784                        match event.kind {
785                            SliderEventType::Tick | SliderEventType::LastTick => {}
786                            SliderEventType::Head => {
787                                let samples = slider.node_samples.first().unwrap_or(&h.samples);
788                                collect_sample(&mut collected_samples, samples, event.time);
789                            }
790                            SliderEventType::Repeat => {
791                                let samples = slider
792                                    .node_samples
793                                    .get((event.span_idx + 1) as usize)
794                                    .unwrap_or(&h.samples);
795
796                                collect_sample(&mut collected_samples, samples, event.time);
797                            }
798                            SliderEventType::Tail => {
799                                let samples = slider
800                                    .node_samples
801                                    .get((slider.repeat_count + 1) as usize)
802                                    .unwrap_or(&h.samples);
803
804                                collect_sample(&mut collected_samples, samples, event.time);
805                            }
806                        }
807                    }
808                }
809                GameMode::Taiko => {} // FIXME: convert slider to drumroll
810                GameMode::Catch => {
811                    let events = juicestream_events(
812                        h.start_time,
813                        slider,
814                        map.format_version,
815                        map.slider_tick_rate,
816                        map.slider_multiplier,
817                        &map.control_points,
818                        &mut curve_bufs,
819                        &mut ticks,
820                    );
821
822                    let mut node_idx = 0;
823
824                    for event in events {
825                        match event.kind {
826                            SliderEventType::Head
827                            | SliderEventType::Repeat
828                            | SliderEventType::Tail => {
829                                let samples =
830                                    slider.node_samples.get(node_idx).unwrap_or(&h.samples);
831                                collect_sample(&mut collected_samples, samples, event.time);
832                                node_idx += 1;
833                            }
834                            SliderEventType::Tick | SliderEventType::LastTick => {}
835                        }
836                    }
837                }
838                GameMode::Mania => collect_sample(&mut collected_samples, &h.samples, h.start_time), // Hold note
839            },
840            HitObjectKind::Hold(_) => {
841                collect_sample(&mut collected_samples, &h.samples, h.start_time);
842            }
843        }
844    }
845
846    collected_samples.sort_by(|a, b| a.time.total_cmp(&b.time));
847    let mut collected_samples = collected_samples.into_iter();
848
849    if let Some(sample) = collected_samples.next() {
850        control_points.add(sample.clone());
851        let mut last_sample = sample;
852
853        for sample in collected_samples {
854            if !sample.is_redundant(&last_sample) {
855                control_points.add(sample.clone());
856                last_sample = sample;
857            }
858        }
859    }
860}
861
862fn collect_sample(
863    collected_samples: &mut Vec<SamplePoint>,
864    samples: &[HitSampleInfo],
865    end_time: f64,
866) {
867    if samples.is_empty() {
868        return;
869    }
870
871    // We know the samples aren't empty so we can unwrap
872    let volume = samples.iter().map(|sample| sample.volume).max().unwrap();
873
874    let custom_idx = samples
875        .iter()
876        .map(|sample| sample.custom_sample_bank)
877        .max()
878        .unwrap();
879
880    let sample = SamplePoint {
881        time: end_time,
882        sample_bank: SamplePoint::DEFAULT_SAMPLE_BANK,
883        sample_volume: volume,
884        custom_sample_bank: custom_idx,
885    };
886
887    collected_samples.push(sample);
888}
889
890fn slider_events<'ticks>(
891    start_time: f64,
892    slider: &mut HitObjectSlider,
893    format_version: i32,
894    slider_tick_rate: f64,
895    control_points: &ControlPoints,
896    curve_bufs: &mut CurveBuffers,
897    ticks: &'ticks mut Vec<SliderEvent>,
898) -> SliderEventsIter<'ticks> {
899    let beat_len = control_points
900        .timing_point_at(start_time)
901        .map_or(TimingPoint::DEFAULT_BEAT_LEN, |point| point.beat_len);
902
903    let (slider_velocity, generate_ticks) = control_points.difficulty_point_at(start_time).map_or(
904        (
905            DifficultyPoint::DEFAULT_SLIDER_VELOCITY,
906            DifficultyPoint::DEFAULT_GENERATE_TICKS,
907        ),
908        |point| (point.slider_velocity, point.generate_ticks),
909    );
910
911    let tick_dist_multiplier = if format_version < 8 {
912        slider_velocity.recip()
913    } else {
914        1.0
915    };
916
917    let scoring_dist = slider.velocity * beat_len;
918
919    let tick_dist = if generate_ticks {
920        scoring_dist / slider_tick_rate * tick_dist_multiplier
921    } else {
922        f64::INFINITY
923    };
924
925    let dist = slider.path.curve_with_bufs(curve_bufs).dist();
926    let span_count = slider.span_count();
927    let span_duration = slider.duration_with_bufs(curve_bufs) / f64::from(span_count);
928
929    SliderEventsIter::new(
930        start_time,
931        span_duration,
932        slider.velocity,
933        tick_dist,
934        dist,
935        span_count,
936        ticks,
937    )
938}
939
940#[allow(clippy::too_many_arguments)]
941fn juicestream_events<'ticks>(
942    start_time: f64,
943    slider: &mut HitObjectSlider,
944    format_version: i32,
945    slider_tick_rate: f64,
946    slider_multiplier: f64,
947    control_points: &ControlPoints,
948    curve_bufs: &mut CurveBuffers,
949    ticks: &'ticks mut Vec<SliderEvent>,
950) -> SliderEventsIter<'ticks> {
951    let slider_velocity = control_points
952        .difficulty_point_at(start_time)
953        .map_or(DifficultyPoint::DEFAULT_SLIDER_VELOCITY, |point| {
954            point.slider_velocity
955        });
956
957    let tick_dist_multiplier = if format_version < 8 {
958        slider_velocity.recip()
959    } else {
960        1.0
961    };
962
963    let tick_dist_factor = f64::from(BASE_SCORING_DIST) * slider_multiplier / slider_tick_rate;
964
965    let tick_dist = tick_dist_factor * tick_dist_multiplier;
966
967    let dist = slider.path.curve_with_bufs(curve_bufs).dist();
968    let span_count = slider.span_count();
969    let span_duration = slider.duration_with_bufs(curve_bufs) / f64::from(span_count);
970
971    SliderEventsIter::new(
972        start_time,
973        span_duration,
974        slider.velocity,
975        tick_dist,
976        dist,
977        span_count,
978        ticks,
979    )
980}