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 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; 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 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; 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 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; 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 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, 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 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 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 .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}