rosu_pp/osu/
object.rs

1use std::borrow::Cow;
2
3use rosu_map::{
4    section::{
5        general::GameMode,
6        hit_objects::{CurveBuffers, SliderEvent, SliderEventType, SliderEventsIter},
7    },
8    util::Pos,
9};
10
11use crate::{
12    model::{
13        control_point::{DifficultyPoint, TimingPoint},
14        hit_object::{HitObject, HitObjectKind, HoldNote, Slider, Spinner},
15    },
16    util::{get_precision_adjusted_beat_len, sort},
17    Beatmap,
18};
19
20use super::PLAYFIELD_BASE_SIZE;
21
22pub struct OsuObject {
23    pub pos: Pos,
24    pub start_time: f64,
25    pub stack_height: i32,
26    pub stack_offset: Pos,
27    pub kind: OsuObjectKind,
28}
29
30impl OsuObject {
31    pub const OBJECT_RADIUS: f32 = 64.0;
32    pub const PREEMPT_MIN: f64 = 450.0;
33
34    const BASE_SCORING_DIST: f32 = 100.0;
35
36    pub fn new(
37        h: &HitObject,
38        map: &Beatmap,
39        curve_bufs: &mut CurveBuffers,
40        ticks_buf: &mut Vec<SliderEvent>,
41    ) -> Self {
42        let kind = match h.kind {
43            HitObjectKind::Circle => OsuObjectKind::Circle,
44            HitObjectKind::Slider(ref slider) => {
45                OsuObjectKind::Slider(OsuSlider::new(h, slider, map, curve_bufs, ticks_buf))
46            }
47            HitObjectKind::Spinner(spinner) => OsuObjectKind::Spinner(spinner),
48            HitObjectKind::Hold(HoldNote { duration }) => {
49                OsuObjectKind::Spinner(Spinner { duration })
50            }
51        };
52
53        Self {
54            pos: h.pos,
55            start_time: h.start_time,
56            stack_height: 0,
57            stack_offset: Pos::default(),
58            kind,
59        }
60    }
61
62    pub fn reflect_vertically(&mut self) {
63        fn reflect_y(y: &mut f32) {
64            *y = PLAYFIELD_BASE_SIZE.y - *y;
65        }
66
67        reflect_y(&mut self.pos.y);
68
69        if let OsuObjectKind::Slider(ref mut slider) = self.kind {
70            // Requires `stack_offset` so we can't add `h.pos` just yet
71            slider.lazy_end_pos.y = -slider.lazy_end_pos.y;
72
73            for nested in slider.nested_objects.iter_mut() {
74                let mut nested_pos = self.pos; // already reflected at this point
75                nested_pos += Pos::new(nested.pos.x, -nested.pos.y);
76                nested.pos = nested_pos;
77            }
78        }
79    }
80
81    pub fn reflect_horizontally(&mut self) {
82        fn reflect_x(x: &mut f32) {
83            *x = PLAYFIELD_BASE_SIZE.x - *x;
84        }
85
86        reflect_x(&mut self.pos.x);
87
88        if let OsuObjectKind::Slider(ref mut slider) = self.kind {
89            // Requires `stack_offset` so we can't add `h.pos` just yet
90            slider.lazy_end_pos.x = -slider.lazy_end_pos.x;
91
92            for nested in slider.nested_objects.iter_mut() {
93                let mut nested_pos = self.pos; // already reflected at this point
94                nested_pos += Pos::new(-nested.pos.x, nested.pos.y);
95                nested.pos = nested_pos;
96            }
97        }
98    }
99
100    pub fn reflect_both_axes(&mut self) {
101        fn reflect(pos: &mut Pos) {
102            pos.x = PLAYFIELD_BASE_SIZE.x - pos.x;
103            pos.y = PLAYFIELD_BASE_SIZE.y - pos.y;
104        }
105
106        reflect(&mut self.pos);
107
108        if let OsuObjectKind::Slider(ref mut slider) = self.kind {
109            // Requires `stack_offset` so we can't add `h.pos` just yet
110            slider.lazy_end_pos.x = -slider.lazy_end_pos.x;
111            slider.lazy_end_pos.y = -slider.lazy_end_pos.y;
112
113            for nested in slider.nested_objects.iter_mut() {
114                let mut nested_pos = self.pos; // already reflected at this point
115                nested_pos += Pos::new(-nested.pos.x, -nested.pos.y);
116                nested.pos = nested_pos;
117            }
118        }
119    }
120
121    pub fn finalize_nested(&mut self) {
122        if let OsuObjectKind::Slider(ref mut slider) = self.kind {
123            for nested in slider.nested_objects.iter_mut() {
124                nested.pos += self.pos;
125            }
126        }
127    }
128
129    pub fn end_time(&self) -> f64 {
130        match self.kind {
131            OsuObjectKind::Circle => self.start_time,
132            OsuObjectKind::Slider(ref slider) => slider.end_time,
133            OsuObjectKind::Spinner(ref spinner) => self.start_time + spinner.duration,
134        }
135    }
136
137    pub const fn stacked_pos(&self) -> Pos {
138        // Performed manually for const-ness
139        // self.pos + self.stack_offset
140
141        Pos::new(
142            self.pos.x + self.stack_offset.x,
143            self.pos.y + self.stack_offset.y,
144        )
145    }
146
147    pub fn end_pos(&self) -> Pos {
148        match self.kind {
149            OsuObjectKind::Circle | OsuObjectKind::Spinner(_) => self.pos,
150            OsuObjectKind::Slider(ref slider) => {
151                slider.tail().map_or(Pos::default(), |nested| nested.pos)
152            }
153        }
154    }
155
156    pub fn stacked_end_pos(&self) -> Pos {
157        self.end_pos() + self.stack_offset
158    }
159
160    pub const fn lazy_travel_time(&self) -> f64 {
161        match self.kind {
162            OsuObjectKind::Circle | OsuObjectKind::Spinner(_) => 0.0,
163            OsuObjectKind::Slider(ref slider) => slider.lazy_travel_time,
164        }
165    }
166
167    pub const fn is_circle(&self) -> bool {
168        matches!(self.kind, OsuObjectKind::Circle)
169    }
170
171    pub const fn is_slider(&self) -> bool {
172        matches!(self.kind, OsuObjectKind::Slider { .. })
173    }
174
175    pub const fn is_spinner(&self) -> bool {
176        matches!(self.kind, OsuObjectKind::Spinner(_))
177    }
178}
179
180pub enum OsuObjectKind {
181    Circle,
182    Slider(OsuSlider),
183    Spinner(Spinner),
184}
185
186pub struct OsuSlider {
187    pub end_time: f64,
188    pub lazy_end_pos: Pos,
189    pub lazy_travel_dist: f32,
190    pub lazy_travel_time: f64,
191    pub nested_objects: Vec<NestedSliderObject>,
192}
193
194impl OsuSlider {
195    fn new(
196        h: &HitObject,
197        slider: &Slider,
198        map: &Beatmap,
199        curve_bufs: &mut CurveBuffers,
200        ticks_buf: &mut Vec<SliderEvent>,
201    ) -> Self {
202        let start_time = h.start_time;
203        let slider_multiplier = map.slider_multiplier;
204        let slider_tick_rate = map.slider_tick_rate;
205
206        let beat_len = map
207            .timing_point_at(start_time)
208            .map_or(TimingPoint::DEFAULT_BEAT_LEN, |point| point.beat_len);
209
210        let (slider_velocity, generate_ticks) = map.difficulty_point_at(start_time).map_or(
211            (
212                DifficultyPoint::DEFAULT_SLIDER_VELOCITY,
213                DifficultyPoint::DEFAULT_GENERATE_TICKS,
214            ),
215            |point| (point.slider_velocity, point.generate_ticks),
216        );
217
218        let path = slider.curve(GameMode::Osu, curve_bufs);
219
220        let span_count = slider.span_count() as f64;
221
222        let velocity = f64::from(OsuObject::BASE_SCORING_DIST) * slider_multiplier
223            / get_precision_adjusted_beat_len(slider_velocity, beat_len);
224        let scoring_dist = velocity * beat_len;
225
226        let end_time = start_time + span_count * path.dist() / velocity;
227
228        let duration = end_time - start_time;
229        let span_duration = duration / span_count;
230
231        let tick_dist_multiplier = if map.version < 8 {
232            slider_velocity.recip()
233        } else {
234            1.0
235        };
236
237        let tick_dist = if generate_ticks {
238            scoring_dist / slider_tick_rate * tick_dist_multiplier
239        } else {
240            f64::INFINITY
241        };
242
243        let events = SliderEventsIter::new(
244            start_time,
245            span_duration,
246            velocity,
247            tick_dist,
248            path.dist(),
249            slider.span_count() as i32,
250            ticks_buf,
251        );
252
253        let span_at = |progress: f64| (progress * span_count) as i32;
254
255        let obj_progress_at = |progress: f64| {
256            let p = progress * span_count % 1.0;
257
258            if span_at(progress) % 2 == 1 {
259                1.0 - p
260            } else {
261                p
262            }
263        };
264
265        let end_path_pos = path.position_at(obj_progress_at(1.0));
266
267        let mut nested_objects: Vec<_> = events
268            .filter_map(|e| {
269                let obj = match e.kind {
270                    SliderEventType::Tick => NestedSliderObject {
271                        pos: path.position_at(e.path_progress),
272                        start_time: e.time,
273                        kind: NestedSliderObjectKind::Tick,
274                    },
275                    SliderEventType::Repeat => NestedSliderObject {
276                        pos: path.position_at(e.path_progress),
277                        start_time: start_time + f64::from(e.span_idx + 1) * span_duration,
278                        kind: NestedSliderObjectKind::Repeat,
279                    },
280                    SliderEventType::Tail => NestedSliderObject {
281                        pos: end_path_pos, // no `h.pos` yet to keep order of float operations
282                        start_time: e.time,
283                        kind: NestedSliderObjectKind::Tail,
284                    },
285                    SliderEventType::Head | SliderEventType::LastTick => return None,
286                };
287
288                Some(obj)
289            })
290            .collect();
291
292        sort::csharp(&mut nested_objects, |a, b| {
293            a.start_time.total_cmp(&b.start_time)
294        });
295
296        let mut nested = Cow::Borrowed(nested_objects.as_slice());
297        let lazy_travel_time = OsuSlider::lazy_travel_time(start_time, duration, &mut nested);
298
299        let mut end_time_min = lazy_travel_time / span_duration;
300
301        if end_time_min % 2.0 >= 1.0 {
302            end_time_min = 1.0 - end_time_min % 1.0;
303        } else {
304            end_time_min %= 1.0;
305        }
306
307        let lazy_end_pos = path.position_at(end_time_min);
308
309        Self {
310            end_time,
311            lazy_end_pos,
312            lazy_travel_dist: 0.0,
313            lazy_travel_time,
314            nested_objects,
315        }
316    }
317
318    pub fn lazy_travel_time(
319        start_time: f64,
320        duration: f64,
321        nested_objects: &mut Cow<'_, [NestedSliderObject]>,
322    ) -> f64 {
323        const TAIL_LENIENCY: f64 = -36.0;
324
325        let mut tracking_end_time =
326            (start_time + duration + TAIL_LENIENCY).max(start_time + duration / 2.0);
327
328        let last_real_tick = nested_objects
329            .iter()
330            .enumerate()
331            .rfind(|(_, nested)| nested.is_tick());
332
333        if let Some((idx, last_real_tick)) =
334            last_real_tick.filter(|(_, tick)| tick.start_time > tracking_end_time)
335        {
336            tracking_end_time = last_real_tick.start_time;
337
338            // * When the last tick falls after the tracking end time, we need to re-sort the nested objects
339            // * based on time. This creates a somewhat weird ordering which is counter to how a user would
340            // * understand the slider, but allows a zero-diff with known diffcalc output.
341            // *
342            // * To reiterate, this is definitely not correct from a difficulty calculation perspective
343            // * and should be revisited at a later date (likely by replacing this whole code with the commented
344            // * version above).
345            nested_objects.to_mut()[idx..].rotate_left(1);
346        }
347
348        tracking_end_time - start_time
349    }
350
351    pub fn repeat_count(&self) -> usize {
352        self.nested_objects
353            .iter()
354            .filter(|nested| matches!(nested.kind, NestedSliderObjectKind::Repeat))
355            .count()
356    }
357
358    /// Counts both ticks and repeats
359    pub fn large_tick_count(&self) -> usize {
360        self.nested_objects
361            .iter()
362            .filter(|nested| {
363                matches!(
364                    nested.kind,
365                    NestedSliderObjectKind::Tick | NestedSliderObjectKind::Repeat
366                )
367            })
368            .count()
369    }
370
371    pub fn tail(&self) -> Option<&NestedSliderObject> {
372        self.nested_objects
373            .iter()
374            // The tail is not necessarily the last nested object, e.g. on very
375            // short and fast buzz sliders (/b/1001757)
376            .rfind(|nested| matches!(nested.kind, NestedSliderObjectKind::Tail))
377    }
378}
379
380#[derive(Clone, Debug)]
381pub struct NestedSliderObject {
382    pub pos: Pos,
383    pub start_time: f64,
384    pub kind: NestedSliderObjectKind,
385}
386
387impl NestedSliderObject {
388    pub const fn is_repeat(&self) -> bool {
389        matches!(self.kind, NestedSliderObjectKind::Repeat)
390    }
391
392    pub const fn is_tick(&self) -> bool {
393        matches!(self.kind, NestedSliderObjectKind::Tick)
394    }
395}
396
397#[derive(Copy, Clone, Debug)]
398pub enum NestedSliderObjectKind {
399    Repeat,
400    Tail,
401    Tick,
402}