Beatsaber Rust Utilities: A Beatsaber V3 parsing library.
beatsaber beatmap
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Integration testing, load of fixes, and better macro.

- Macro type must be specified.
- The macro now has a special case for string types.
- Maps in the `test_maps` folder now get parsed during testing.
- Many values added in later versions are now optional.
- Fixed rotation renames being swapped.

AlephCubed 184ef3b2 d01b9329

+202 -158
+2
.gitignore
··· 1 1 /target 2 + /test_maps/* 3 + !/test_maps/README.md
+3 -2
src/difficulty.rs
··· 12 12 13 13 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 14 14 #[serde(rename_all = "camelCase")] 15 - pub struct DifficultyV3_2 { 15 + pub struct Difficulty { 16 16 pub version: String, 17 17 pub bpm_events: Vec<BpmEvent>, 18 18 #[serde(rename = "rotationEvents")] ··· 36 36 pub color_event_boxes: Vec<ColorEventBox>, 37 37 #[serde(rename = "lightRotationEventBoxGroups")] 38 38 pub rotation_event_boxes: Vec<RotationEventBoxGroup>, 39 + /// Only present in difficulty file V3.2 or higher. 39 40 #[serde(rename = "lightTranslationEventBoxGroups")] 40 - pub translation_event_boxes: Vec<Value>, // Todo 41 + pub translation_event_boxes: Option<Vec<Value>>, // Todo 41 42 #[doc(alias = "keyword_events")] 42 43 #[serde(rename = "basicEventTypesWithKeywords")] 43 44 pub special_events: SpecialEvent,
+1 -1
src/difficulty/gameplay_event.rs
··· 16 16 #[serde(rename = "b")] 17 17 pub beat: f32, 18 18 #[serde(rename = "m")] 19 - pub bpm: i32, 19 + pub bpm: f32, 20 20 }
+2 -2
src/difficulty/lightshow.rs
··· 20 20 /// The value represents the total difference between the first and last step. 21 21 /// ### Step: 22 22 /// The value represents the different between the current and next step. 23 - #[derive(Default)] 24 - DistributionType { 23 + #[derive(Default, Copy)] 24 + DistributionType: i32 { 25 25 #[default] 26 26 Wave = 1, 27 27 Step = 2,
+1 -1
src/difficulty/lightshow/basic.rs
··· 41 41 #[serde(rename_all = "camelCase")] 42 42 pub struct SpecialEvent { 43 43 #[serde(rename = "d")] 44 - pub keywords: Vec<Keyword>, 44 + pub keywords: Option<Vec<Keyword>>, 45 45 } 46 46 47 47 /// Allows basic event lanes to be overridden with environment-specific behaviour, using secret keys.
+6 -5
src/difficulty/lightshow/color.rs
··· 31 31 pub bright_dist_value: f32, 32 32 #[serde(rename = "b")] 33 33 pub bright_dist_effect_first: LooseBool, 34 + /// Only present in difficulty file V3.2 or higher. 34 35 #[serde(rename = "i")] 35 - pub bright_dist_easing: Easing, 36 + pub bright_dist_easing: Option<Easing>, 36 37 #[serde(rename = "e")] 37 38 pub data: Vec<ColorEventData>, 38 39 } ··· 53 54 } 54 55 55 56 loose_enum! { 56 - #[derive(Default)] 57 - ColorTransitionType { 57 + #[derive(Default, Copy)] 58 + ColorTransitionType: i32 { 58 59 #[default] 59 60 Instant = 0, 60 61 Transition = 1, ··· 63 64 } 64 65 65 66 loose_enum! { 66 - #[derive(Default)] 67 - LightColor { 67 + #[derive(Default, Copy)] 68 + LightColor: i32 { 68 69 #[default] 69 70 Primary = 0, 70 71 Secondary = 1,
+2 -2
src/difficulty/lightshow/easing.rs
··· 2 2 use simple_easing::*; 3 3 4 4 loose_enum! { 5 - #[derive(Default)] 6 - Easing { 5 + #[derive(Default, Copy)] 6 + Easing: i32 { 7 7 Linear = 0, 8 8 InQuad = 1, 9 9 OutQuad = 2,
+12 -7
src/difficulty/lightshow/filter.rs
··· 18 18 #[serde(rename = "r")] 19 19 pub reverse: LooseBool, 20 20 // V3.1 21 + /// Only present in difficulty file V3.1 or higher. 21 22 #[serde(rename = "c")] 22 - pub chunks: i32, 23 + pub chunks: Option<i32>, 24 + /// Only present in difficulty file V3.1 or higher. 23 25 #[serde(rename = "n")] 24 - pub random_behaviour: i32, 26 + pub random_behaviour: Option<i32>, 27 + /// Only present in difficulty file V3.1 or higher. 25 28 #[serde(rename = "s")] 26 - pub random_seed: i32, 29 + pub random_seed: Option<i32>, 30 + /// Only present in difficulty file V3.1 or higher. 27 31 #[serde(rename = "d")] 28 - pub limit_behaviour: i32, 32 + pub limit_behaviour: Option<i32>, 33 + /// Only present in difficulty file V3.1 or higher. 29 34 #[serde(rename = "l")] 30 - pub limit_percent: i32, 35 + pub limit_percent: Option<f32>, 31 36 } 32 37 33 38 loose_enum! { ··· 43 48 /// Alternates selecting and not selecting lights. 44 49 /// - Parameter 1 is the index of the first light that will be selected, starting at 0. 45 50 /// - Parameter 2 determines the number of lights that will be skipped between selections. 46 - #[derive(Default)] 47 - FilterType { 51 + #[derive(Default, Copy)] 52 + FilterType: i32 { 48 53 #[default] 49 54 //Todo Doesn't match wiki 50 55 Division = 1,
+12 -11
src/difficulty/lightshow/rotation.rs
··· 28 28 #[serde(rename = "t")] 29 29 pub rotation_dist_type: DistributionType, 30 30 #[serde(rename = "s")] 31 - pub rotation_dist_value: i32, 31 + pub rotation_dist_value: f32, 32 32 #[serde(rename = "b")] 33 33 pub rotation_dist_effect_first: LooseBool, 34 + /// Only present in difficulty file V3.2 or higher. 34 35 #[serde(rename = "i")] 35 - pub rotation_dist_easing: Easing, 36 + pub rotation_dist_easing: Option<Easing>, 36 37 #[serde(rename = "a")] 37 38 pub axis: i32, 38 39 #[serde(rename = "r")] ··· 42 43 } 43 44 44 45 loose_enum! { 45 - #[derive(Default)] 46 - Axis { 46 + #[derive(Default, Copy)] 47 + Axis: i32 { 47 48 #[default] 48 49 X = 0, 49 50 Y = 1, ··· 60 61 pub transition_type: RotationTransitionType, 61 62 #[serde(rename = "e")] 62 63 pub easing: Easing, 63 - #[serde(rename = "l")] 64 - pub degrees: i32, 65 64 #[serde(rename = "r")] 65 + pub degrees: f32, 66 + #[serde(rename = "o")] 66 67 pub direction: RotationDirection, 67 - #[serde(rename = "o")] 68 + #[serde(rename = "l")] 68 69 pub loops: i32, 69 70 } 70 71 ··· 75 76 /// and the values from the previous event will be used instead. 76 77 /// 77 78 /// More info [here](https://bsmg.wiki/mapping/map-format/lightshow.html#light-rotation-events-type). 78 - #[derive(Default)] 79 - RotationTransitionType { 79 + #[derive(Default, Copy)] 80 + RotationTransitionType: i32 { 80 81 #[default] 81 82 Transition = 0, 82 83 Extend = 1, ··· 86 87 loose_enum! { 87 88 /// Determines the direction that the rotation event will rotate. 88 89 /// Automatic will choose the shortest distance. 89 - #[derive(Default)] 90 - RotationDirection { 90 + #[derive(Default, Copy)] 91 + RotationDirection: i32 { 91 92 #[default] 92 93 Automatic = 0, 93 94 Clockwise = 1,
+7 -7
src/difficulty/playfield.rs
··· 19 19 } 20 20 21 21 loose_enum! { 22 - #[derive(Default)] 23 - NoteColor { 22 + #[derive(Default, Copy)] 23 + NoteColor: i32 { 24 24 #[default] 25 25 Left = 0, 26 26 Right = 1, ··· 28 28 } 29 29 30 30 loose_enum! { 31 - #[derive(Default)] 32 - CutDirection { 31 + #[derive(Default, Copy)] 32 + CutDirection: i32 { 33 33 #[default] 34 34 Up = 0, 35 35 Down = 1, ··· 122 122 } 123 123 124 124 loose_enum! { 125 - #[derive(Default)] 126 - MidAnchorMode { 125 + #[derive(Default, Copy)] 126 + MidAnchorMode: i32 { 127 127 #[default] 128 128 Straight = 0, 129 129 Clockwise = 1, ··· 155 155 #[serde(rename = "sc")] 156 156 pub link_count: i32, 157 157 #[serde(rename = "s")] 158 - pub link_squish: i32, 158 + pub link_squish: f32, 159 159 }
+57 -97
src/info.rs
··· 3 3 use serde_json::Value; 4 4 5 5 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 6 - pub struct InfoV2 { 6 + pub struct Beatmap { 7 7 #[serde(rename = "_version")] 8 8 pub version: String, 9 9 #[serde(rename = "_songName")] ··· 21 21 #[serde(rename = "_shuffle")] 22 22 pub shuffle: i32, 23 23 #[serde(rename = "_shufflePeriod")] 24 - pub shuffle_period: i32, 24 + pub shuffle_period: f32, 25 25 #[serde(rename = "_previewStartTime")] 26 26 pub preview_start_time: f32, 27 27 #[serde(rename = "_previewDuration")] ··· 34 34 pub environment: Environment, 35 35 #[serde(rename = "_allDirectionsEnvironmentName")] 36 36 pub all_directions_environment: AllDirectionEnvironment, 37 + /// Only present in info file V2.1 or higher. 37 38 #[serde(rename = "_environmentNames")] 38 - pub environments: Vec<Environment>, 39 + pub environments: Option<Vec<Environment>>, 39 40 #[serde(rename = "_colorSchemes")] 40 - pub color_schemes: Vec<Value>, // Todo 41 + /// Only present in info file V2.1 or higher. 42 + pub color_schemes: Option<Vec<Value>>, // Todo 41 43 #[serde(rename = "_difficultyBeatmapSets")] 42 44 pub difficulty_sets: Vec<DifficultySet>, 43 45 } ··· 45 47 // Todo: Serde rename is not supported by macro. 46 48 loose_enum! { 47 49 #[derive(Default)] 48 - Environment { 49 - #[doc(alias = "TheFirst")] 50 + Environment: String { 50 51 #[default] 51 - DefaultEnvironment = 0, 52 + TheFirst = "DefaultEnvironment", 52 53 53 - TriangleEnvironment = 1, 54 - NiceEnvironment = 2, 55 - BigMirrorEnvironment = 3, 56 - KDAEnvironment = 4, 57 - MonstercatEnvironment = 5, 58 - CrabRaveEnvironment = 6, 59 - DragonsEnvironment = 7, 60 - OriginsEnvironment = 8, 61 - PanicEnvironment = 9, 62 - RocketEnvironment = 10, 63 - GreenDayEnvironment = 11, 64 - GreenDayGrenadeEnvironment = 12, 65 - TimbalandEnvironment = 13, 66 - FitBeatEnvironment = 14, 67 - LinkinParkEnvironment = 15, 68 - BTSEnvironment = 16, 69 - KaleidoscopeEnvironment = 17, 70 - InterscopeEnvironment = 18, 71 - SkrillexEnvironment = 19, 72 - #[doc(alias = "BillieEilish")] 73 - BillieEnvironment = 20, 74 - #[doc(alias = "Spooky")] 75 - HalloweenEnvironment = 21, 76 - #[doc(alias = "LadyGaga")] 77 - GagaEnvironment = 22, 54 + Triangle = "TriangleEnvironment", 55 + Nice = "NiceEnvironment", 56 + BigMirror = "BigMirrorEnvironment", 57 + KDA = "KDAEnvironment", 58 + Monstercat = "MonstercatEnvironment", 59 + CrabRave = "CrabRaveEnvironment", 60 + Dragons = "DragonsEnvironment", 61 + Origins = "OriginsEnvironment", 62 + Panic = "PanicEnvironment", 63 + Rocket = "RocketEnvironment", 64 + GreenDay = "GreenDayEnvironment", 65 + GreenDayGrenade = "GreenDayGrenadeEnvironment", 66 + Timbaland = "TimbalandEnvironment", 67 + FitBeat = "FitBeatEnvironment", 68 + LinkinPark = "LinkinParkEnvironment", 69 + BTS = "BTSEnvironment", 70 + Kaleidoscope = "KaleidoscopeEnvironment", 71 + Interscope = "InterscopeEnvironment", 72 + Skrillex = "SkrillexEnvironment", 73 + BillieEilish = "BillieEnvironment", 74 + #[doc(alias = "Halloween")] 75 + Spooky = "HalloweenEnvironment", 76 + LadyGaga = "GagaEnvironment", 78 77 // V3: 79 - WeaveEnvironment = 23, 80 - #[doc(alias = "FallOutBoy")] 81 - PyroEnvironment = 24, 82 - EDMEnvironment = 25, 83 - TheSecondEnvironment = 26, 84 - LizzoEnvironment = 27, 85 - TheWeekndEnvironment = 28, 86 - RockMixtapeEnvironment = 29, 87 - Dragons2Environment = 30, 88 - Panic2Environment = 31, 89 - QueenEnvironment = 32, 78 + Weave = "WeaveEnvironment", 79 + #[doc(alias = "Pyro")] 80 + FallOutBoy = "PyroEnvironment", 81 + EDM = "EDMEnvironment", 82 + TheSecond = "TheSecondEnvironment", 83 + Lizzo = "LizzoEnvironment", 84 + TheWeeknd = "TheWeekndEnvironment", 85 + RockMixtape = "RockMixtapeEnvironment", 86 + Dragons2 = "Dragons2Environment", 87 + Panic2 = "Panic2Environment", 88 + Queen = "QueenEnvironment", 90 89 // Todo Add more. 91 90 } 92 91 } 93 92 94 93 loose_enum! { 95 94 #[derive(Default)] 96 - AllDirectionEnvironment { 95 + AllDirectionEnvironment: String { 97 96 #[default] 98 - GlassDesertEnvironment = 0, 97 + GlassDesert = "GlassDesertEnvironment", 99 98 } 100 99 } 101 100 ··· 107 106 pub difficulties: Vec<DifficultyInfo>, 108 107 } 109 108 110 - #[derive(Default, Debug, Clone, PartialEq)] 111 - pub enum Characteristic { 112 - #[default] 113 - Standard, 114 - NoArrows, 115 - OneSaber, 116 - Rotate360, 117 - Rotate90, 118 - Legacy, 119 - //Custom types. 120 - Lawless, 121 - Lightshow, 122 - Unknown(String), 123 - } 124 - 125 - // Todo Replace if macro gets expanded to support more types. 126 - impl<'de> serde::Deserialize<'de> for Characteristic { 127 - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 128 - where 129 - D: serde::Deserializer<'de>, 130 - { 131 - let val = String::deserialize(deserializer)?; 132 - Ok(match val.as_str() { 133 - "Standard" => Characteristic::Standard, 134 - "NoArrows" => Characteristic::NoArrows, 135 - "OneSaber" => Characteristic::OneSaber, 136 - "360Degree" => Characteristic::Rotate360, 137 - "90Degree" => Characteristic::Rotate90, 138 - "Legacy" => Characteristic::Legacy, 139 - "Lawless" => Characteristic::Lawless, 140 - "Lightshow" => Characteristic::Lightshow, 141 - s => Characteristic::Unknown(s.to_string()), 142 - }) 143 - } 144 - } 145 - 146 - impl serde::Serialize for Characteristic { 147 - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 148 - where 149 - S: serde::Serializer, 150 - { 151 - match self { 152 - Characteristic::Standard => serializer.serialize_str("Standard"), 153 - Characteristic::NoArrows => serializer.serialize_str("NoArrows"), 154 - Characteristic::OneSaber => serializer.serialize_str("OneSaber"), 155 - Characteristic::Rotate360 => serializer.serialize_str("360Degree"), 156 - Characteristic::Rotate90 => serializer.serialize_str("90Degree"), 157 - Characteristic::Legacy => serializer.serialize_str("Legacy"), 158 - Characteristic::Lawless => serializer.serialize_str("Legacy"), 159 - Characteristic::Lightshow => serializer.serialize_str("Lawless"), 160 - Characteristic::Unknown(s) => serializer.serialize_str(s), 161 - } 109 + loose_enum! { 110 + #[derive(Default)] 111 + Characteristic: String { 112 + #[default] 113 + Standard = "Standard", 114 + NoArrows = "NoArrows", 115 + OneSaber = "OneSaber", 116 + Rotate360 = "360Degree", 117 + Rotate90 = "90Degree", 118 + Legacy = "Legacy", 119 + //Custom types. 120 + Lawless = "Lawless", 121 + Lightshow = "Lightshow", 162 122 } 163 123 } 164 124
-15
src/lib.rs
··· 1 1 pub mod difficulty; 2 2 pub mod info; 3 3 pub mod macros; 4 - 5 - pub fn add(left: u64, right: u64) -> u64 { 6 - left + right 7 - } 8 - 9 - #[cfg(test)] 10 - pub mod tests { 11 - use super::*; 12 - 13 - #[test] 14 - fn it_works() { 15 - let result = add(2, 2); 16 - assert_eq!(result, 4); 17 - } 18 - }
+57 -8
src/macros.rs
··· 1 1 #[macro_export] 2 2 macro_rules! loose_enum { 3 + // Special case for strings: 3 4 ( 4 5 $(#[$outer:meta])* 5 - $name:ident 6 + $name:ident: String 7 + { 8 + $( 9 + $(#[$meta:meta])* 10 + $variant:ident = $value:expr 11 + ),+ $(,)? 12 + } 13 + ) => { 14 + #[derive(Debug, Clone, Eq, PartialEq)] 15 + $(#[$outer])* 16 + pub enum $name { 17 + $( 18 + $(#[$meta])* 19 + $variant 20 + ),+, 21 + Unknown(String), 22 + } 23 + 24 + impl<'de> serde::Deserialize<'de> for $name { 25 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 26 + where 27 + D: serde::Deserializer<'de>, 28 + { 29 + let val = String::deserialize(deserializer)?; 30 + Ok(match val.as_str() { 31 + $( $value => $name::$variant, )+ 32 + other => $name::Unknown(other.to_string()), 33 + }) 34 + } 35 + } 36 + 37 + impl serde::Serialize for $name { 38 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 39 + where 40 + S: serde::Serializer, 41 + { 42 + match self { 43 + $( 44 + $name::$variant => str::serialize($value, serializer), 45 + )+ 46 + $name::Unknown(val) => str::serialize(val, serializer), 47 + } 48 + } 49 + } 50 + }; 51 + // All other types: 52 + ( 53 + $(#[$outer:meta])* 54 + $name:ident: $ty:ident 6 55 { 7 56 $( 8 57 $(#[$meta:meta])* ··· 10 59 ),+ $(,)? 11 60 } 12 61 ) => { 13 - #[derive(Debug, Clone, Copy, Eq, PartialEq)] 62 + #[derive(Debug, Clone, Eq, PartialEq)] 14 63 $(#[$outer])* 15 64 pub enum $name { 16 65 $( 17 66 $(#[$meta])* 18 67 $variant 19 68 ),+, 20 - Unknown(i32), 69 + Unknown($ty), 21 70 } 22 71 23 72 impl<'de> serde::Deserialize<'de> for $name { ··· 25 74 where 26 75 D: serde::Deserializer<'de>, 27 76 { 28 - let val = i32::deserialize(deserializer)?; 77 + let val = $ty::deserialize(deserializer)?; 29 78 Ok(match val { 30 79 $( $value => $name::$variant, )+ 31 80 other => $name::Unknown(other), ··· 40 89 { 41 90 match self { 42 91 $( 43 - $name::$variant => serializer.serialize_i32($value), 92 + $name::$variant => $ty::serialize(&$value, serializer), 44 93 )+ 45 - $name::Unknown(val) => serializer.serialize_i32(*val), 94 + $name::Unknown(val) => $ty::serialize(val, serializer), 46 95 } 47 96 } 48 97 } ··· 50 99 } 51 100 52 101 loose_enum! { 53 - #[derive(Default)] 54 - LooseBool { 102 + #[derive(Default, Copy)] 103 + LooseBool: i32 { 55 104 #[default] 56 105 False = 0, 57 106 True = 1,
+1
test_maps/README.md
··· 1 + Put testing maps in this directory.
+39
tests/parse_map.rs
··· 1 + use bsru::difficulty::Difficulty; 2 + use bsru::info::Beatmap; 3 + use std::fs; 4 + 5 + #[test] 6 + fn parse_beatmaps() { 7 + let paths = fs::read_dir("test_maps").unwrap().filter_map(|result| { 8 + if let Ok(dir) = result { 9 + if dir.path().is_dir() { 10 + return Some(dir.path()); 11 + } 12 + } 13 + None 14 + }); 15 + 16 + for path in paths { 17 + println!("{path:?}"); 18 + let mut info_path = path.clone(); 19 + info_path.push("Info.dat"); 20 + 21 + let info_file = fs::File::open(&info_path).expect("Map missing info file"); 22 + let map: Beatmap = serde_json::from_reader(info_file).expect("Invalid info file"); 23 + 24 + for set in map.difficulty_sets { 25 + println!("\t{}", set.characteristic); 26 + 27 + for dif in set.difficulties { 28 + println!("\t\t{} ({})", dif.name, dif.rank); 29 + 30 + let mut dif_path = path.clone(); 31 + dif_path.push(dif.file); 32 + 33 + let dif_file = fs::File::open(&dif_path).expect("Map missing difficulty file"); 34 + let _: Difficulty = 35 + serde_json::from_reader(dif_file).expect("Invalid difficulty file"); 36 + } 37 + } 38 + } 39 + }