1use crate::prelude::*;
4use std::{convert::identity, hash::Hash};
5
6const CHANGE_20140609: u32 = 20140609;
9const CHANGE_20191106: u32 = 20191106;
10
11#[cfg_attr(feature = "ser-de", derive(Serialize, Deserialize))]
14#[derive(Debug, Clone, PartialEq)]
15pub struct Listing {
16 pub version: u32,
19
20 pub folder_count: u32,
23
24 pub unban_date: Option<DateTime<Utc>>,
26
27 pub player_name: Option<String>,
29
30 pub beatmaps: Vec<Beatmap>,
33
34 pub user_permissions: u32,
37}
38impl Listing {
39 pub fn from_bytes(bytes: &[u8]) -> Result<Listing, Error> {
40 Ok(listing(bytes).map(|(_rem, listing)| listing)?)
41 }
42
43 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Listing, Error> {
45 Self::from_bytes(&fs::read(path)?)
46 }
47
48 pub fn to_writer<W: Write>(&self, mut out: W) -> io::Result<()> {
50 self.wr(&mut out)
51 }
52
53 pub fn save<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
55 self.to_writer(BufWriter::new(File::create(path)?))
56 }
57}
58
59#[cfg_attr(feature = "ser-de", derive(Serialize, Deserialize))]
60#[derive(Debug, Clone, PartialEq)]
61pub struct Beatmap {
62 pub artist_ascii: Option<String>,
64 pub artist_unicode: Option<String>,
66 pub title_ascii: Option<String>,
68 pub title_unicode: Option<String>,
70 pub creator: Option<String>,
72 pub difficulty_name: Option<String>,
74 pub audio: Option<String>,
76 pub hash: Option<String>,
78 pub file_name: Option<String>,
80 pub status: RankedStatus,
81 pub hitcircle_count: u16,
82 pub slider_count: u16,
83 pub spinner_count: u16,
84 pub last_modified: DateTime<Utc>,
85 pub approach_rate: f32,
86 pub circle_size: f32,
87 pub hp_drain: f32,
88 pub overall_difficulty: f32,
89 pub slider_velocity: f64,
90 pub std_ratings: StarRatings,
91 pub taiko_ratings: StarRatings,
92 pub ctb_ratings: StarRatings,
93 pub mania_ratings: StarRatings,
94 pub drain_time: u32,
96 pub total_time: u32,
98 pub preview_time: u32,
101 pub timing_points: Vec<TimingPoint>,
102 pub beatmap_id: i32,
103 pub beatmapset_id: i32,
104 pub thread_id: u32,
105 pub std_grade: Grade,
106 pub taiko_grade: Grade,
107 pub ctb_grade: Grade,
108 pub mania_grade: Grade,
109 pub local_beatmap_offset: u16,
110 pub stack_leniency: f32,
111 pub mode: Mode,
112 pub song_source: Option<String>,
114 pub tags: Option<String>,
116 pub online_offset: u16,
117 pub title_font: Option<String>,
118 pub last_played: Option<DateTime<Utc>>,
120 pub is_osz2: bool,
122 pub folder_name: Option<String>,
124 pub last_online_check: DateTime<Utc>,
126 pub ignore_sounds: bool,
127 pub ignore_skin: bool,
128 pub disable_storyboard: bool,
129 pub disable_video: bool,
130 pub visual_override: bool,
131 pub mysterious_short: Option<u16>,
133 pub mysterious_last_modified: u32,
138 pub mania_scroll_speed: u8,
139}
140
141#[cfg_attr(feature = "ser-de", derive(Serialize, Deserialize))]
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
143pub enum RankedStatus {
144 Unknown,
145 Unsubmitted,
146 PendingWipGraveyard,
148 Ranked,
149 Approved,
150 Qualified,
151 Loved,
152}
153impl RankedStatus {
154 pub fn from_raw(byte: u8) -> Option<RankedStatus> {
155 use self::RankedStatus::*;
156 Some(match byte {
157 0 => Unknown,
158 1 => Unsubmitted,
159 2 => PendingWipGraveyard,
160 4 => Ranked,
161 5 => Approved,
162 6 => Qualified,
163 7 => Loved,
164 _ => return None,
165 })
166 }
167
168 pub fn raw(self) -> u8 {
169 use self::RankedStatus::*;
170 match self {
171 Unknown => 0,
172 Unsubmitted => 1,
173 PendingWipGraveyard => 2,
174 Ranked => 4,
175 Approved => 5,
176 Qualified => 6,
177 Loved => 7,
178 }
179 }
180}
181
182pub type StarRatings = Vec<(ModSet, f64)>;
191
192#[cfg_attr(feature = "ser-de", derive(Serialize, Deserialize))]
193#[derive(Debug, Clone, PartialEq)]
194pub struct TimingPoint {
195 pub bpm: f64,
197 pub offset: f64,
199 pub inherits: bool,
206}
207
208#[cfg_attr(feature = "ser-de", derive(Serialize, Deserialize))]
215#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
216pub enum Grade {
217 SSPlus,
220 SPlus,
223 SS,
226 S,
227 A,
228 B,
229 C,
230 D,
231 Unplayed,
233}
234impl Grade {
235 pub fn raw(self) -> u8 {
236 use self::Grade::*;
237 match self {
238 SSPlus => 0,
239 SPlus => 1,
240 SS => 2,
241 S => 3,
242 A => 4,
243 B => 5,
244 C => 6,
245 D => 7,
246 Unplayed => 9,
247 }
248 }
249 pub fn from_raw(raw: u8) -> Option<Grade> {
250 use self::Grade::*;
251 Some(match raw {
252 0 => SSPlus,
253 1 => SPlus,
254 2 => SS,
255 3 => S,
256 4 => A,
257 5 => B,
258 6 => C,
259 7 => D,
260 9 => Unplayed,
261 _ => return None,
262 })
263 }
264}
265
266fn listing(bytes: &[u8]) -> IResult<&[u8], Listing> {
267 let (rem, version) = int(bytes)?;
268 let (rem, folder_count) = int(rem)?;
269 let (rem, account_unlocked) = boolean(rem)?;
270 let (rem, unlock_date) = datetime(rem)?;
271 let (rem, player_name) = opt_string(rem)?;
272 let (rem, beatmaps) = length_count(map(int, identity), |bytes| beatmap(bytes, version))(rem)?;
273 let (rem, user_permissions) = int(rem)?;
274
275 let listing = Listing {
276 version,
277 folder_count,
278 unban_date: build_option(account_unlocked, unlock_date),
279 player_name,
280 beatmaps,
281 user_permissions,
282 };
283
284 Ok((rem, listing))
285}
286
287writer!(Listing [this, out] {
288 this.version.wr(out)?;
289 this.folder_count.wr(out)?;
290 write_option(out,this.unban_date,0_u64)?;
291 this.player_name.wr(out)?;
292 PrefixedList(&this.beatmaps).wr_args(out,this.version)?;
293 this.user_permissions.wr(out)?;
294});
295
296fn beatmap(bytes: &[u8], version: u32) -> IResult<&[u8], Beatmap> {
297 let (rem, _beatmap_size) = cond(version < CHANGE_20191106, int)(bytes)?;
298 let (rem, artist_ascii) = opt_string(rem)?;
299 let (rem, artist_unicode) = opt_string(rem)?;
300 let (rem, title_ascii) = opt_string(rem)?;
301 let (rem, title_unicode) = opt_string(rem)?;
302 let (rem, creator) = opt_string(rem)?;
303 let (rem, difficulty_name) = opt_string(rem)?;
304 let (rem, audio) = opt_string(rem)?;
305 let (rem, hash) = opt_string(rem)?;
306 let (rem, file_name) = opt_string(rem)?;
307 let (rem, status) = ranked_status(rem)?;
308 let (rem, hitcircle_count) = short(rem)?;
309 let (rem, slider_count) = short(rem)?;
310 let (rem, spinner_count) = short(rem)?;
311 let (rem, last_modified) = datetime(rem)?;
312 let (rem, approach_rate) = difficulty_value(rem, version)?;
313 let (rem, circle_size) = difficulty_value(rem, version)?;
314 let (rem, hp_drain) = difficulty_value(rem, version)?;
315 let (rem, overall_difficulty) = difficulty_value(rem, version)?;
316 let (rem, slider_velocity) = double(rem)?;
317 let (rem, std_ratings) = star_ratings(rem, version)?;
318 let (rem, taiko_ratings) = star_ratings(rem, version)?;
319 let (rem, ctb_ratings) = star_ratings(rem, version)?;
320 let (rem, mania_ratings) = star_ratings(rem, version)?;
321 let (rem, drain_time) = int(rem)?;
322 let (rem, total_time) = int(rem)?;
323 let (rem, preview_time) = int(rem)?;
324 let (rem, timing_points) = length_count(map(int, identity), timing_point)(rem)?;
325 let (rem, beatmap_id) = int(rem)?;
326 let (rem, beatmapset_id) = int(rem)?;
327 let (rem, thread_id) = int(rem)?;
328 let (rem, std_grade) = grade(rem)?;
329 let (rem, taiko_grade) = grade(rem)?;
330 let (rem, ctb_grade) = grade(rem)?;
331 let (rem, mania_grade) = grade(rem)?;
332 let (rem, local_beatmap_offset) = short(rem)?;
333 let (rem, stack_leniency) = single(rem)?;
334 let (rem, mode) = map_opt(byte, Mode::from_raw)(rem)?;
335 let (rem, song_source) = opt_string(rem)?;
336 let (rem, tags) = opt_string(rem)?;
337 let (rem, online_offset) = short(rem)?;
338 let (rem, title_font) = opt_string(rem)?;
339 let (rem, unplayed) = boolean(rem)?;
340 let (rem, last_played) = datetime(rem)?;
341 let (rem, is_osz2) = boolean(rem)?;
342 let (rem, folder_name) = opt_string(rem)?;
343 let (rem, last_online_check) = datetime(rem)?;
344 let (rem, ignore_sounds) = boolean(rem)?;
345 let (rem, ignore_skin) = boolean(rem)?;
346 let (rem, disable_storyboard) = boolean(rem)?;
347 let (rem, disable_video) = boolean(rem)?;
348 let (rem, visual_override) = boolean(rem)?;
349 let (rem, mysterious_short) = cond(version < CHANGE_20140609, short)(rem)?;
350 let (rem, mysterious_last_modified) = int(rem)?;
351 let (rem, mania_scroll_speed) = byte(rem)?;
352
353 let map = Beatmap {
354 artist_ascii,
355 artist_unicode,
356 title_ascii,
357 title_unicode,
358 creator,
359 difficulty_name,
360 audio,
361 hash,
362 file_name,
363 status,
364 hitcircle_count,
365 slider_count,
366 spinner_count,
367 last_modified,
368 approach_rate,
369 circle_size,
370 hp_drain,
371 overall_difficulty,
372 slider_velocity,
373 std_ratings,
374 taiko_ratings,
375 ctb_ratings,
376 mania_ratings,
377 drain_time,
378 total_time,
379 preview_time,
380 timing_points,
381 beatmap_id: beatmap_id as i32,
382 beatmapset_id: beatmapset_id as i32,
383 thread_id,
384 std_grade,
385 taiko_grade,
386 ctb_grade,
387 mania_grade,
388 local_beatmap_offset,
389 stack_leniency,
390 mode,
391 song_source,
392 tags,
393 online_offset,
394 title_font,
395 last_played: build_option(unplayed, last_played),
396 is_osz2,
397 folder_name,
398 last_online_check,
399 ignore_sounds,
400 ignore_skin,
401 disable_storyboard,
402 disable_video,
403 visual_override,
404 mysterious_short,
405 mysterious_last_modified,
406 mania_scroll_speed,
407 };
408
409 Ok((rem, map))
410}
411
412writer!(Beatmap [this,out,version: u32] {
413 fn write_dry<W: Write>(this: &Beatmap, out: &mut W, version: u32) -> io::Result<()> {
415 macro_rules! wr_difficulty_value {
416 ($f32:expr) => {{
417 if version>=CHANGE_20140609 {
418 $f32.wr(out)?;
419 }else{
420 ($f32 as u8).wr(out)?;
421 }
422 }};
423 }
424 this.artist_ascii.wr(out)?;
425 this.artist_unicode.wr(out)?;
426 this.title_ascii.wr(out)?;
427 this.title_unicode.wr(out)?;
428 this.creator.wr(out)?;
429 this.difficulty_name.wr(out)?;
430 this.audio.wr(out)?;
431 this.hash.wr(out)?;
432 this.file_name.wr(out)?;
433 this.status.wr(out)?;
434 this.hitcircle_count.wr(out)?;
435 this.slider_count.wr(out)?;
436 this.spinner_count.wr(out)?;
437 this.last_modified.wr(out)?;
438 wr_difficulty_value!(this.approach_rate);
439 wr_difficulty_value!(this.circle_size);
440 wr_difficulty_value!(this.hp_drain);
441 wr_difficulty_value!(this.overall_difficulty);
442 this.slider_velocity.wr(out)?;
443 this.std_ratings.wr_args(out,version)?;
444 this.taiko_ratings.wr_args(out,version)?;
445 this.ctb_ratings.wr_args(out,version)?;
446 this.mania_ratings.wr_args(out,version)?;
447 this.drain_time.wr(out)?;
448 this.total_time.wr(out)?;
449 this.preview_time.wr(out)?;
450 PrefixedList(&this.timing_points).wr(out)?;
451 (this.beatmap_id as u32).wr(out)?;
452 (this.beatmapset_id as u32).wr(out)?;
453 this.thread_id.wr(out)?;
454 this.std_grade.wr(out)?;
455 this.taiko_grade.wr(out)?;
456 this.ctb_grade.wr(out)?;
457 this.mania_grade.wr(out)?;
458 this.local_beatmap_offset.wr(out)?;
459 this.stack_leniency.wr(out)?;
460 this.mode.raw().wr(out)?;
461 this.song_source.wr(out)?;
462 this.tags.wr(out)?;
463 this.online_offset.wr(out)?;
464 this.title_font.wr(out)?;
465 write_option(out,this.last_played,0_u64)?;
466 this.is_osz2.wr(out)?;
467 this.folder_name.wr(out)?;
468 this.last_online_check.wr(out)?;
469 this.ignore_sounds.wr(out)?;
470 this.ignore_skin.wr(out)?;
471 this.disable_storyboard.wr(out)?;
472 this.disable_video.wr(out)?;
473 this.visual_override.wr(out)?;
474 if version<CHANGE_20140609 {
475 this.mysterious_short.unwrap_or(0).wr(out)?;
476 }
477 this.mysterious_last_modified.wr(out)?;
478 this.mania_scroll_speed.wr(out)?;
479 Ok(())
480 }
481 if version < CHANGE_20191106 {
482 let mut raw_buf = Vec::new();
485 write_dry(this, &mut raw_buf, version)?;
486 (raw_buf.len() as u32).wr(out)?;
488 out.write_all(&raw_buf)?;
489 }else{
490 write_dry(this, out, version)?;
492 }
493});
494
495fn timing_point(bytes: &[u8]) -> IResult<&[u8], TimingPoint> {
496 let (rem, bpm) = double(bytes)?;
497 let (rem, offset) = double(rem)?;
498 let (rem, inherits) = boolean(rem)?;
499
500 let timing_point = TimingPoint {
501 bpm,
502 offset,
503 inherits,
504 };
505
506 Ok((rem, timing_point))
507}
508
509writer!(TimingPoint [this,out] {
510 this.bpm.wr(out)?;
511 this.offset.wr(out)?;
512 this.inherits.wr(out)?;
513});
514
515fn star_ratings(bytes: &[u8], version: u32) -> IResult<&[u8], Vec<(ModSet, f64)>> {
516 if version >= CHANGE_20140609 {
517 length_count(map(int, identity), star_rating)(bytes)
518 } else {
519 Ok((bytes, Vec::new()))
520 }
521}
522
523fn star_rating(bytes: &[u8]) -> IResult<&[u8], (ModSet, f64)> {
524 let (rem, _tag) = tag(&[0x08])(bytes)?;
525 let (rem, mods) = map(int, ModSet::from_bits)(rem)?;
526 let (rem, _tag) = tag(&[0x0d])(rem)?;
527 let (rem, stars) = double(rem)?;
528
529 Ok((rem, (mods, stars)))
530}
531
532writer!(Vec<(ModSet,f64)> [this,out,version: u32] {
533 if version>=CHANGE_20140609 {
534 PrefixedList(this).wr(out)?;
535 }
536});
537writer!((ModSet,f64) [this,out] {
538 0x08_u8.wr(out)?;
539 this.0.bits().wr(out)?;
540 0x0d_u8.wr(out)?;
541 this.1.wr(out)?;
542});
543
544fn difficulty_value(bytes: &[u8], version: u32) -> IResult<&[u8], f32> {
548 if version >= CHANGE_20140609 {
549 single(bytes)
550 } else {
551 byte(bytes).map(|(rem, b)| (rem, b as f32))
552 }
553}
554
555fn ranked_status(bytes: &[u8]) -> IResult<&[u8], RankedStatus> {
556 map_opt(byte, RankedStatus::from_raw)(bytes)
557}
558
559writer!(RankedStatus [this,out] this.raw().wr(out)?);
560
561fn grade(bytes: &[u8]) -> IResult<&[u8], Grade> {
562 map_opt(byte, Grade::from_raw)(bytes)
563}
564
565writer!(Grade [this,out] this.raw().wr(out)?);
566
567fn build_option<T>(is_none: bool, content: T) -> Option<T> {
568 if is_none {
569 None
570 } else {
571 Some(content)
572 }
573}
574fn write_option<W: Write, T: SimpleWritable, D: SimpleWritable>(
575 out: &mut W,
576 opt: Option<T>,
577 def: D,
578) -> io::Result<()> {
579 match opt {
580 Some(t) => {
581 false.wr(out)?;
582 t.wr(out)?;
583 }
584 None => {
585 true.wr(out)?;
586 def.wr(out)?;
587 }
588 }
589 Ok(())
590}