1pub mod error;
2pub mod types;
3
4use crate::osu_file::types::Decimal;
5use either::Either;
6use nom::branch::alt;
7use nom::bytes::complete::*;
8use nom::character::complete::char;
9use nom::combinator::*;
10use nom::error::context;
11use nom::sequence::*;
12use nom::*;
13use rust_decimal_macros::dec;
14
15use crate::helper::*;
16use crate::parsers::*;
17
18pub use error::*;
19pub use types::*;
20
21use super::Error;
22use super::Integer;
23use super::Position;
24use super::Version;
25use super::VersionedDefault;
26use super::VersionedFromStr;
27use super::VersionedToString;
28use super::VersionedTryFrom;
29
30#[derive(Clone, Debug, Default, Hash, PartialEq, Eq)]
31pub struct HitObjects(pub Vec<HitObject>);
32
33impl VersionedFromStr for HitObjects {
34 type Err = Error<ParseError>;
35
36 fn from_str(s: &str, version: Version) -> std::result::Result<Option<Self>, Self::Err> {
37 let mut hitobjects = Vec::new();
38
39 for (line_index, s) in s.lines().enumerate() {
40 if s.trim().is_empty() {
41 continue;
42 }
43
44 hitobjects.push(Error::new_from_result_into(
45 HitObject::from_str(s, version).map(|v| v.unwrap()),
46 line_index,
47 )?);
48 }
49
50 Ok(Some(HitObjects(hitobjects)))
51 }
52}
53
54impl VersionedToString for HitObjects {
55 fn to_string(&self, version: Version) -> Option<String> {
56 Some(
57 self.0
58 .iter()
59 .filter_map(|o| o.to_string(version))
60 .collect::<Vec<_>>()
61 .join("\n"),
62 )
63 }
64}
65
66impl VersionedDefault for HitObjects {
67 fn default(_: Version) -> Option<Self> {
68 Some(HitObjects(Vec::new()))
69 }
70}
71
72#[derive(Clone, Debug, Hash, PartialEq, Eq)]
79#[non_exhaustive]
80pub struct HitObject {
81 pub position: Position,
83 pub time: Decimal,
85 pub obj_params: HitObjectParams,
89 pub new_combo: bool,
91 pub combo_skip_count: ComboSkipCount,
93 pub hitsound: HitSound,
95 pub hitsample: Option<HitSample>,
97}
98
99impl HitObject {
100 fn type_to_string(&self) -> String {
101 let mut bit_flag: u8 = 0;
102
103 bit_flag |= match self.obj_params {
104 HitObjectParams::HitCircle => 1,
105 HitObjectParams::Slider { .. } => 2,
106 HitObjectParams::Spinner { .. } => 8,
107 HitObjectParams::OsuManiaHold { .. } => 128,
108 };
109
110 if self.new_combo {
111 bit_flag |= 4;
112 }
113
114 bit_flag |= self.combo_skip_count.get() << 4;
116
117 bit_flag.to_string()
118 }
119
120 pub fn hitcircle_default() -> Self {
121 Self {
122 position: Default::default(),
123 time: Default::default(),
124 obj_params: HitObjectParams::HitCircle,
125 new_combo: Default::default(),
126 combo_skip_count: Default::default(),
127 hitsound: Default::default(),
128 hitsample: Default::default(),
129 }
130 }
131
132 pub fn spinner_default() -> Self {
133 Self {
134 position: Default::default(),
135 time: Default::default(),
136 obj_params: HitObjectParams::Spinner {
137 end_time: Default::default(),
138 },
139 new_combo: Default::default(),
140 combo_skip_count: Default::default(),
141 hitsound: Default::default(),
142 hitsample: Default::default(),
143 }
144 }
145
146 pub fn osu_mania_hold_default() -> Self {
147 Self {
148 position: Position {
149 x: dec!(0).into(),
150 ..Default::default()
151 },
152 time: Default::default(),
153 obj_params: HitObjectParams::OsuManiaHold {
154 end_time: Default::default(),
155 },
156 new_combo: Default::default(),
157 combo_skip_count: Default::default(),
158 hitsound: Default::default(),
159 hitsample: Default::default(),
160 }
161 }
162}
163
164const OLD_VERSION_TIME_OFFSET: rust_decimal::Decimal = dec!(24);
165
166impl VersionedFromStr for HitObject {
167 type Err = ParseHitObjectError;
168
169 fn from_str(s: &str, version: Version) -> std::result::Result<Option<Self>, Self::Err> {
170 let hitsound = context(
171 ParseHitObjectError::InvalidHitSound.into(),
172 comma_field_versioned_type(version),
173 );
174 let mut hitsample = alt((
175 nothing().map(|_| None),
176 preceded(
177 context(ParseHitObjectError::MissingHitSample.into(), comma()),
178 context(
179 ParseHitObjectError::InvalidHitSample.into(),
180 map_res(rest, |s| {
181 HitSample::from_str(s, version).map(|v| v.unwrap())
182 }),
183 ),
184 )
185 .map(Some),
186 ));
187
188 let (s, (position, time, obj_type, hitsound)) = tuple((
189 tuple((
190 context(ParseHitObjectError::InvalidX.into(), comma_field_type()),
191 preceded(
192 context(ParseHitObjectError::MissingY.into(), comma()),
193 context(ParseHitObjectError::InvalidY.into(), comma_field_type()),
194 ),
195 ))
196 .map(|(x, y)| (Position { x, y })),
197 preceded(
198 context(ParseHitObjectError::MissingTime.into(), comma()),
199 context(ParseHitObjectError::InvalidTime.into(), comma_field_type()),
200 )
201 .map(|mut t: Decimal| {
203 if (3..=4).contains(&version) {
204 if let Either::Left(value) = t.get_mut() {
205 *value += OLD_VERSION_TIME_OFFSET;
206 }
207 }
208
209 t
210 }),
211 preceded(
212 context(ParseHitObjectError::MissingObjType.into(), comma()),
213 context(
214 ParseHitObjectError::InvalidObjType.into(),
215 comma_field_type::<_, Integer>(),
216 ),
217 ),
218 preceded(
219 context(ParseHitObjectError::MissingHitSound.into(), comma()),
220 hitsound,
221 ),
222 ))(s)?;
223
224 let new_combo = nth_bit_state_i64(obj_type as i64, 2);
225 let combo_skip_count = <ComboSkipCount as VersionedTryFrom<u8>>::try_from(
226 (obj_type >> 4 & 0b111) as u8,
227 version,
228 )
229 .unwrap()
230 .unwrap();
231
232 let hitobject = if nth_bit_state_i64(obj_type as i64, 0) {
233 let (_, hitsample) = hitsample(s)?;
234
235 HitObject {
237 position,
238 time,
239 obj_params: HitObjectParams::HitCircle,
240 new_combo,
241 combo_skip_count,
242 hitsound,
243 hitsample,
244 }
245 } else if nth_bit_state_i64(obj_type as i64, 1) {
246 let pipe = char('|');
248
249 let (
250 _,
251 (
252 (curve_type, curve_points),
253 slides,
254 length,
255 (
256 edge_sounds,
257 edge_sets,
258 hitsample,
259 edge_sounds_short_hand,
260 edge_sets_shorthand,
261 ),
262 ),
263 ) = tuple((
264 alt((
265 preceded(
267 context(ParseHitObjectError::MissingCurveType.into(), comma()),
268 context(
269 ParseHitObjectError::InvalidCurveType.into(),
270 comma_field_versioned_type(version),
271 ),
272 )
273 .map(|curve_type| (curve_type, Vec::new())),
274 tuple((
276 preceded(
277 context(ParseHitObjectError::MissingCurveType.into(), comma()),
278 context(
279 ParseHitObjectError::InvalidCurveType.into(),
280 map_res(take_till(|c| c == '|'), |f: &str| {
281 CurveType::from_str(f, version).map(|c| c.unwrap())
282 }),
283 ),
284 ),
285 preceded(
286 context(ParseHitObjectError::MissingCurvePoint.into(), pipe),
287 context(
288 ParseHitObjectError::InvalidCurvePoint.into(),
289 pipe_vec_versioned_map(version).map(|mut v| {
290 if version == 3 && !v.is_empty() {
291 v.remove(0);
292 }
293 v
294 }),
295 ),
296 ),
297 )),
298 )),
299 preceded(
300 context(ParseHitObjectError::MissingSlidesCount.into(), comma()),
301 context(
302 ParseHitObjectError::InvalidSlidesCount.into(),
303 comma_field_type(),
304 ),
305 ),
306 preceded(
307 context(ParseHitObjectError::MissingLength.into(), comma()),
308 context(
309 ParseHitObjectError::InvalidLength.into(),
310 comma_field_type(),
311 ),
312 ),
313 alt((
314 nothing().map(|_| (Vec::new(), Vec::new(), None, true, true)),
315 tuple((
316 preceded(
317 context(ParseHitObjectError::MissingEdgeSound.into(), comma()),
318 context(
319 ParseHitObjectError::InvalidEdgeSound.into(),
320 pipe_vec_versioned_map(version),
321 ),
322 ),
323 alt((
324 nothing().map(|_| (Vec::new(), None, true)),
325 tuple((
326 preceded(
327 context(ParseHitObjectError::MissingEdgeSet.into(), comma()),
328 context(
329 ParseHitObjectError::InvalidEdgeSet.into(),
330 pipe_vec_versioned_map(version),
331 ),
332 ),
333 hitsample,
334 ))
335 .map(|(edge_sets, hitsample)| (edge_sets, hitsample, false)),
336 )),
337 ))
338 .map(
339 |(edge_sounds, (edge_sets, hitsample, edge_sets_shorthand))| {
340 (
341 edge_sounds,
342 edge_sets,
343 hitsample,
344 false,
345 edge_sets_shorthand,
346 )
347 },
348 ),
349 )),
350 ))(s)?;
351
352 HitObject {
353 position,
354 time,
355 obj_params: HitObjectParams::Slider(SlideParams {
356 curve_type,
357 curve_points,
358 slides,
359 length,
360 edge_sounds,
361 edge_sets,
362 edge_sets_shorthand,
363 edge_sounds_short_hand,
364 }),
365 new_combo,
366 combo_skip_count,
367 hitsound,
368 hitsample,
369 }
370 } else if nth_bit_state_i64(obj_type as i64, 3) {
371 let (_, (end_time, hitsample)) = tuple((
373 preceded(
374 context(ParseHitObjectError::MissingEndTime.into(), comma()),
375 context(
376 ParseHitObjectError::InvalidEndTime.into(),
377 comma_field_type(),
378 ),
379 )
380 .map(|mut t: Decimal| {
381 if (3..=4).contains(&version) {
382 if let Either::Left(value) = t.get_mut() {
383 *value += OLD_VERSION_TIME_OFFSET;
384 }
385 }
386
387 t
388 }),
389 hitsample,
390 ))(s)?;
391
392 HitObject {
393 position,
394 time,
395 obj_params: HitObjectParams::Spinner { end_time },
396 new_combo,
397 combo_skip_count,
398 hitsound,
399 hitsample,
400 }
401 } else if nth_bit_state_i64(obj_type as i64, 7) {
402 let hitsample = alt((
405 nothing().map(|_| None),
406 preceded(
407 context(ParseHitObjectError::MissingHitSample.into(), char(':')),
408 context(
409 ParseHitObjectError::InvalidHitSample.into(),
410 map_res(rest, |s| {
411 HitSample::from_str(s, version).map(|v| v.unwrap())
412 }),
413 ),
414 )
415 .map(Some),
416 ));
417 let end_time = context(
418 ParseHitObjectError::InvalidEndTime.into(),
419 map_res(take_until(":"), |s: &str| s.parse()),
420 )
421 .map(|mut t: Decimal| {
422 if (3..=4).contains(&version) {
423 if let Either::Left(value) = t.get_mut() {
424 *value += OLD_VERSION_TIME_OFFSET;
425 }
426 }
427
428 t
429 });
430 let (_, (end_time, hitsample)) = tuple((
431 preceded(
432 context(ParseHitObjectError::MissingEndTime.into(), comma()),
433 end_time,
434 ),
435 hitsample,
436 ))(s)?;
437
438 HitObject {
439 position,
440 time,
441 obj_params: HitObjectParams::OsuManiaHold { end_time },
442 new_combo,
443 combo_skip_count,
444 hitsound,
445 hitsample,
446 }
447 } else {
448 return Err(ParseHitObjectError::UnknownObjType);
449 };
450
451 Ok(Some(hitobject))
452 }
453}
454
455impl VersionedToString for HitObject {
456 fn to_string(&self, version: Version) -> Option<String> {
457 let mut properties: Vec<String> = vec![
458 self.position.x.to_string(),
459 self.position.y.to_string(),
460 if (3..=4).contains(&version) {
461 match self.time.get() {
462 Either::Left(value) => (value - OLD_VERSION_TIME_OFFSET).to_string(),
463 Either::Right(value) => value.to_string(),
464 }
465 } else {
466 self.time.to_string()
467 },
468 self.type_to_string(),
469 self.hitsound.to_string(version).unwrap(),
470 ];
471
472 match &self.obj_params {
473 HitObjectParams::HitCircle => (),
474 HitObjectParams::Slider(SlideParams {
475 curve_type,
476 curve_points,
477 slides,
478 length,
479 edge_sounds,
480 edge_sets,
481 edge_sounds_short_hand,
482 edge_sets_shorthand,
483 }) => {
484 properties.push(curve_type.to_string(version).unwrap());
485
486 let has_curve_points = version == 3 || !curve_points.is_empty();
487
488 let mut properties_2 = Vec::new();
489
490 if version == 3 {
491 let mut curve_points = curve_points.clone();
492 curve_points.insert(0, CurvePoint(self.position.clone()));
493 properties_2.push(pipe_vec_to_string(&curve_points, version));
494 } else if has_curve_points {
495 properties_2.push(pipe_vec_to_string(curve_points, version));
496 }
497 properties_2.push(slides.to_string());
498 properties_2.push(length.to_string());
499
500 if !edge_sounds.is_empty()
501 || !*edge_sounds_short_hand
502 || !edge_sets.is_empty()
503 || !*edge_sets_shorthand
504 || self.hitsample.is_some()
505 {
506 properties_2.push(pipe_vec_to_string(edge_sounds, version));
507 }
508 if !edge_sets.is_empty() || !*edge_sets_shorthand || self.hitsample.is_some() {
509 properties_2.push(pipe_vec_to_string(edge_sets, version));
510 }
511 if let Some(hitsample) = &self.hitsample {
512 if let Some(hitsample) = hitsample.to_string(version) {
513 properties_2.push(hitsample);
514 }
515 }
516
517 let slider_str = if has_curve_points {
518 format!("{}|{}", properties.join(","), properties_2.join(","))
519 } else {
520 format!("{},{}", properties.join(","), properties_2.join(","))
521 };
522
523 return Some(slider_str);
524 }
525 HitObjectParams::Spinner { end_time } => {
526 properties.push(if (3..=4).contains(&version) {
527 match end_time.get() {
528 Either::Left(value) => (value - OLD_VERSION_TIME_OFFSET).to_string(),
529 Either::Right(value) => value.to_string(),
530 }
531 } else {
532 end_time.to_string()
533 });
534 }
535 HitObjectParams::OsuManiaHold { end_time } => {
536 properties.push(if (3..=4).contains(&version) {
537 match end_time.get() {
538 Either::Left(value) => (value - OLD_VERSION_TIME_OFFSET).to_string(),
539 Either::Right(value) => value.to_string(),
540 }
541 } else {
542 end_time.to_string()
543 });
544
545 let hitsample = if let Some(hitsample) = &self.hitsample {
546 if let Some(hitsample) = hitsample.to_string(version) {
547 hitsample
548 } else {
549 String::new()
550 }
551 } else {
552 String::new()
553 };
554
555 return Some(format!("{}:{hitsample}", properties.join(",")));
556 }
557 }
558
559 if let Some(hitsample) = &self.hitsample {
560 if let Some(hitsample) = hitsample.to_string(version) {
561 properties.push(hitsample);
562 }
563 }
564
565 let s = properties.join(",");
566
567 let s = if version == 3 && matches!(self.obj_params, HitObjectParams::HitCircle) {
569 format!("{s},")
570 } else {
571 s
572 };
573
574 Some(s)
575 }
576}
577
578#[derive(Clone, Debug, Hash, PartialEq, Eq)]
579#[non_exhaustive]
580pub enum HitObjectParams {
581 HitCircle,
582 Slider(SlideParams),
583 Spinner { end_time: Decimal },
584 OsuManiaHold { end_time: Decimal },
585}
586
587#[derive(Clone, Debug, Hash, PartialEq, Eq)]
588pub struct SlideParams {
589 pub curve_type: CurveType,
590 pub curve_points: Vec<CurvePoint>,
591 pub slides: Integer,
592 pub length: Decimal,
593 pub edge_sounds: Vec<HitSound>,
594 edge_sounds_short_hand: bool,
595 pub edge_sets: Vec<EdgeSet>,
596 edge_sets_shorthand: bool,
597}
598
599impl SlideParams {
600 pub fn new(
601 curve_type: CurveType,
602 curve_points: Vec<CurvePoint>,
603 slides: Integer,
604 length: Decimal,
605 edge_sounds: Vec<HitSound>,
606 edge_sets: Vec<EdgeSet>,
607 ) -> Self {
608 Self {
609 curve_type,
610 curve_points,
611 slides,
612 length,
613 edge_sounds,
614 edge_sets,
615 edge_sets_shorthand: true,
616 edge_sounds_short_hand: true,
617 }
618 }
619}