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 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 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 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 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 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 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 => {} 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), },
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 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}