rosu-map 0.2.1

Library to de- and encode .osu files
Documentation
use std::{fmt::Debug, fs, num::NonZeroI32};

use rosu_map::{
    section::{
        general::GameMode,
        hit_objects::{
            HitObject, HitObjectKind, HitObjectSlider, PathControlPoint, PathType, SliderPath,
        },
    },
    util::Pos,
    Beatmap,
};
use test_log::test;

#[test]
fn stability() {
    let mut bytes = Vec::with_capacity(4096);

    for entry in fs::read_dir("./resources").unwrap() {
        let entry = entry.unwrap();
        let filename = entry.file_name();
        let filename = filename.to_str().unwrap();

        if !(filename.ends_with(".osu") || filename.ends_with(".osb")) {
            continue;
        }

        let mut decoded = Beatmap::from_path(entry.path())
            .unwrap_or_else(|e| panic!("Failed to decode beatmap {filename:?}: {e:?}"));

        bytes.clear();

        decoded
            .encode(&mut bytes)
            .unwrap_or_else(|e| panic!("Failed to encode beatmap {filename:?}: {e:?}"));

        let decoded_after_encode = Beatmap::from_bytes(&bytes).unwrap_or_else(|e| {
            panic!("Failed to decode beatmap after encoding {filename:?}: {e:?}")
        });

        assert_eq_list(
            &decoded.control_points.timing_points,
            &decoded_after_encode.control_points.timing_points,
            filename,
        );
        assert_eq_list(
            &decoded.control_points.effect_points,
            &decoded_after_encode.control_points.effect_points,
            filename,
        );
        assert_eq_list(
            &decoded.hit_objects,
            &decoded_after_encode.hit_objects,
            filename,
        );

        assert_eq!(
            decoded.custom_colors, decoded_after_encode.custom_colors,
            "{filename:?}"
        );
        assert_eq!(
            decoded.custom_combo_colors, decoded_after_encode.custom_combo_colors,
            "{filename:?}"
        );
    }

    #[track_caller]
    fn assert_eq_list<T: Debug + PartialEq>(expected: &[T], actual: &[T], filename: &str) {
        if let Some(idx) = expected.iter().zip(actual).position(|(a, b)| a != b) {
            panic!(
                "[{idx}] filename: {filename:?}\nleft:\n{:?}\nright:\n{:?}",
                expected[idx], actual[idx]
            );
        }
    }
}

#[test]
fn bspline_curve_type() {
    let control_points = vec![
        PathControlPoint {
            pos: Pos::new(0.0, 0.0),
            path_type: Some(PathType::new_b_spline(NonZeroI32::new(3).unwrap())),
        },
        PathControlPoint {
            pos: Pos::new(50.0, 50.0),
            path_type: None,
        },
        PathControlPoint {
            pos: Pos::new(100.0, 100.0),
            path_type: Some(PathType::new_b_spline(NonZeroI32::new(3).unwrap())),
        },
        PathControlPoint {
            pos: Pos::new(150.0, 150.0),
            path_type: None,
        },
    ];

    let path = SliderPath::new(GameMode::Taiko, control_points, None);

    let slider = HitObjectSlider {
        pos: Pos::new(0.0, 0.0),
        new_combo: false,
        combo_offset: 0,
        path,
        node_samples: Vec::new(),
        repeat_count: 0,
        velocity: 0.0,
    };

    let hit_object = HitObject {
        start_time: 0.0,
        kind: HitObjectKind::Slider(slider),
        samples: Vec::new(),
    };

    let mut map = Beatmap {
        hit_objects: vec![hit_object],
        ..Default::default()
    };

    let mut bytes = Vec::with_capacity(512);

    map.encode(&mut bytes).unwrap();
    let decoded_after_encode = Beatmap::from_bytes(&bytes).unwrap();

    let HitObjectKind::Slider(ref expected) = map.hit_objects[0].kind else {
        unreachable!()
    };

    let HitObjectKind::Slider(ref actual) = decoded_after_encode.hit_objects[0].kind else {
        unreachable!()
    };

    assert_eq!(actual.path.control_points().len(), 4);
    assert_eq!(expected.path.control_points(), actual.path.control_points());
}

#[test]
fn multi_segment_slider_with_floating_point_error() {
    let control_points = vec![
        PathControlPoint {
            pos: Pos::new(0.0, 0.0),
            path_type: Some(PathType::BEZIER),
        },
        PathControlPoint {
            pos: Pos::new(0.5, 0.5),
            path_type: None,
        },
        PathControlPoint {
            pos: Pos::new(0.51, 0.51),
            path_type: None,
        },
        PathControlPoint {
            pos: Pos::new(1.0, 1.0),
            path_type: Some(PathType::BEZIER),
        },
        PathControlPoint {
            pos: Pos::new(2.0, 2.0),
            path_type: None,
        },
    ];

    let path = SliderPath::new(GameMode::Taiko, control_points, None);

    let slider = HitObjectSlider {
        pos: Pos::new(0.6, 0.6),
        new_combo: false,
        combo_offset: 0,
        path,
        node_samples: Vec::new(),
        repeat_count: 0,
        velocity: 0.0,
    };

    let hit_object = HitObject {
        start_time: 0.0,
        kind: HitObjectKind::Slider(slider),
        samples: Vec::new(),
    };

    let mut map = Beatmap {
        hit_objects: vec![hit_object],
        ..Default::default()
    };

    let mut bytes = Vec::with_capacity(512);

    map.encode(&mut bytes).unwrap();
    let decoded_after_encode = Beatmap::from_bytes(&bytes).unwrap();

    let HitObjectKind::Slider(ref decoded_slider) = decoded_after_encode.hit_objects[0].kind else {
        unreachable!()
    };

    assert_eq!(decoded_slider.path.control_points().len(), 5);
}