osu_beatmap_parser/
lib.rs

1use crate::error::BeatmapParseError;
2use crate::section::colours::Colours;
3use crate::section::difficulty::DifficultySection;
4use crate::section::editor::EditorSection;
5use crate::section::events::Event;
6use crate::section::general::GeneralSection;
7use crate::section::hit_objects::HitObject;
8use crate::section::metadata::MetadataSection;
9use crate::section::timing_points::TimingPoint;
10use crate::section::CommaListOf;
11use crate::BeatmapParseError::SectionNotFound;
12use std::error::Error;
13use std::fs::File;
14use std::io::Read;
15use std::path::Path;
16use std::str::FromStr;
17use std::{fs, io};
18
19mod error;
20mod section;
21mod types;
22
23#[derive(Debug, Default)]
24pub struct BeatmapLevel {
25    pub general: GeneralSection,
26    pub editor: EditorSection,
27    pub metadata: MetadataSection,
28    pub difficulty: DifficultySection,
29    pub events: CommaListOf<Event>,
30    pub timing_points: CommaListOf<TimingPoint>,
31    pub colours: Colours,
32    pub hit_objects: CommaListOf<HitObject>,
33}
34
35impl BeatmapLevel {
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    pub fn parse(str: &str) -> Result<Self, BeatmapParseError> {
41        Self::from_str(str)
42    }
43    pub fn open(path: &Path) -> Result<Self, Box<dyn Error>> {
44        Ok(path.try_into()?)
45    }
46    pub fn save(&self, path: &Path) -> io::Result<()> {
47        Ok(fs::write(path, self.to_string())?)
48    }
49}
50
51impl TryFrom<File> for BeatmapLevel {
52    type Error = Box<dyn Error>;
53
54    fn try_from(mut value: File) -> Result<Self, Self::Error> {
55        let buf = &mut String::new();
56        value.read_to_string(buf)?;
57        Ok(BeatmapLevel::from_str(buf)?)
58    }
59}
60
61impl TryFrom<&Path> for BeatmapLevel {
62    type Error = Box<dyn Error>;
63
64    fn try_from(value: &Path) -> Result<Self, Self::Error> {
65        Ok(File::open(value)?.try_into()?)
66    }
67}
68
69impl FromStr for BeatmapLevel {
70    type Err = BeatmapParseError;
71
72    fn from_str(s: &str) -> Result<Self, Self::Err> {
73        let general_index = s.find("[General]").ok_or_else(|| SectionNotFound {
74            section: "General".to_string(),
75        })?;
76        let editor_index = s.find("[Editor]").ok_or_else(|| SectionNotFound {
77            section: "Editor".to_string(),
78        })?;
79        let metadata_index = s.find("[Metadata]").ok_or_else(|| SectionNotFound {
80            section: "Metadata".to_string(),
81        })?;
82        let difficulty_index = s.find("[Difficulty]").ok_or_else(|| SectionNotFound {
83            section: "Difficulty".to_string(),
84        })?;
85        let events_index = s.find("[Events]").ok_or_else(|| SectionNotFound {
86            section: "Events".to_string(),
87        })?;
88        let timing_points_index = s.find("[TimingPoints]").ok_or_else(|| SectionNotFound {
89            section: "TimingPoints".to_string(),
90        })?;
91        let colours_index = s.find("[Colours]").ok_or_else(|| SectionNotFound {
92            section: "Colours".to_string(),
93        })?;
94        let hit_objects_index = s.find("[HitObjects]").ok_or_else(|| SectionNotFound {
95            section: "HitObjects".to_string(),
96        })?;
97
98        let general_str = s[general_index..editor_index]
99            .strip_prefix("[General]")
100            .unwrap()
101            .trim();
102        let editor_str = s[editor_index..metadata_index]
103            .strip_prefix("[Editor]")
104            .unwrap()
105            .trim();
106        let metadata_str = s[metadata_index..difficulty_index]
107            .strip_prefix("[Metadata]")
108            .unwrap()
109            .trim();
110        let difficulty_str = s[difficulty_index..events_index]
111            .strip_prefix("[Difficulty]")
112            .unwrap()
113            .trim();
114        let events_str = s[events_index..timing_points_index]
115            .strip_prefix("[Events]")
116            .unwrap()
117            .trim();
118        let timing_points_str = s[timing_points_index..colours_index]
119            .strip_prefix("[TimingPoints]")
120            .unwrap()
121            .trim();
122        let colours_str = s[colours_index..hit_objects_index]
123            .strip_prefix("[Colours]")
124            .unwrap()
125            .trim();
126        let hit_objects_str = s[hit_objects_index..]
127            .strip_prefix("[HitObjects]")
128            .unwrap()
129            .trim();
130
131        Ok(BeatmapLevel {
132            general: general_str.parse()?,
133            editor: editor_str.parse()?,
134            metadata: metadata_str.parse()?,
135            difficulty: difficulty_str.parse()?,
136            events: events_str.parse()?,
137            timing_points: timing_points_str.parse()?,
138            colours: colours_str.parse()?,
139            hit_objects: hit_objects_str.parse()?,
140        })
141    }
142}
143
144impl ToString for BeatmapLevel {
145    fn to_string(&self) -> String {
146        format! {"osu file format v14\n\
147        \n\
148        [General]\n\
149        {}\n\
150        [Editor]\n\
151        {}\n\
152        [Metadata]\n\
153        {}\n\
154        [Difficulty]\n\
155        {}\n\
156        [Events]\n\
157        {}\n\
158        [TimingPoints]\n\
159        {}\n\
160        [Colours]\n\
161        {}\n\
162        [HitObjects]\n\
163        {}", self.general.to_string(), self.editor.to_string(), self.metadata.to_string(),
164        self.difficulty.to_string(), self.events.to_string(), self.timing_points.to_string(),
165        self.colours.to_string(), self.hit_objects.to_string()}
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use crate::BeatmapLevel;
172    use std::fs::File;
173    use std::io::Read;
174    use std::path::Path;
175
176    const TEST_BEATMAP_LEVEL_PATH: &'static str = "./assets/examples/test.osu";
177    const OUTPUT_BEATMAP_LEVEL_PATH: &'static str = "./assets/examples/test_output.osu";
178
179    #[test]
180    fn parse_save_beatmap_level() {
181        let mut file = File::open(TEST_BEATMAP_LEVEL_PATH).unwrap();
182        let buf = &mut String::new();
183        file.read_to_string(buf).unwrap();
184
185        let beatmap_level = BeatmapLevel::parse(buf).unwrap();
186        beatmap_level
187            .save(&Path::new(OUTPUT_BEATMAP_LEVEL_PATH))
188            .unwrap();
189    }
190}