1use nom::{
2 branch::alt,
3 bytes::complete::tag,
4 combinator::eof,
5 error::context,
6 sequence::{preceded, tuple},
7 Parser,
8};
9
10use crate::{
11 osu_file::{FilePath, Integer, Position, Version, VersionedFromStr, VersionedToString},
12 parsers::{
13 comma, comma_field, comma_field_type, comma_field_versioned_type, consume_rest_type,
14 consume_rest_versioned_type,
15 },
16 Decimal,
17};
18
19pub use error::*;
20
21use self::types::{LayerLegacy, OriginTypeLegacy};
22
23use super::{storyboard::cmds::Command, EventWithCommands, Volume, OLD_VERSION_TIME_OFFSET};
24
25pub mod error;
26mod parsers;
27pub mod types;
28use parsers::*;
29
30fn position_str(position: &Option<Position>) -> String {
31 match position {
32 Some(position) => format!(",{},{}", position.x, position.y),
33 None => String::new(),
34 }
35}
36
37fn time_to_string(time: Integer, version: Version) -> String {
38 let time = if (3..=4).contains(&version) {
39 time - OLD_VERSION_TIME_OFFSET
40 } else {
41 time
42 };
43
44 time.to_string()
45}
46
47#[derive(Clone, Debug, Hash, PartialEq, Eq)]
48pub struct Background {
49 pub start_time: Integer,
50 pub file_name: FilePath,
51 pub position: Option<Position>,
52 pub commands: Vec<Command>,
53}
54
55pub const BACKGROUND_HEADER: &str = "0";
56
57impl VersionedFromStr for Background {
58 type Err = ParseBackgroundError;
59
60 fn from_str(s: &str, _: Version) -> std::result::Result<Option<Self>, Self::Err> {
61 let (_, (start_time, (filename, position))) = preceded(
62 tuple((
63 context(
64 ParseBackgroundError::WrongEventType.into(),
65 tag(BACKGROUND_HEADER),
66 ),
67 context(ParseBackgroundError::MissingStartTime.into(), comma()),
68 )),
69 tuple((
70 context(
71 ParseBackgroundError::InvalidStartTime.into(),
72 comma_field_type(),
73 ),
74 preceded(
75 context(ParseBackgroundError::MissingFileName.into(), comma()),
76 file_name_and_position(
77 ParseBackgroundError::MissingX.into(),
78 ParseBackgroundError::InvalidX.into(),
79 ParseBackgroundError::MissingY.into(),
80 ParseBackgroundError::InvalidY.into(),
81 ),
82 ),
83 )),
84 )(s)?;
85
86 Ok(Some(Background {
87 start_time,
88 file_name: filename,
89 position,
90 commands: Vec::new(),
91 }))
92 }
93}
94
95impl VersionedToString for Background {
96 fn to_string(&self, version: Version) -> Option<String> {
97 self.to_string_variables(version, &[])
98 }
99}
100
101impl EventWithCommands for Background {
102 fn commands(&self) -> &[Command] {
103 &self.commands
104 }
105
106 fn commands_mut(&mut self) -> &mut Vec<Command> {
107 &mut self.commands
108 }
109
110 fn to_string_cmd(&self, version: Version) -> Option<String> {
111 Some(format!(
112 "{BACKGROUND_HEADER},{},{}{}",
113 self.start_time,
114 self.file_name.to_string(version).unwrap(),
115 position_str(&self.position),
116 ))
117 }
118}
119
120#[derive(Clone, Debug, Hash, PartialEq, Eq)]
121pub struct Video {
122 pub start_time: Integer,
123 pub file_name: FilePath,
124 pub position: Option<Position>,
125 pub commands: Vec<Command>,
126 short_hand: bool,
127}
128
129impl Video {
130 pub fn new(start_time: Integer, file_name: FilePath, position: Option<Position>) -> Self {
131 Self {
132 commands: Vec::new(),
133 start_time,
134 file_name,
135 position,
136 short_hand: true,
137 }
138 }
139}
140
141pub const VIDEO_HEADER: &str = "1";
142pub const VIDEO_HEADER_LONG: &str = "Video";
143
144impl VersionedFromStr for Video {
145 type Err = ParseVideoError;
146
147 fn from_str(s: &str, version: Version) -> std::result::Result<Option<Self>, Self::Err> {
148 let (_, (short_hand, start_time, (file_name, position))) = tuple((
149 alt((
150 tag(VIDEO_HEADER).map(|_| true),
151 context(
152 ParseVideoError::WrongEventType.into(),
153 tag(VIDEO_HEADER_LONG).map(|_| false),
154 ),
155 )),
156 preceded(
157 context(ParseVideoError::MissingStartTime.into(), comma()),
158 start_time_offset(ParseVideoError::InvalidStartTime.into(), version),
159 ),
160 preceded(
161 context(ParseVideoError::MissingFileName.into(), comma()),
162 file_name_and_position(
163 ParseVideoError::MissingX.into(),
164 ParseVideoError::InvalidX.into(),
165 ParseVideoError::MissingY.into(),
166 ParseVideoError::InvalidY.into(),
167 ),
168 ),
169 ))(s)?;
170
171 Ok(Some(Video {
172 commands: Vec::new(),
173 start_time,
174 file_name,
175 position,
176 short_hand,
177 }))
178 }
179}
180
181impl VersionedToString for Video {
182 fn to_string(&self, version: Version) -> Option<String> {
183 self.to_string_variables(version, &[])
184 }
185}
186
187impl EventWithCommands for Video {
188 fn commands(&self) -> &[Command] {
189 &self.commands
190 }
191
192 fn commands_mut(&mut self) -> &mut Vec<Command> {
193 &mut self.commands
194 }
195
196 fn to_string_cmd(&self, version: Version) -> Option<String> {
197 Some(format!(
198 "{},{},{}{}",
199 if self.short_hand {
200 VIDEO_HEADER
201 } else {
202 VIDEO_HEADER_LONG
203 },
204 time_to_string(self.start_time, version),
205 self.file_name.to_string(version).unwrap(),
206 position_str(&self.position)
207 ))
208 }
209}
210
211#[derive(Clone, Debug, Hash, PartialEq, Eq)]
212pub struct Break {
213 pub start_time: Integer,
214 pub end_time: Integer,
215 short_hand: bool,
216}
217
218impl Break {
219 pub fn new(start_time: Integer, end_time: Integer) -> Self {
220 Self {
221 start_time,
222 end_time,
223 short_hand: true,
224 }
225 }
226}
227
228pub const BREAK_HEADER: &str = "2";
229pub const BREAK_HEADER_LONG: &str = "Break";
230
231impl VersionedFromStr for Break {
232 type Err = ParseBreakError;
233
234 fn from_str(s: &str, version: Version) -> std::result::Result<Option<Self>, Self::Err> {
235 let (_, (short_hand, start_time, end_time)) = tuple((
236 alt((
237 tag(BREAK_HEADER).map(|_| true),
238 context(
239 ParseBreakError::WrongEventType.into(),
240 tag(BREAK_HEADER_LONG).map(|_| false),
241 ),
242 )),
243 preceded(
244 context(ParseBreakError::MissingStartTime.into(), comma()),
245 start_time_offset(ParseBreakError::InvalidStartTime.into(), version),
246 ),
247 preceded(
248 context(ParseBreakError::MissingEndTime.into(), comma()),
249 end_time(ParseBreakError::InvalidEndTime.into(), version),
250 ),
251 ))(s)?;
252
253 Ok(Some(Break {
254 start_time,
255 end_time,
256 short_hand,
257 }))
258 }
259}
260
261impl VersionedToString for Break {
262 fn to_string(&self, version: Version) -> Option<String> {
263 Some(format!(
264 "{},{},{}",
265 if self.short_hand {
266 BREAK_HEADER
267 } else {
268 BREAK_HEADER_LONG
269 },
270 time_to_string(self.start_time, version),
271 time_to_string(self.end_time, version)
272 ))
273 }
274}
275
276#[derive(Clone, Debug, Hash, PartialEq, Eq)]
277pub struct ColourTransformation {
278 pub start_time: Integer,
279 pub red: u8,
280 pub green: u8,
281 pub blue: u8,
282}
283
284pub const COLOUR_TRANSFORMATION_HEADER: &str = "3";
285
286impl VersionedFromStr for ColourTransformation {
287 type Err = ParseColourTransformationError;
288
289 fn from_str(s: &str, version: Version) -> std::result::Result<Option<Self>, Self::Err> {
290 let (_, (start_time, red, green, blue)) = tuple((
291 preceded(
292 tuple((
293 context(
294 ParseColourTransformationError::WrongEventType.into(),
295 tag(COLOUR_TRANSFORMATION_HEADER),
296 ),
297 context(
298 ParseColourTransformationError::MissingStartTime.into(),
299 comma(),
300 ),
301 )),
302 start_time_offset(
303 ParseColourTransformationError::InvalidStartTime.into(),
304 version,
305 ),
306 ),
307 preceded(
308 context(ParseColourTransformationError::MissingRed.into(), comma()),
309 context(
310 ParseColourTransformationError::InvalidRed.into(),
311 comma_field_type(),
312 ),
313 ),
314 preceded(
315 context(ParseColourTransformationError::MissingGreen.into(), comma()),
316 context(
317 ParseColourTransformationError::InvalidGreen.into(),
318 comma_field_type(),
319 ),
320 ),
321 preceded(
322 context(ParseColourTransformationError::MissingBlue.into(), comma()),
323 context(
324 ParseColourTransformationError::InvalidBlue.into(),
325 consume_rest_type(),
326 ),
327 ),
328 ))(s)?;
329
330 Ok(Some(ColourTransformation {
331 start_time,
332 red,
333 green,
334 blue,
335 }))
336 }
337}
338
339impl VersionedToString for ColourTransformation {
340 fn to_string(&self, version: Version) -> Option<String> {
341 if version < 14 {
342 Some(format!(
343 "{COLOUR_TRANSFORMATION_HEADER},{},{},{},{}",
344 time_to_string(self.start_time, version),
345 self.red,
346 self.green,
347 self.blue
348 ))
349 } else {
350 None
351 }
352 }
353}
354
355#[derive(Clone, Debug, Hash, PartialEq, Eq)]
356pub struct SpriteLegacy {
358 pub layer: LayerLegacy,
359 pub origin: OriginTypeLegacy,
360 pub file_name: FilePath,
361 pub position: Option<Position>,
362 pub commands: Vec<Command>,
363}
364
365pub const SPRITE_LEGACY_HEADER: &str = "4";
366
367impl VersionedFromStr for SpriteLegacy {
368 type Err = ParseSpriteLegacyError;
369
370 fn from_str(s: &str, version: Version) -> std::result::Result<Option<Self>, Self::Err> {
371 let (_, (layer, origin, (file_name, position))) = preceded(
372 tuple((
373 context(
374 ParseSpriteLegacyError::WrongEventType.into(),
375 tag(SPRITE_LEGACY_HEADER),
376 ),
377 context(ParseSpriteLegacyError::MissingLayer.into(), comma()),
378 )),
379 tuple((
380 context(
381 ParseSpriteLegacyError::InvalidLayer.into(),
382 comma_field_versioned_type(version),
383 ),
384 preceded(
385 context(ParseSpriteLegacyError::MissingOrigin.into(), comma()),
386 context(
387 ParseSpriteLegacyError::InvalidOrigin.into(),
388 comma_field_versioned_type(version),
389 ),
390 ),
391 preceded(
392 context(ParseSpriteLegacyError::MissingFileName.into(), comma()),
393 file_name_and_position(
394 ParseSpriteLegacyError::MissingX.into(),
395 ParseSpriteLegacyError::InvalidX.into(),
396 ParseSpriteLegacyError::MissingY.into(),
397 ParseSpriteLegacyError::InvalidY.into(),
398 ),
399 ),
400 )),
401 )(s)?;
402
403 Ok(Some(SpriteLegacy {
404 layer,
405 origin,
406 file_name,
407 position,
408 commands: Vec::new(),
409 }))
410 }
411}
412
413impl VersionedToString for SpriteLegacy {
414 fn to_string(&self, version: Version) -> Option<String> {
415 Some(format!(
416 "{SPRITE_LEGACY_HEADER},{},{},{}{}",
417 self.layer.to_string(version).unwrap(),
418 self.origin.to_string(version).unwrap(),
419 self.file_name.to_string(version).unwrap(),
420 position_str(&self.position),
421 ))
422 }
423}
424
425impl EventWithCommands for SpriteLegacy {
426 fn commands(&self) -> &[Command] {
427 &self.commands
428 }
429
430 fn commands_mut(&mut self) -> &mut Vec<Command> {
431 &mut self.commands
432 }
433
434 fn to_string_cmd(&self, version: Version) -> Option<String> {
435 self.to_string(version)
436 }
437}
438
439#[derive(Clone, Debug, Hash, PartialEq, Eq)]
440pub struct AnimationLegacy {
441 pub layer: LayerLegacy,
442 pub origin: OriginTypeLegacy,
443 pub file_name: FilePath,
444 pub position: Option<Position>,
445 pub commands: Vec<Command>,
446}
447
448pub const ANIMATION_LEGACY_HEADER: &str = "5";
449
450impl VersionedFromStr for AnimationLegacy {
451 type Err = ParseAnimationLegacyError;
452 fn from_str(s: &str, version: Version) -> std::result::Result<Option<Self>, Self::Err> {
453 let (_, (layer, origin, (file_name, position))) = preceded(
454 tuple((
455 context(
456 ParseAnimationLegacyError::WrongEventType.into(),
457 tag(ANIMATION_LEGACY_HEADER),
458 ),
459 context(ParseAnimationLegacyError::MissingLayer.into(), comma()),
460 )),
461 tuple((
462 context(
463 ParseAnimationLegacyError::InvalidLayer.into(),
464 comma_field_versioned_type(version),
465 ),
466 preceded(
467 context(ParseAnimationLegacyError::MissingOrigin.into(), comma()),
468 context(
469 ParseAnimationLegacyError::InvalidOrigin.into(),
470 comma_field_versioned_type(version),
471 ),
472 ),
473 preceded(
474 context(ParseAnimationLegacyError::MissingFileName.into(), comma()),
475 file_name_and_position(
476 ParseAnimationLegacyError::MissingX.into(),
477 ParseAnimationLegacyError::InvalidX.into(),
478 ParseAnimationLegacyError::MissingY.into(),
479 ParseAnimationLegacyError::InvalidY.into(),
480 ),
481 ),
482 )),
483 )(s)?;
484
485 Ok(Some(AnimationLegacy {
486 layer,
487 origin,
488 file_name,
489 position,
490 commands: Vec::new(),
491 }))
492 }
493}
494
495impl VersionedToString for AnimationLegacy {
496 fn to_string(&self, version: Version) -> Option<String> {
497 Some(format!(
498 "{ANIMATION_LEGACY_HEADER},{},{},{}{}",
499 self.layer.to_string(version).unwrap(),
500 self.origin.to_string(version).unwrap(),
501 self.file_name.to_string(version).unwrap(),
502 position_str(&self.position),
503 ))
504 }
505}
506
507impl EventWithCommands for AnimationLegacy {
508 fn commands(&self) -> &[Command] {
509 &self.commands
510 }
511 fn commands_mut(&mut self) -> &mut Vec<Command> {
512 &mut self.commands
513 }
514 fn to_string_cmd(&self, version: Version) -> Option<String> {
515 self.to_string(version)
516 }
517}
518
519#[derive(Clone, Debug, Hash, PartialEq, Eq)]
520pub struct SampleLegacy {
521 pub time: Decimal,
522 pub layer: LayerLegacy,
523 pub file_name: FilePath,
524 pub volume: Option<Volume>,
525 pub commands: Vec<Command>,
526}
527
528pub const SAMPLE_LEGACY_HEADER: &str = "6";
529
530impl VersionedFromStr for SampleLegacy {
531 type Err = ParseSampleLegacyError;
532 fn from_str(s: &str, version: Version) -> std::result::Result<Option<Self>, Self::Err> {
533 let (_, (time, layer, file_name, volume)) = preceded(
534 tuple((
535 context(
536 ParseSampleLegacyError::WrongEventType.into(),
537 tag(SAMPLE_LEGACY_HEADER),
538 ),
539 context(ParseSampleLegacyError::MissingTime.into(), comma()),
540 )),
541 tuple((
542 context(
543 ParseSampleLegacyError::InvalidTime.into(),
544 comma_field_type(),
545 ),
546 preceded(
547 context(ParseSampleLegacyError::MissingLayer.into(), comma()),
548 context(
549 ParseSampleLegacyError::InvalidLayer.into(),
550 comma_field_versioned_type(version),
551 ),
552 ),
553 preceded(
554 context(ParseSampleLegacyError::MissingFileName.into(), comma()),
555 comma_field().map(|f| f.into()),
556 ),
557 alt((
558 eof.map(|_| None),
559 preceded(
560 context(ParseSampleLegacyError::MissingVolume.into(), comma()),
561 context(
562 ParseSampleLegacyError::InvalidVolume.into(),
563 consume_rest_versioned_type(version),
564 ),
565 )
566 .map(Some),
567 )),
568 )),
569 )(s)?;
570
571 Ok(Some(SampleLegacy {
572 time,
573 layer,
574 file_name,
575 volume,
576 commands: Vec::new(),
577 }))
578 }
579}
580
581impl VersionedToString for SampleLegacy {
582 fn to_string(&self, version: Version) -> Option<String> {
583 Some(format!(
584 "{SAMPLE_LEGACY_HEADER},{},{},{}{}",
585 self.time,
586 self.layer.to_string(version).unwrap(),
587 self.file_name.to_string(version).unwrap(),
588 self.volume
589 .map(|v| format!(",{}", v.to_string(version).unwrap()))
590 .unwrap_or_default(),
591 ))
592 }
593}
594
595impl EventWithCommands for SampleLegacy {
596 fn commands(&self) -> &[Command] {
597 &self.commands
598 }
599
600 fn commands_mut(&mut self) -> &mut Vec<Command> {
601 &mut self.commands
602 }
603
604 fn to_string_cmd(&self, version: Version) -> Option<String> {
605 self.to_string(version)
606 }
607}