osu-file-parser 1.1.0

A crate to parse an osu! beatmap file
Documentation
mod error_line_index;
mod hitobjects;
mod osu_files;
mod parsers;
mod storyboard;

use crate::osu_file::types::Decimal;
use pretty_assertions::assert_eq;
use std::path::{Path, PathBuf};

use rust_decimal_macros::dec;

use crate::osu_file::{
    colours::{Colour, Colours, Rgb},
    difficulty::Difficulty,
    editor::{self, Editor},
    events::{Background, Break, Event, Events},
    general::{Countdown, General, Mode, OverlayPosition, SampleSet},
    metadata::Metadata,
    timingpoints,
    timingpoints::{Effects, SampleIndex, TimingPoint, TimingPoints, Volume},
    types::Position,
    VersionedFromStr, VersionedToString,
};

#[test]
fn general_parse_v14() {
    let i_str = "AudioFilename: test.mp3
AudioLeadIn: 555
PreviewTime: 5
Countdown: 3
SampleSet: Soft
StackLeniency: 0.9
Mode: 1
LetterboxInBreaks: 1
StoryFireInFront: 0
UseSkinSprites: 1
AlwaysShowPlayfield: 0
OverlayPosition: Above
SkinPreference: myskin
EpilepsyWarning: 1
CountdownOffset: 120
SpecialStyle: 1
WidescreenStoryboard: 1
SamplesMatchPlaybackRate: 1";
    let i = General::from_str(i_str, 14).unwrap().unwrap();

    let g = General {
        audio_filename: Some(PathBuf::from("test.mp3").into()),
        audio_lead_in: Some(555.into()),
        audio_hash: None,
        preview_time: Some(5.into()),
        countdown: Some(Countdown::Double),
        sample_set: Some(SampleSet::Soft),
        stack_leniency: Some(Decimal::from(dec!(0.9)).into()),
        mode: Some(Mode::Taiko),
        letterbox_in_breaks: Some(true.into()),
        story_fire_in_front: Some(false.into()),
        use_skin_sprites: Some(true.into()),
        always_show_playfield: Some(false.into()),
        overlay_position: Some(OverlayPosition::Above),
        skin_preference: Some("myskin".to_string().into()),
        epilepsy_warning: Some(true.into()),
        countdown_offset: Some(120.into()),
        special_style: Some(true.into()),
        widescreen_storyboard: Some(true.into()),
        samples_match_playback_rate: Some(true.into()),
        ..General::new()
    };

    assert_eq!(i, g);
    assert_eq!(i_str, i.to_string(14).unwrap());
}

#[test]
fn editor_parse_v14() {
    let i_str = "Bookmarks: 11018,21683,32349,37683,48349,59016,69683,80349,91016
DistanceSpacing: 0.8
BeatDivisor: 12
GridSize: 8
TimelineZoom: 2";
    let i = Editor::from_str(i_str, 14).unwrap().unwrap();

    let e = Editor {
        bookmarks: Some(
            vec![
                11018, 21683, 32349, 37683, 48349, 59016, 69683, 80349, 91016,
            ]
            .into(),
        ),
        distance_spacing: Some(Decimal::from(dec!(0.8)).into()),
        beat_divisor: Some(Decimal::from(dec!(12)).into()),
        grid_size: Some(8.into()),
        timeline_zoom: Some(Decimal::from(dec!(2)).into()),
        current_time: None,
    };

    assert_eq!(i, e);
    assert_eq!(i_str, i.to_string(14).unwrap());
}

#[test]
fn metadata_parse_v14() {
    let i_str = "Title:LOVE IS ORANGE
TitleUnicode:LOVE IS ORANGE
Artist:Orange Lounge
ArtistUnicode:Orange Lounge
Creator:Xnery
Version:Bittersweet Love
Source:beatmania IIDX 8th style
Tags:famoss 舟木智介 tomosuke funaki 徳井志津江 videogame ハードシャンソン Tart&Toffee
BeatmapID:3072232
BeatmapSetID:1499093";
    let i = Metadata::from_str(i_str, 14).unwrap().unwrap();

    let m = Metadata {
        title: Some("LOVE IS ORANGE".to_string().into()),
        title_unicode: Some("LOVE IS ORANGE".to_string().into()),
        artist: Some("Orange Lounge".to_string().into()),
        artist_unicode: Some("Orange Lounge".to_string().into()),
        creator: Some("Xnery".to_string().into()),
        version: Some("Bittersweet Love".to_string().into()),
        source: Some("beatmania IIDX 8th style".to_string().into()),
        tags: Some(
            vec![
                "famoss".to_string(),
                "舟木智介".to_string(),
                "tomosuke".to_string(),
                "funaki".to_string(),
                "徳井志津江".to_string(),
                "videogame".to_string(),
                "ハードシャンソン".to_string(),
                "Tart&Toffee".to_string(),
            ]
            .into(),
        ),
        beatmap_id: Some(3072232.into()),
        beatmap_set_id: Some(1499093.into()),
    };

    assert_eq!(i, m);
    assert_eq!(i_str, i.to_string(14).unwrap());
}

