osu_db/
listing.rs

1//! Parsing for the `osu!.db` file, containing cached information about the beatmap listing.
2
3use crate::prelude::*;
4use std::{convert::identity, hash::Hash};
5
6/// In these `osu!.db` versions several breaking changes were introduced.
7/// While parsing, these changes are automatically handled depending on the `osu!.db` version.
8const CHANGE_20140609: u32 = 20140609;
9const CHANGE_20191106: u32 = 20191106;
10
11/// A structure representing the `osu!.db` binary database.
12/// This database contains pre-processed data and settings for all available osu! beatmaps.
13#[cfg_attr(feature = "ser-de", derive(Serialize, Deserialize))]
14#[derive(Debug, Clone, PartialEq)]
15pub struct Listing {
16    /// The `osu!.db` version number.
17    /// This is a decimal number in the form `YYYYMMDD` (eg. `20150203`).
18    pub version: u32,
19
20    /// The amount of folders within the "Songs" directory.
21    /// Probably for quick checking of changes within the directory.
22    pub folder_count: u32,
23
24    /// Whether the account is locked/banned, and when will be it be unbanned.
25    pub unban_date: Option<DateTime<Utc>>,
26
27    /// Self-explanatory.
28    pub player_name: Option<String>,
29
30    /// All stored beatmaps and the information stored about them.
31    /// The main bulk of information.
32    pub beatmaps: Vec<Beatmap>,
33
34    /// User permissions (0 = None, 1 = Normal, 2 = Moderator, 4 = Supporter,
35    /// 8 = Friend, 16 = peppy, 32 = World Cup staff)
36    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    /// Parse a listing from the `osu!.db` database file.
44    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Listing, Error> {
45        Self::from_bytes(&fs::read(path)?)
46    }
47
48    /// Write the listing to an arbitrary writer.
49    pub fn to_writer<W: Write>(&self, mut out: W) -> io::Result<()> {
50        self.wr(&mut out)
51    }
52
53    /// Similar to `to_writer` but writes the listing to a file (ie. `osu!.db`).
54    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    /// The name of the artist without special characters.
63    pub artist_ascii: Option<String>,
64    /// The unrestrained artist name.
65    pub artist_unicode: Option<String>,
66    /// The song title without special characters.
67    pub title_ascii: Option<String>,
68    /// The unrestrained song title.
69    pub title_unicode: Option<String>,
70    /// The name of the beatmap mapper.
71    pub creator: Option<String>,
72    /// The name of this specific difficulty.
73    pub difficulty_name: Option<String>,
74    /// The filename of the song file.
75    pub audio: Option<String>,
76    /// The MD5 hash of the beatmap.
77    pub hash: Option<String>,
78    /// The filename of the `.osu` file corresponding to this specific difficulty.
79    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    /// Drain time in seconds.
95    pub drain_time: u32,
96    /// Total beatmap time in milliseconds.
97    pub total_time: u32,
98    /// When should the song start playing when previewed, in milliseconds since the start of the
99    /// song.
100    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    /// Where did the song come from, if anywhere.
113    pub song_source: Option<String>,
114    /// Song tags, separated by whitespace.
115    pub tags: Option<String>,
116    pub online_offset: u16,
117    pub title_font: Option<String>,
118    /// Whether the beatmap has been played, and if it has, when was it last played.
119    pub last_played: Option<DateTime<Utc>>,
120    /// Whether the beatmap was in `osz2` format.
121    pub is_osz2: bool,
122    /// The folder name of the beatmapset within the "Songs" folder.
123    pub folder_name: Option<String>,
124    /// When was the beatmap last checked against the online osu! repository.
125    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    /// Quoting the wiki: "Unknown. Only present if version is less than 20140609".
132    pub mysterious_short: Option<u16>,
133    /// Who knows.
134    ///
135    /// Perhaps an early attempt at "last modified", but scrapped once peppy noticed it only had
136    /// 32 bits.
137    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    /// Any of the three.
147    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
182/// A list of the precalculated amount of difficulty stars a given mod combination yields for a
183/// beatmap.
184///
185/// You might want to convert this list into a map using
186/// `ratings.into_iter().collect::<HashMap<_>>()` or variations, allowing for quick indexing with
187/// different mod combinations.
188///
189/// Note that old "osu!.db" files (before the 2014/06/09 version) do not have these ratings.
190pub type StarRatings = Vec<(ModSet, f64)>;
191
192#[cfg_attr(feature = "ser-de", derive(Serialize, Deserialize))]
193#[derive(Debug, Clone, PartialEq)]
194pub struct TimingPoint {
195    /// The bpm of the timing point.
196    pub bpm: f64,
197    /// The amount of milliseconds from the start of the song this timing point is located on.
198    pub offset: f64,
199    /// Whether the timing point inherits or not.
200    ///
201    /// Basically, inherited timing points are absolute, and define a new bpm independent of any previous bpms.
202    /// On the other hand, timing points that do not inherit have a negative bpm representing a percentage of the
203    /// bpm of the previous timing point.
204    /// See the osu wiki on the `.osu` format for more details.
205    pub inherits: bool,
206}
207
208/// A grade obtained by passing a beatmap.
209/// Also called a rank.
210///
211/// Note that currently grades are just exposed as a raw byte.
212/// I am not sure of how do this bytes map to grades as of now.
213/// TODO: Figure out grades.
214#[cfg_attr(feature = "ser-de", derive(Serialize, Deserialize))]
215#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
216pub enum Grade {
217    /// SS+, silver SS rank
218    /// Ie. only perfect scores with hidden mod enabled.
219    SSPlus,
220    /// S+, silver S rank
221    /// Ie. highest performance with hidden mod enabled.
222    SPlus,
223    /// SS rank
224    /// Ie. only perfect scores.
225    SS,
226    S,
227    A,
228    B,
229    C,
230    D,
231    /// No rank achieved yet.
232    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    //Write into a writer without prefixing the length
414    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        //Write beatmap into a temporary buffer, as beatmap length needs to be
483        //known and prefixed
484        let mut raw_buf = Vec::new();
485        write_dry(this, &mut raw_buf, version)?;
486        //Write the raw buffer prefixed by its length
487        (raw_buf.len() as u32).wr(out)?;
488        out.write_all(&raw_buf)?;
489    }else{
490        //Write beatmap as-is
491        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
544/// Before the breaking change in 2014 several difficulty values were stored as bytes.
545/// After it they were stored as single floats.
546/// Accomodate this differences.
547fn 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}