1use crate::{
7 sm_msd::{self, MsdElement, MsdFile},
8 utils::ByteString,
9};
10use std::{
11 ffi::{OsStr, OsString},
12 fmt::Display,
13 fs, io,
14 path::{Path, PathBuf},
15 str::Utf8Error,
16};
17use thiserror::Error;
18
19#[derive(Error, Debug, PartialEq, Eq)]
21pub enum LoadError {
22 #[error("this was not valid sm ({0})")]
26 InvalidSM(String),
27
28 #[error("expected {0} to parse as utf-8, got Utf8Error {1}")]
31 UnexpectedNonUtf8(String, Utf8Error),
32}
33
34#[derive(Debug, Clone, PartialEq)]
36pub struct Bpm {
37 pub bpm: f64,
39 pub offset_beats: f64,
41}
42
43impl Bpm {
44 fn from_bytes(bytes: &[u8]) -> Result<Self, LoadError> {
45 let str = match std::str::from_utf8(bytes) {
46 Ok(str) => str,
47 Err(utf8err) => return Err(LoadError::UnexpectedNonUtf8("BPM".into(), utf8err)),
48 };
49
50 let elements = str.split('=').collect::<Vec<&str>>();
51
52 if elements.len() < 2 {
53 return Err(LoadError::InvalidSM(format!(
54 "Invalid SM BPM string '{}'.",
55 str
56 )));
57 }
58
59 let (offset, bpm) = (elements[0], elements[1]);
60
61 let (offset_beats, _) = match lexical::parse_partial(offset) {
62 Ok(v) => v,
63 Err(_) => {
64 return Err(LoadError::InvalidSM(format!(
65 "Failed to parse {offset} as a float."
66 )))
67 }
68 };
69
70 let (bpm, _) = match lexical::parse_partial(bpm) {
71 Ok(v) => v,
72 Err(_) => {
73 return Err(LoadError::InvalidSM(format!(
74 "Failed to parse {bpm} as a float."
75 )))
76 }
77 };
78
79 if bpm <= 0.0 {
80 return Err(LoadError::InvalidSM(format!("BPM was negative ({bpm})")));
81 }
82
83 Ok(Bpm { bpm, offset_beats })
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct SongMetadata {
90 pub offset_secs: Option<f64>,
93
94 pub bpms: Vec<Bpm>,
96
97 pub subtitle: Option<String>,
99 pub artist: String,
101 pub title: String,
103 pub music: Option<OsString>,
111}
112
113#[derive(Debug, Clone)]
115pub struct Chart {
116 pub tags: MsdFile,
118
119 pub song_info: SongMetadata,
121 pub chart_data: ChartData,
123 pub path: PathBuf,
125}
126
127macro_rules! msd_tag_fallback {
128 ($metadata: expr, $tag: expr, $fallback: expr) => {{
129 match $metadata.first_tag_first_val($tag) {
130 Some(slice) => String::from_utf8_lossy(&slice).into_owned(),
131 None => $fallback.to_owned(),
132 }
133 }};
134}
135
136macro_rules! msd_tag {
137 ($metadata: expr, $tag: expr) => {{
138 match $metadata.first_tag_first_val($tag) {
139 Some(slice) => Some(String::from_utf8_lossy(&slice).into_owned()),
140 None => None,
141 }
142 }};
143}
144
145impl Chart {
146 fn infer_author(path: impl AsRef<Path>) -> Option<String> {
174 let path = path.as_ref().clone();
175
176 let name = path.parent()?.file_name()?;
177 let name = OsStr::to_string_lossy(name);
178
179 use regex::Regex;
180
181 let standard = Regex::new(r"\((.+)\) *$").expect("Invalid standard regex.");
182 let square = Regex::new(r"\[(.+)\] *$").expect("Invalid square regex.");
183 let std_start = Regex::new(r"^ *\((.+)\)").expect("Invalid std_start regex.");
184 let sqr_start = Regex::new(r"^ *\[(.+)\]").expect("Invalid sqr_start regex.");
185
186 if let Some(m) = standard.captures(&name) {
187 return m.get(1).map(|f| f.as_str().trim().to_owned());
188 }
189
190 if let Some(m) = square.captures(&name) {
191 return m.get(1).map(|f| f.as_str().trim().to_owned());
192 }
193
194 if let Some(m) = std_start.captures(&name) {
195 return m.get(1).map(|f| f.as_str().trim().to_owned());
196 }
197
198 if let Some(m) = sqr_start.captures(&name) {
199 return m.get(1).map(|f| f.as_str().trim().to_owned());
200 }
201
202 None
203 }
204
205 fn look_for_audio(path: impl AsRef<Path>) -> Option<OsString> {
211 let dir = path.as_ref().parent()?;
212
213 let entries = fs::read_dir(dir).ok()?;
214
215 for entry in entries.flatten() {
216 match entry.path().extension().and_then(OsStr::to_str) {
217 Some("ogg" | "mp3" | "wav" | "oga") => return Some(entry.file_name()),
219 _ => continue,
220 }
221 }
222
223 None
224 }
225}
226
227pub fn from_path(sm_path: impl AsRef<Path>) -> io::Result<Result<Vec<Chart>, LoadError>> {
236 let bytes = fs::read(&sm_path)?;
237
238 Ok(from_bytes(&bytes, sm_path))
239}
240
241pub fn from_bytes(bytes: &[u8], sm_path: impl AsRef<Path>) -> Result<Vec<Chart>, LoadError> {
251 let (metadata, charts) = parse(bytes);
252
253 let bpms: Vec<Bpm> = match metadata.first_tag_first_val("BPMS") {
254 Some(bpm_str) => parse_bpms(&bpm_str).unwrap_or(vec![Bpm {
255 bpm: 60.0,
256 offset_beats: 0.0,
257 }]),
258 None => vec![Bpm {
259 bpm: 60.0,
260 offset_beats: 0.0,
261 }],
262 };
263
264 let offset = match metadata.first_tag_first_val("OFFSET") {
265 Some(v) => {
266 let str = match std::str::from_utf8(&v) {
267 Ok(s) => s,
268 Err(err) => return Err(LoadError::UnexpectedNonUtf8("OFFSET".into(), err)),
269 };
270
271 match lexical::parse_partial::<f64, &str>(str) {
272 Ok((float, _)) => {
273 if float == 0.0 {
276 None
277 } else {
278 Some(-1.0 * float)
282 }
283 }
284
285 Err(_) => None,
287 }
288 }
289 None => None,
290 };
291
292 let music_path = metadata.first_tag_first_val("MUSIC").map(|bytes| {
293 #[cfg(unix)]
297 {
298 use std::os::unix::ffi::OsStrExt;
299
300 OsStr::from_bytes(&bytes).to_owned()
301 }
302
303 #[cfg(windows)]
304 {
305 use std::os::windows::ffi::OsStrExt;
306
307 OsString::from_wide(&bytes)
310 }
311 });
312
313 let music_path = music_path.map(|os_str| {
314 let path = sm_path.as_ref().to_path_buf();
315 let path = path.join(&os_str);
316
317 if path.exists() {
318 os_str
319 } else {
320 match Chart::look_for_audio(&sm_path) {
323 Some(data) => data,
324 None => os_str,
327 }
328 }
329 });
330
331 let song_info = SongMetadata {
332 artist: msd_tag_fallback!(metadata, "ARTIST", "Unknown Artist"),
333 title: match metadata.first_tag_first_val("TITLE") {
334 Some(v) => String::from_utf8_lossy(&v).into_owned(),
335 None => {
336 let mut path = sm_path.as_ref().to_path_buf();
340
341 path.pop();
342
343 match path.file_name() {
344 Some(name) => name.to_string_lossy().into_owned(),
345 None => "Untitled Song".to_owned(),
346 }
347 }
348 },
349 subtitle: msd_tag!(metadata, "SUBTITLE"),
350 bpms,
351 music: music_path,
352 offset_secs: offset,
353 };
354
355 let mut ok_charts = vec![];
356
357 for chart in charts {
358 let mut chart = chart?;
360
361 if chart.author.is_empty() || &*chart.author == b"Copied From" || &*chart.author == b"Blank"
365 {
366 if let Some(inferred_name) = Chart::infer_author(&sm_path) {
367 chart.author = inferred_name.as_bytes().into();
368 }
369 }
370
371 let full_file = Chart {
372 tags: metadata.clone(),
373 song_info: song_info.clone(),
374 chart_data: chart,
375 path: sm_path.as_ref().to_path_buf(),
376 };
377
378 ok_charts.push(full_file);
379 }
380
381 Ok(ok_charts)
382}
383
384#[derive(Debug, Clone, PartialEq, Eq)]
392#[allow(missing_docs)]
393pub enum StepsType {
394 DanceSingle,
395 DanceDouble,
396 DanceCouple,
397 DanceSolo,
398 DanceThreepanel,
399 DanceRoutine,
400 PumpSingle,
401 PumpHalfDouble,
402 PumpDouble,
403 PumpCouple,
404 PumpRoutine,
405 Kb7Single,
406 Ez2Single,
407 Ez2Double,
408 Ez2Real,
409 ParaSingle,
410 Ds3ddxSingle,
411 BmSingle5,
412 BmVersus5,
413 BmDouble5,
414 BmSingle7,
415 BmVersus7,
416 BmDouble7,
417 ManiaxSingle,
418 ManiaxDouble,
419 TechnoSingle4,
420 TechnoSingle5,
421 TechnoSingle8,
422 TechnoDouble4,
423 TechnoDouble5,
424 TechnoDouble8,
425 PnmFive,
426 PnmNine,
427 LightsCabinet,
428 KickboxHuman,
429 KickboxQuadarm,
430 KickboxInsect,
431 KickboxArachnid,
432
433 Other(ByteString),
435}
436
437impl Display for StepsType {
438 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
439 let str = match self {
440 StepsType::DanceSingle => "dance-single".into(),
441 StepsType::DanceDouble => "dance-double".into(),
442 StepsType::DanceCouple => "dance-couple".into(),
443 StepsType::DanceSolo => "dance-solo".into(),
444 StepsType::DanceThreepanel => "dance-threepanel".into(),
445 StepsType::DanceRoutine => "dance-routine".into(),
446 StepsType::PumpSingle => "pump-single".into(),
447 StepsType::PumpHalfDouble => "pump-halfdouble".into(),
448 StepsType::PumpDouble => "pump-double".into(),
449 StepsType::PumpCouple => "pump-couple".into(),
450 StepsType::PumpRoutine => "pump-routine".into(),
451 StepsType::Kb7Single => "kb7-single".into(),
452 StepsType::Ez2Single => "ez2-single".into(),
453 StepsType::Ez2Double => "ez2-double".into(),
454 StepsType::Ez2Real => "ez2-real".into(),
455 StepsType::ParaSingle => "para-single".into(),
456 StepsType::Ds3ddxSingle => "ds3ddx-single".into(),
457 StepsType::BmSingle5 => "bm-single5".into(),
458 StepsType::BmVersus5 => "bm-versus5".into(),
459 StepsType::BmDouble5 => "bm-double5".into(),
460 StepsType::BmSingle7 => "bm-single7".into(),
461 StepsType::BmVersus7 => "bm-versus7".into(),
462 StepsType::BmDouble7 => "bm-double7".into(),
463 StepsType::ManiaxSingle => "maniax-single".into(),
464 StepsType::ManiaxDouble => "maniax-double".into(),
465 StepsType::TechnoSingle4 => "techno-single4".into(),
466 StepsType::TechnoSingle5 => "techno-single5".into(),
467 StepsType::TechnoSingle8 => "techno-single8".into(),
468 StepsType::TechnoDouble4 => "techno-double4".into(),
469 StepsType::TechnoDouble5 => "techno-double5".into(),
470 StepsType::TechnoDouble8 => "techno-double8".into(),
471 StepsType::PnmFive => "pnm-five".into(),
472 StepsType::PnmNine => "pnm-nine".into(),
473 StepsType::LightsCabinet => "lights-cabinet".into(),
474 StepsType::KickboxHuman => "kickbox-human".into(),
475 StepsType::KickboxQuadarm => "kickbox-quadarm".into(),
476 StepsType::KickboxInsect => "kickbox-insect".into(),
477 StepsType::KickboxArachnid => "kickbox-arachnid".into(),
478 StepsType::Other(a) => String::from_utf8_lossy(a).into_owned(),
479 };
480
481 f.write_str(&str)
482 }
483}
484
485impl StepsType {
486 fn from_bytes(bytes: &ByteString) -> Self {
487 match &**bytes {
488 b"dance-single" => StepsType::DanceSingle,
489 b"dance-double" => StepsType::DanceDouble,
490 b"dance-couple" => StepsType::DanceCouple,
491 b"dance-solo" => StepsType::DanceSolo,
492 b"dance-threepanel" => StepsType::DanceThreepanel,
493 b"dance-routine" => StepsType::DanceRoutine,
494 b"pump-single" => StepsType::PumpSingle,
495 b"pump-halfdouble" => StepsType::PumpHalfDouble,
496 b"pump-double" => StepsType::PumpDouble,
497 b"pump-couple" => StepsType::PumpCouple,
498 b"pump-routine" => StepsType::PumpRoutine,
499 b"kb7-single" => StepsType::Kb7Single,
500 b"ez2-single" => StepsType::Ez2Single,
501 b"ez2-double" => StepsType::Ez2Double,
502 b"ez2-real" => StepsType::Ez2Real,
503 b"para-single" => StepsType::ParaSingle,
504 b"ds3ddx-single" => StepsType::Ds3ddxSingle,
505 b"bm-single5" => StepsType::BmSingle5,
506 b"bm-versus5" => StepsType::BmVersus5,
507 b"bm-double5" => StepsType::BmDouble5,
508 b"bm-single7" => StepsType::BmSingle7,
509 b"bm-versus7" => StepsType::BmVersus7,
510 b"bm-double7" => StepsType::BmDouble7,
511 b"maniax-single" => StepsType::ManiaxSingle,
512 b"maniax-double" => StepsType::ManiaxDouble,
513 b"techno-single4" => StepsType::TechnoSingle4,
514 b"techno-single5" => StepsType::TechnoSingle5,
515 b"techno-single8" => StepsType::TechnoSingle8,
516 b"techno-double4" => StepsType::TechnoDouble4,
517 b"techno-double5" => StepsType::TechnoDouble5,
518 b"techno-double8" => StepsType::TechnoDouble8,
519 b"pnm-five" => StepsType::PnmFive,
520 b"pnm-nine" => StepsType::PnmNine,
521 b"lights-cabinet" => StepsType::LightsCabinet,
522 b"kickbox-human" => StepsType::KickboxHuman,
523 b"kickbox-quadarm" => StepsType::KickboxQuadarm,
524 b"kickbox-insect" => StepsType::KickboxInsect,
525 b"kickbox-arachnid" => StepsType::KickboxArachnid,
526 _ => StepsType::Other(bytes.clone()),
527 }
528 }
529}
530
531#[derive(Debug, PartialEq, Eq, Clone)]
537pub enum Difficulty {
538 Beginner,
540 Easy,
542 Medium,
544 Hard,
546 Challenge,
548 Edit(ByteString),
551}
552
553impl Display for Difficulty {
554 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
555 use Difficulty::*;
556
557 let str = match self {
558 Beginner => "Beginner".into(),
559 Easy => "Easy".into(),
560 Medium => "Medium".into(),
561 Hard => "Hard".into(),
562 Challenge => "Challenge".into(),
563 Edit(txt) => format!("Edit {}", String::from_utf8_lossy(txt)),
564 };
565
566 write!(f, "{str}")
567 }
568}
569
570#[derive(Debug, Clone, PartialEq, Eq)]
573pub struct ChartData {
574 pub steps_type: StepsType,
576 pub author: ByteString,
579 pub difficulty: Difficulty,
581
582 pub level: usize,
584
585 pub notedata: Vec<Measure>,
588}
589
590#[derive(Debug, PartialEq, Clone, Eq)]
592pub enum NoteVariant {
593 Note,
595 HoldStart,
597 RollStart,
599 HoldOrRollEnd,
601
602 AutoKeysound,
604 Lift,
606 Fake,
608 Mine,
610
611 Unknown(u8),
614}
615
616#[derive(Debug, PartialEq, Clone, Eq)]
620pub struct Event {
621 pub row: usize,
624
625 pub column: usize,
632
633 pub variant: NoteVariant,
635}
636
637#[derive(Debug, PartialEq, Clone, Eq)]
640pub struct Measure {
641 pub size: usize,
643 pub events: Vec<Event>,
645}
646
647fn parse_notedata(raw_notedata: &[u8]) -> Vec<Measure> {
648 let mut measures = vec![];
649
650 for measure in raw_notedata.split(|char| *char == b',') {
651 let mut size = 0;
652 let mut events = vec![];
653
654 for row in measure.split(|char| *char == b'\n') {
655 let row = row.trim_ascii();
656
657 if row.is_empty() {
658 continue;
659 }
660
661 let mut skip_until_closebrck = false;
664 let mut index = 0;
665
666 for ch in row.iter() {
667 if *ch == b'[' {
677 skip_until_closebrck = true;
678 continue;
679 }
680
681 if *ch == b']' {
682 skip_until_closebrck = false;
683 continue;
684 }
685
686 if skip_until_closebrck {
687 continue;
688 }
689
690 let variant = match ch {
691 b'0' => {
693 index += 1;
694 continue;
695 }
696 b'1' => NoteVariant::Note,
697 b'2' => NoteVariant::HoldStart,
698 b'3' => NoteVariant::HoldOrRollEnd,
699 b'4' => NoteVariant::RollStart,
700 b'M' => NoteVariant::Mine,
701 b'K' => NoteVariant::AutoKeysound,
702 b'L' => NoteVariant::Lift,
703 b'F' => NoteVariant::Fake,
704 ch => NoteVariant::Unknown(*ch),
705 };
706
707 events.push(Event {
708 row: size,
709 column: index,
710 variant,
711 });
712
713 index += 1;
714 }
715
716 size += 1;
717 }
718
719 if size == 0 {
720 continue;
722 }
723
724 measures.push(Measure { size, events })
725 }
726
727 measures
728}
729
730fn parse(sm: &[u8]) -> (MsdFile, Vec<Result<ChartData, LoadError>>) {
733 let msd_file = sm_msd::from_bytes(sm);
734
735 let charts = msd_file
736 .all_with_tag("NOTES")
737 .iter()
738 .map(|el| parse_notes(el))
739 .collect();
740
741 (msd_file, charts)
742}
743
744fn parse_bpms(bpms: &[u8]) -> Result<Vec<Bpm>, LoadError> {
745 let mut bpm_vec = vec![];
746
747 for bpm in bpms.split(|by| *by == b',') {
748 match Bpm::from_bytes(bpm.trim_ascii()) {
749 Ok(v) => bpm_vec.push(v),
750 Err(err) => return Err(err),
751 }
752 }
753
754 Ok(bpm_vec)
755}
756
757fn parse_notes(el: &MsdElement) -> Result<ChartData, LoadError> {
758 if el.values.len() < 6 {
768 return Err(LoadError::InvalidSM(format!(
769 "Invalid amount of fields inside #NOTES. Got {}, expected at least 6.",
770 el.values.len()
771 )));
772 }
773
774 let fields = &el.values;
775
776 let steps_type = StepsType::from_bytes(&fields[0]);
777
778 let author = &fields[1];
779 let diff = &fields[2];
780
781 let difficulty = match diff.to_ascii_lowercase().as_slice() {
782 b"beginner" => Difficulty::Beginner,
783 b"easy" | b"basic" | b"light" => Difficulty::Easy,
784 b"medium" | b"another" | b"trick" | b"standard" | b"difficult" => Difficulty::Medium,
785 b"hard" | b"ssr" | b"maniac" | b"heavy" => Difficulty::Hard,
786 b"challenge" | b"expert" | b"oni" => Difficulty::Challenge,
787 b"edit" => Difficulty::Edit(author.clone()),
788 d => {
789 return Err(LoadError::InvalidSM(format!(
790 "Unknown difficulty {}",
791 String::from_utf8_lossy(d),
792 )))
793 }
794 };
795
796 let level = String::from_utf8_lossy(&fields[3]).parse().unwrap_or(1);
797
798 let raw_notedata = &fields[5];
799
800 let notedata = parse_notedata(raw_notedata);
801
802 Ok(ChartData {
803 steps_type,
804 author: author.clone(),
805 difficulty,
806 level,
807 notedata,
808 })
809}
810
811#[cfg(test)]
812mod tests {
813 use pretty_assertions::assert_eq;
814
815 use super::*;
816
817 #[test]
818 fn bpms() {
819 assert_eq!(
820 parse_bpms(b"0.000=104.03"),
821 Ok(vec![Bpm {
822 bpm: 104.03,
823 offset_beats: 0.0
824 }])
825 );
826
827 assert_eq!(
828 parse_bpms(b"0.000=104.03,1.000=400"),
829 Ok(vec![
830 Bpm {
831 bpm: 104.03,
832 offset_beats: 0.0
833 },
834 Bpm {
835 bpm: 400.00,
836 offset_beats: 1.0
837 }
838 ])
839 );
840
841 assert_eq!(
842 parse_bpms(b"0.000=-104.03"),
843 Err(LoadError::InvalidSM("BPM was negative (-104.03)".into()))
844 );
845 }
846
847 #[test]
848 fn bpm_partial() {
849 assert_eq!(
850 parse_bpms(b"0.000=123.456.789"),
851 Ok(vec![Bpm {
852 bpm: 123.456,
853 offset_beats: 0.0
854 }])
855 );
856 }
857
858 #[test]
859 fn load_notes() {
860 assert_eq!(
861 parse_notes(&MsdElement {
862 tag: Box::new(*b"NOTES"),
863 values: vec![
864 Box::new(*b"dance-single"),
865 Box::new(*b"Author"),
866 Box::new(*b"Hard"),
867 Box::new(*b"1"),
868 Box::new(*b"nonsense groove"),
869 Box::new(
870 *b"1000
8710100
8720010
8730001,
874M000
87500000
8761234
877LKMF"
878 )
879 ]
880 }),
881 Ok(ChartData {
882 steps_type: StepsType::DanceSingle,
883 author: Box::new(*b"Author"),
884 difficulty: Difficulty::Hard,
885 level: 1,
886 notedata: vec![
887 Measure {
888 size: 4,
889 events: vec![
890 Event {
891 column: 0,
892 row: 0,
893 variant: NoteVariant::Note
894 },
895 Event {
896 column: 1,
897 row: 1,
898 variant: NoteVariant::Note
899 },
900 Event {
901 column: 2,
902 row: 2,
903 variant: NoteVariant::Note
904 },
905 Event {
906 column: 3,
907 row: 3,
908 variant: NoteVariant::Note
909 },
910 ]
911 },
912 Measure {
913 size: 4,
914 events: vec![
915 Event {
916 column: 0,
917 row: 0,
918 variant: NoteVariant::Mine
919 },
920 Event {
921 column: 0,
922 row: 2,
923 variant: NoteVariant::Note
924 },
925 Event {
926 column: 1,
927 row: 2,
928 variant: NoteVariant::HoldStart
929 },
930 Event {
931 column: 2,
932 row: 2,
933 variant: NoteVariant::HoldOrRollEnd
934 },
935 Event {
936 column: 3,
937 row: 2,
938 variant: NoteVariant::RollStart
939 },
940 Event {
941 column: 0,
942 row: 3,
943 variant: NoteVariant::Lift
944 },
945 Event {
946 column: 1,
947 row: 3,
948 variant: NoteVariant::AutoKeysound
949 },
950 Event {
951 column: 2,
952 row: 3,
953 variant: NoteVariant::Mine
954 },
955 Event {
956 column: 3,
957 row: 3,
958 variant: NoteVariant::Fake
959 },
960 ]
961 }
962 ]
963 })
964 )
965 }
966
967 #[test]
968 fn load_notes_obscurekeysounds() {
969 assert_eq!(
970 parse_notes(&MsdElement {
971 tag: Box::new(*b"NOTES"),
972 values: vec![
973 Box::new(*b"dance-single"),
974 Box::new(*b"Author"),
975 Box::new(*b"Hard"),
976 Box::new(*b"1"),
977 Box::new(*b"nonsense groove"),
978 Box::new(
979 *b"1000
9800100[1]
981001[100000]0
9820001[1,
983[1]M000
98400[1]000
985123[999}>)]4
986LKMF"
987 )
988 ]
989 }),
990 Ok(ChartData {
991 steps_type: StepsType::DanceSingle,
992 author: Box::new(*b"Author"),
993 difficulty: Difficulty::Hard,
994 level: 1,
995 notedata: vec![
996 Measure {
997 size: 4,
998 events: vec![
999 Event {
1000 column: 0,
1001 row: 0,
1002 variant: NoteVariant::Note
1003 },
1004 Event {
1005 column: 1,
1006 row: 1,
1007 variant: NoteVariant::Note
1008 },
1009 Event {
1010 column: 2,
1011 row: 2,
1012 variant: NoteVariant::Note
1013 },
1014 Event {
1015 column: 3,
1016 row: 3,
1017 variant: NoteVariant::Note
1018 },
1019 ]
1020 },
1021 Measure {
1022 size: 4,
1023 events: vec![
1024 Event {
1025 column: 0,
1026 row: 0,
1027 variant: NoteVariant::Mine
1028 },
1029 Event {
1030 column: 0,
1031 row: 2,
1032 variant: NoteVariant::Note
1033 },
1034 Event {
1035 column: 1,
1036 row: 2,
1037 variant: NoteVariant::HoldStart
1038 },
1039 Event {
1040 column: 2,
1041 row: 2,
1042 variant: NoteVariant::HoldOrRollEnd
1043 },
1044 Event {
1045 column: 3,
1046 row: 2,
1047 variant: NoteVariant::RollStart
1048 },
1049 Event {
1050 column: 0,
1051 row: 3,
1052 variant: NoteVariant::Lift
1053 },
1054 Event {
1055 column: 1,
1056 row: 3,
1057 variant: NoteVariant::AutoKeysound
1058 },
1059 Event {
1060 column: 2,
1061 row: 3,
1062 variant: NoteVariant::Mine
1063 },
1064 Event {
1065 column: 3,
1066 row: 3,
1067 variant: NoteVariant::Fake
1068 },
1069 ]
1070 }
1071 ]
1072 })
1073 )
1074 }
1075
1076 #[test]
1077 fn infer_author_normal() {
1078 assert_eq!(
1079 Chart::infer_author("Songs/Tachyon Epsilon/Hello (Kommisar)/chart.sm"),
1080 Some("Kommisar".into())
1081 );
1082 }
1083
1084 #[test]
1085 fn infer_author_square() {
1086 assert_eq!(
1087 Chart::infer_author("Songs/Tachyon Epsilon/Hello [Kommisar]/chart.sm"),
1088 Some("Kommisar".into())
1089 );
1090 }
1091
1092 #[test]
1093 fn infer_author_start() {
1094 assert_eq!(
1095 Chart::infer_author("Songs/Tachyon Epsilon/(Kommisar) Hello/chart.sm"),
1096 Some("Kommisar".into())
1097 );
1098 }
1099
1100 #[test]
1101 fn infer_author_sq_start() {
1102 assert_eq!(
1103 Chart::infer_author("Songs/Tachyon Epsilon/[Kommisar] Hello/chart.sm"),
1104 Some("Kommisar".into())
1105 );
1106 }
1107
1108 #[test]
1109 fn infer_author_space() {
1110 assert_eq!(
1111 Chart::infer_author("Songs/Tachyon Epsilon/[ Kommisar ] Hello/chart.sm"),
1112 Some("Kommisar".into())
1113 );
1114 }
1115
1116 #[test]
1117 fn infer_author_space2() {
1118 assert_eq!(
1119 Chart::infer_author("Songs/Tachyon Epsilon/( Kommisar ) Hello/chart.sm"),
1120 Some("Kommisar".into())
1121 );
1122 }
1123}