#[test]
fn difficulty_parse_v14() {
    let i_str = "HPDrainRate:8
CircleSize:5
OverallDifficulty:8
ApproachRate:5
SliderMultiplier:1.4
SliderTickRate:1";
    let i = Difficulty::from_str(i_str, 14).unwrap().unwrap();

    let d = Difficulty {
        hp_drain_rate: Some(Decimal::from(dec!(8)).into()),
        circle_size: Some(Decimal::from(dec!(5)).into()),
        overall_difficulty: Some(Decimal::from(dec!(8)).into()),
        approach_rate: Some(Decimal::from(dec!(5)).into()),
        slider_multiplier: Some(Decimal::from(dec!(1.4)).into()),
        slider_tickrate: Some(Decimal::from(rust_decimal::Decimal::ONE).into()),
    };

    assert_eq!(i, d);
    assert_eq!(i_str, i.to_string(14).unwrap());
}

#[test]
fn colours_parse_v14() {
    let i_str = "Combo1 : 255,128,255
SliderTrackOverride : 100,99,70
SliderBorder : 120,130,140";
    let i = Colours::from_str(i_str, 14).unwrap().unwrap();

    let c = vec![
        Colour::Combo(
            1,
            Rgb {
                red: 255,
                green: 128,
                blue: 255,
            },
        ),
        Colour::SliderTrackOverride(Rgb {
            red: 100,
            green: 99,
            blue: 70,
        }),
        Colour::SliderBorder(Rgb {
            red: 120,
            green: 130,
            blue: 140,
        }),
    ];

    assert_eq!(i, Colours(c));
    assert_eq!(i_str, i.to_string(14).unwrap());
}

#[test]
fn timing_points_parse_v14() {
    let i_str = "10000,333.33,4,0,0,100,1,1
12000,-25,4,3,0,100,0,1";
    let i = TimingPoints::from_str(i_str, 14).unwrap().unwrap();

    let t = vec![
        TimingPoint::new_uninherited(
            10000,
            dec!(333.33).into(),
            4,
            timingpoints::SampleSet::BeatmapDefault,
            SampleIndex::OsuDefaultHitsounds,
            Volume::new(100, 14).unwrap(),
            Effects::new(true, false),
        ),
        TimingPoint::new_inherited(
            12000,
            dec!(4),
            4,
            timingpoints::SampleSet::Drum,
            SampleIndex::OsuDefaultHitsounds,
            Volume::new(100, 14).unwrap(),
            Effects::new(true, false),
        ),
    ];

    assert_eq!(i, TimingPoints(t));
    assert_eq!(i_str, i.to_string(14).unwrap());
}

#[test]
fn events_parse_v14() {
    let i_str = "0,0,\"bg2.jpg\",0,0
0,0,bg2.jpg,0,1
//Break Periods
2,100,163";
    let i = Events::from_str(i_str, 14).unwrap().unwrap();

    let e = Events(vec![
        Event::Background(Background {
            commands: Vec::new(),
            start_time: 0,
            file_name: Path::new("\"bg2.jpg\"").into(),
            position: Some(Position {
                x: dec!(0).into(),
                y: dec!(0).into(),
            }),
        }),
        Event::Background(Background {
            commands: Vec::new(),
            start_time: 0,
            file_name: Path::new("bg2.jpg").into(),
            position: Some(Position {
                x: dec!(0).into(),
                y: dec!(1).into(),
            }),
        }),
        Event::Comment("Break Periods".to_string()),
        Event::Break(Break::new(100, 163)),
    ]);

    assert_eq!(i, e);
    assert_eq!(i_str, i.to_string(14).unwrap());
}

#[test]
fn colour_parse_error() {
    let i = "Combo1: foo";
    let err = Colour::from_str(i, 14).unwrap_err();

    assert_eq!(err.to_string(), "Invalid red value");
}

#[test]
fn editor_bookmarks_error() {
    let i = "0,foo";
    let i_err = editor::Bookmarks::from_str(i, 14).unwrap_err();
    assert_eq!(i_err.to_string(), "invalid digit found in string");

    let i = "0-";
    let i_err = editor::Bookmarks::from_str(i, 14).unwrap_err();
    assert_eq!(i_err.to_string(), "invalid digit found in string");

    let i = "0,,";
    let i_err = editor::Bookmarks::from_str(i, 14).unwrap_err();
    assert_eq!(
        i_err.to_string(),
        "Invalid comma list, expected format of `key: value, value, value, ...`"
    );
}