1use bitflags::bitflags;
2use num::clamp;
3use std::{
4 fs::File,
5 io::{self, BufRead},
6 path::Path,
7};
8
9use crate::enums::GameMode;
10
11use super::qua::{
12 CustomAudioSampleInfo, HitObjectInfo, QuaverMap, SliderVelocityInfo, SoundEffectInfo,
13 TimingPointInfo,
14};
15
16#[derive(Default)]
17pub struct OsuBeatmap {
18 pub original_file_name: String,
19 pub is_valid: bool,
20 pub peppy_file_format: String,
21
22 pub audio_file_name: String,
24 pub audio_lead_in: i32,
25 pub preview_time: i32,
26 pub countdown: i32,
27 pub sample_set: String,
28 pub stack_leniency: f32,
29 pub mode: i32,
30 pub letterbox_in_breaks: i32,
31 pub special_style: i32,
32 pub widescreen_storyboard: i32,
33
34 pub bookmarks: String,
36 pub distance_spacing: f32,
37 pub beat_divisor: i32,
38 pub grid_size: i32,
39 pub timeline_zoom: i32,
40
41 pub title: String,
43 pub title_unicode: String,
44 pub artist: String,
45 pub artist_unicode: String,
46 pub creator: String,
47 pub version: String,
48 pub source: String,
49 pub tags: String,
50 pub beatmap_id: i32,
51 pub beatmap_set_id: i32,
52
53 pub hp_drain_rate: f32,
55 pub key_count: i32,
56 pub overall_difficulty: f32,
57 pub approach_rate: f32,
58 pub slider_multiplier: f32,
59 pub slider_tick_rate: f32,
60
61 pub background: String,
63 pub sound_effects: Vec<OsuSampleInfo>,
64
65 pub timing_points: Vec<OsuTimingPoint>,
67
68 pub hit_objects: Vec<OsuHitObject>,
70
71 pub custom_audio_samples: Vec<String>,
72}
73
74impl OsuBeatmap {
75 pub fn from_path(file_path: &str) -> Self {
76 let mut self_ = Self::default();
77
78 if !Path::new(file_path).exists() {
79 self_.is_valid = false;
80 }
81
82 self_.is_valid = true;
83 self_.original_file_name = file_path.to_string();
84
85 let mut section: &str = "";
86
87 if let Ok(lines) = Self::read_lines(&self_.original_file_name) {
88 for raw_line in lines {
89 let raw_line = raw_line.unwrap();
90 if raw_line.trim().is_empty()
91 || raw_line.starts_with("//")
92 || raw_line.starts_with(' ')
93 || raw_line.starts_with('_')
94 {
95 continue;
96 }
97
98 let line = Self::strip_comments(raw_line.as_str());
99
100 section = match line.trim() {
101 "[General]" => "[General]",
102 "[Editor]" => "[Editor]",
103 "[Metadata]" => "[Metadata]",
104 "[Difficulty]" => "[Difficulty]",
105 "[Events]" => "[Events]",
106 "[TimingPoints]" => "[TimingPoints]",
107 "[HitObjects]" => "[HitObjects]",
108 "[Colours]" => "[Colours]",
109 _ => section,
110 };
111
112 if line.starts_with("osu file format") {
113 self_.peppy_file_format = line.to_string();
114 }
115
116 if section.eq("[General]") && line.contains(':') {
117 let key = &line[..line.find(':').unwrap()];
118 let value = line.split(':').last().unwrap().trim();
119 match key.trim() {
120 "AudioFilename" => self_.audio_file_name = value.parse().unwrap(),
121 "AudioLeadIn" => self_.audio_lead_in = value.parse().unwrap(),
122 "PreviewTime" => self_.preview_time = value.parse().unwrap(),
123 "Countdown" => self_.countdown = value.parse().unwrap(),
124 "SampleSet" => self_.sample_set = value.parse().unwrap(),
125 "StackLeniency" => self_.stack_leniency = value.parse().unwrap(),
126 "Mode" => {
127 self_.mode = value.parse().unwrap();
128 if self_.mode != 3 {
129 self_.is_valid = false
130 }
131 }
132 "LetterboxInBreaks" => self_.letterbox_in_breaks = value.parse().unwrap(),
133 "SpecialStyle" => self_.special_style = value.parse().unwrap(),
134 "WidescreenStoryboard" => {
135 self_.widescreen_storyboard = value.parse().unwrap()
136 }
137 _ => (),
138 }
139 }
140
141 if section.eq("[Editor]") && line.contains(':') {
142 let key = &line[..line.find(':').unwrap()];
143 let value = line.split(':').last().unwrap().trim();
144
145 match key.trim() {
146 "Bookmarks" => self_.bookmarks = value.parse().unwrap(),
147 "DistanceSpacing" => self_.distance_spacing = value.parse().unwrap(),
148 "BeatDivisor" => self_.beat_divisor = value.parse().unwrap(),
149 "GridSize" => self_.grid_size = value.parse().unwrap(),
150 "TimelineZoom" => self_.timeline_zoom = value.parse().unwrap(),
151 _ => (),
152 }
153 }
154
155 if section.eq("[Metadata]") && line.contains(':') {
156 let key = &line[..line.find(':').unwrap()];
157 let value = line.split(':').last().unwrap().trim();
158
159 match key.trim() {
160 "Title" => self_.title = value.parse().unwrap(),
161 "TitleUnicode" => self_.title_unicode = value.parse().unwrap(),
162 "Artist" => self_.artist = value.parse().unwrap(),
163 "ArtistUnicode" => self_.artist_unicode = value.parse().unwrap(),
164 "Creator" => self_.creator = value.parse().unwrap(),
165 "Version" => self_.version = value.parse().unwrap(),
166 "Source" => self_.source = value.parse().unwrap(),
167 "Tags" => self_.tags = value.parse().unwrap(),
168 "BeatmapID" => self_.beatmap_id = value.parse().unwrap(),
169 "BeatmapSetID" => self_.beatmap_set_id = value.parse().unwrap(),
170 _ => (),
171 }
172 }
173
174 if section.eq("[Difficulty]") && line.contains(':') {
175 let key = &line[..line.find(':').unwrap()];
176 let value = line.split(':').last().unwrap().trim();
177
178 match key.trim() {
179 "HPDrainRate" => self_.hp_drain_rate = value.parse().unwrap(),
180 "CircleSize" => {
181 let key_count = value.parse().unwrap();
182
183 if key_count != 4 && key_count != 7 && key_count != 5 && key_count != 8
184 {
185 self_.is_valid = false;
186 }
187
188 self_.key_count = key_count;
189 }
190 "OverallDifficulty" => self_.overall_difficulty = value.parse().unwrap(),
191 "ApproachRate" => self_.approach_rate = value.parse().unwrap(),
192 "SliderMultiplier" => self_.slider_multiplier = value.parse().unwrap(),
193 "SliderTickRate" => self_.slider_tick_rate = value.parse().unwrap(),
194 _ => (),
195 }
196 }
197
198 if section.eq("[Events]") {
199 let values: Vec<&str> = line.split(',').collect();
200
201 if line.to_lowercase().contains("png")
202 || line.to_lowercase().contains("jpg")
203 || line.to_lowercase().contains("jpeg")
204 {
205 self_.background = values[2].replace('\"', "");
206 }
207
208 if values[0] == "Sample" || values[0] == "5" {
209 self_.sound_effects.push(OsuSampleInfo {
216 start_time: values[1].parse().unwrap(),
217 layer: values[2].parse().unwrap(),
218 volume: std::cmp::max(
219 0,
220 std::cmp::min(
221 100,
222 if values.len() >= 5 {
223 values[4].parse().unwrap()
224 } else {
225 100
226 },
227 ),
228 ),
229 sample: 0,
230 })
231 }
232 }
233
234 if section.eq("[TimingPoints]") && line.contains(',') {
235 let values: Vec<&str> = line.split(',').collect();
236
237 let ms_per_beat: f32 = values[1].parse().unwrap();
238
239 let timing_point = OsuTimingPoint {
240 offset: values[0].parse().unwrap(),
241 milliseconds_per_beat: ms_per_beat,
242 sample_type: values[3].parse().unwrap(),
248 sample_set: values[4].parse().unwrap(),
249 volume: values[5].parse().unwrap(),
250 inherited: values[6].parse().unwrap(),
251 kiai_mode: values[7].parse().unwrap(),
252 };
253
254 self_.timing_points.push(timing_point);
255 }
256
257 if section.eq("[HitObjects]") && line.contains(',') {
258 let values: Vec<&str> = line.split(',').collect();
259
260 let mut hit_object = OsuHitObject {
261 x: values[0].parse().unwrap(),
262 y: values[1].parse().unwrap(),
263 start_time: values[2].parse().unwrap(),
264 type_: HitObjectType::from_bits(values[3].parse().unwrap()).unwrap(),
265 hit_sound: HitSoundType::from_bits(values[4].parse().unwrap()).unwrap(),
266 additions: String::from("0:0:0:0:"),
267 key_sound: -1,
268 end_time: 0,
269 volume: 0,
270 ..Default::default()
271 };
272
273 if hit_object.type_ == HitObjectType::Hold {
274 let end_time = &values[5][..values[5].find(':').unwrap()];
275 hit_object.end_time = end_time.parse().unwrap();
276 }
277
278 if values.len() > 5 {
279 let additions: Vec<&str> = values[5].split(':').collect();
280
281 let volume_field = if hit_object.type_ == HitObjectType::Hold {
282 4
283 } else {
284 3
285 };
286
287 if additions.len() > volume_field && !additions[volume_field].is_empty() {
288 hit_object.volume =
289 std::cmp::max(0, additions[volume_field].parse().unwrap());
290 }
291
292 let key_sound_field = volume_field + 1;
293 if additions.len() > key_sound_field
294 && !additions[key_sound_field].is_empty()
295 {
296 }
303
304 self_.hit_objects.push(hit_object);
305 }
306 }
307 }
308 }
309
310 self_
311 }
312
313 pub fn to_qua(self) -> QuaverMap {
314 let mut qua = QuaverMap {
315 audio_file: self.audio_file_name,
316 song_preview_time: self.preview_time,
317 background_file: self.background,
318 map_id: -1,
319 map_set_id: -1,
320 title: self.title,
321 artist: self.artist,
322 source: self.source,
323 tags: self.tags,
324 creator: self.creator,
325 difficulty_name: self.version,
326 description: String::from("This is a Quaver converted osu! map"),
327 ..Default::default()
328 };
329
330 match self.key_count {
331 4 => qua.mode = GameMode::Keys4,
332 7 => qua.mode = GameMode::Keys7,
333 8 => {
334 qua.mode = GameMode::Keys7;
335 qua.has_scratch_key = true;
336 }
337 _ => qua.mode = GameMode::Keys4,
338 }
339
340 for path in self.custom_audio_samples {
341 qua.custom_audio_samples.push(CustomAudioSampleInfo {
342 path,
343 unaffected_by_rate: false,
344 })
345 }
346
347 for info in self.sound_effects {
348 if info.volume == 0 {
349 continue;
350 }
351
352 qua.sound_effects.push(SoundEffectInfo {
353 start_time: info.start_time as f32,
354 sample: info.sample + 1,
355 volume: info.volume,
356 })
357 }
358
359 for tp in self.timing_points {
360 let is_sv = tp.inherited == 0 || tp.milliseconds_per_beat < 0.;
361
362 if is_sv {
363 qua.slider_velocities.push(SliderVelocityInfo {
364 start_time: tp.offset,
365 multiplier: clamp(-100. / tp.milliseconds_per_beat, 0.1, 10.),
366 })
367 } else {
368 qua.timing_points.push(TimingPointInfo {
369 start_time: tp.offset,
370 bpm: 60000. / tp.milliseconds_per_beat,
371 ..Default::default()
373 })
374 }
375 }
376
377 for hit_object in self.hit_objects {
378 let mut key_lane = clamp(
379 hit_object.x as f64 / (512f64 / self.key_count as f64),
380 0.,
381 (self.key_count - 1) as f64,
382 ) as i32
383 + 1;
384
385 if qua.has_scratch_key {
386 if key_lane == 1 {
387 key_lane = self.key_count;
388 } else {
389 key_lane -= 1
390 };
391 }
392
393 if hit_object.type_ == HitObjectType::Circle {
394 qua.hit_objects.push(HitObjectInfo {
395 start_time: hit_object.start_time,
396 lane: key_lane,
397 end_time: 0,
398 })
401 }
402 }
403
404 qua.sort();
405
406 qua
407 }
408
409 fn custom_audio_sample_index(&mut self, path: &str) -> i32 {
410 for i in 0..self.custom_audio_samples.len() {
411 if self.custom_audio_samples[i] == path {
412 return i as i32;
413 }
414 }
415
416 self.custom_audio_samples.push(path.to_string());
417 self.custom_audio_samples.len() as i32 - 1
418 }
419
420 fn strip_comments(line: &str) -> &str {
421 let index = line.find("//").unwrap_or(0);
422 if index > 0 {
423 return &line[..index];
424 }
425 line
426 }
427
428 fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
429 where
430 P: AsRef<Path>,
431 {
432 let file = File::open(filename)?;
433 Ok(io::BufReader::new(file).lines())
434 }
435}
436
437bitflags! {
438 #[derive(Default)]
439 pub struct HitObjectType: i32{
440 const Circle = 1 << 0;
441 const Slider = 1 << 1;
442 const NewCombo = 1 << 2;
443 const Spinner = 1 << 3;
444 const ComboOffset = 1 << 4 | 1 << 5 | 1 << 6;
445 const Hold = 1 << 7;
446 }
447}
448
449bitflags! {
450 #[derive(Default)]
451 pub struct HitSoundType: u32 {
452 const None = 0;
453 const Normal = 1;
454 const Whistle = 2;
455 const Finish = 4;
456 const Clap = 8;
457 }
458}
459
460pub struct OsuTimingPoint {
461 pub offset: f32,
462 pub milliseconds_per_beat: f32,
463 pub sample_type: i32,
465 pub sample_set: i32,
466 pub volume: i32,
467 pub inherited: i32,
468 pub kiai_mode: i32,
469}
470
471#[derive(Default, Debug)]
472pub struct OsuHitObject {
473 pub x: i32,
474 pub y: i32,
475 pub start_time: i32,
476 pub type_: HitObjectType,
477 pub hit_sound: HitSoundType,
478 pub end_time: i32,
479 pub additions: String,
480 pub key1: bool,
481 pub key2: bool,
482 pub key3: bool,
483 pub key4: bool,
484 pub key5: bool,
485 pub key6: bool,
486 pub key7: bool,
487 pub volume: i32,
488 pub key_sound: i32,
489}
490
491pub struct OsuSampleInfo {
492 pub start_time: i32,
493 pub layer: i32,
494 pub volume: i32,
495 pub sample: i32,
496}