osu_beatmap_parser/
lib.rs1use 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}