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.

Merge pull request #18 from AlephCubed/fx-events

Add support for FX events.

authored by

AlephCubed and committed by
GitHub
65158795 4a80d032

+526 -6
+12 -1
Cargo.lock
··· 156 156 157 157 [[package]] 158 158 name = "bsru" 159 - version = "0.2.0" 159 + version = "0.3.0-beta.1" 160 160 dependencies = [ 161 161 "bevy_color", 162 162 "bevy_reflect", 163 + "indexmap", 164 + "ordered-float", 163 165 "serde", 164 166 "serde_json", 165 167 "simple-easing", ··· 390 392 version = "1.21.3" 391 393 source = "registry+https://github.com/rust-lang/crates.io-index" 392 394 checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 395 + 396 + [[package]] 397 + name = "ordered-float" 398 + version = "5.0.0" 399 + source = "registry+https://github.com/rust-lang/crates.io-index" 400 + checksum = "e2c1f9f56e534ac6a9b8a4600bdf0f530fb393b5f393e7b4d03489c3cf0c3f01" 401 + dependencies = [ 402 + "num-traits", 403 + ] 393 404 394 405 [[package]] 395 406 name = "parking_lot"
+3 -1
Cargo.toml
··· 1 1 [package] 2 2 name = "bsru" 3 - version = "0.2.0" 3 + version = "0.3.0-beta.1" 4 4 edition = "2024" 5 5 description = "Beatsaber Rust Utilities: A Beatsaber V3 parsing library." 6 6 categories = ["game-development", "data-structures", "parser-implementations"] ··· 18 18 "std", 19 19 ] } 20 20 bevy_reflect = { version = "0.16.1", optional = true, default-features = false } 21 + indexmap = "2.10.0" 22 + ordered-float = "5.0.0" 21 23 serde = { version = "1.0.219", features = ["derive"] } 22 24 serde_json = "1.0.140" 23 25 simple-easing = "1.0.1"
+2
src/difficulty.rs
··· 46 46 pub color_event_boxes: Vec<ColorEventBox>, 47 47 #[serde(rename = "lightRotationEventBoxGroups")] 48 48 pub rotation_event_boxes: Vec<RotationEventBox>, 49 + #[serde(flatten)] 50 + pub fx_event_boxes: Option<FxEventContainer>, 49 51 /// > Only present in difficulty file V3.2 or higher. 50 52 #[serde(rename = "lightTranslationEventBoxGroups")] 51 53 pub translation_event_boxes: Option<Vec<TranslationEventBox>>,
+3
src/difficulty/lightshow/group.rs
··· 1 1 //! The advanced group lighting system events. 2 2 3 3 pub mod color; 4 + pub mod fx; 4 5 pub mod rotation; 5 6 pub mod translation; 6 7 7 8 #[doc(hidden)] 8 9 pub use color::*; 10 + #[doc(hidden)] 11 + pub use fx::*; 9 12 #[doc(hidden)] 10 13 pub use rotation::*; 11 14 #[doc(hidden)]
+3 -2
src/difficulty/lightshow/group/color.rs
··· 55 55 /// A value of zero will have no effect. 56 56 #[serde(rename = "w")] 57 57 pub beat_dist_value: f32, 58 + 59 + #[serde(rename = "t")] 60 + pub bright_dist_type: DistributionType, 58 61 /// The strength of the brightness distribution. Dependent on the [distribution type](Self::bright_dist_type). 59 62 /// 60 63 /// A value of zero will have no effect. 61 - #[serde(rename = "t")] 62 - pub bright_dist_type: DistributionType, 63 64 #[serde(rename = "r")] 64 65 pub bright_dist_value: f32, 65 66 /// Whether the first [`ColorEventData`] of the group will be effected by brightness distribution.
+501
src/difficulty/lightshow/group/fx.rs
··· 1 + //! Events that control animations unique to each environment. 2 + //! 3 + //! Unlike the other V3 group event types, FX events use a template-like JSON syntax. 4 + //! In order to have standardized structure across all V3 events, custom serialization has been written in [`FxEventContainer`]. 5 + //! Because of this, neither [`FxEventBox`] nor [`FxEventGroup`] implement [`Serialize`] nor [`Deserialize`] directly. 6 + 7 + use crate::difficulty::lightshow::DistributionType; 8 + use crate::difficulty::lightshow::easing::Easing; 9 + use crate::difficulty::lightshow::filter::Filter; 10 + use crate::utils::LooseBool; 11 + use crate::{TransitionType, impl_event_box, impl_event_data, impl_event_group, impl_timed}; 12 + use indexmap::IndexSet; 13 + use ordered_float::OrderedFloat; 14 + use serde::ser::SerializeStruct; 15 + use serde::{Deserialize, Deserializer, Serialize, Serializer}; 16 + use std::ops::{Deref, DerefMut}; 17 + 18 + /// Contains a list of [`FxEventBox`] as well as the [`Serialize`] and [`Deserialize`] implementations for FX events. 19 + #[derive(Debug, Clone, PartialEq, Default)] 20 + #[cfg_attr( 21 + feature = "bevy_reflect", 22 + derive(bevy_reflect::Reflect), 23 + reflect(Debug, Clone, PartialEq) 24 + )] 25 + pub struct FxEventContainer { 26 + pub event_boxes: Vec<FxEventBox>, 27 + } 28 + 29 + impl Deref for FxEventContainer { 30 + type Target = Vec<FxEventBox>; 31 + 32 + fn deref(&self) -> &Self::Target { 33 + &self.event_boxes 34 + } 35 + } 36 + 37 + impl DerefMut for FxEventContainer { 38 + fn deref_mut(&mut self) -> &mut Self::Target { 39 + &mut self.event_boxes 40 + } 41 + } 42 + 43 + /// The format that is actually stored in JSON. 44 + #[derive(Deserialize)] 45 + struct FxEventInput { 46 + #[serde(rename = "vfxEventBoxGroups")] 47 + event_boxes: Vec<FxEventBoxRaw>, 48 + #[serde(rename = "_fxEventsCollection")] 49 + arrays: FxEventArrays, 50 + } 51 + 52 + #[derive(Deserialize, Serialize)] 53 + struct FxEventArrays { 54 + #[serde(rename = "_fl")] 55 + event_data: Vec<FxEventData>, 56 + } 57 + 58 + impl<'de> Deserialize<'de> for FxEventContainer { 59 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 60 + where 61 + D: Deserializer<'de>, 62 + { 63 + let FxEventInput { 64 + event_boxes, 65 + arrays, 66 + } = FxEventInput::deserialize(deserializer)?; 67 + 68 + let event_boxes = event_boxes 69 + .into_iter() 70 + .map(|raw_box| { 71 + let groups = raw_box 72 + .groups 73 + .into_iter() 74 + .map(|raw_group| { 75 + let data = raw_group 76 + .data_ids 77 + .into_iter() 78 + .map(|id| { 79 + arrays.event_data.get(id).cloned().ok_or_else(|| { 80 + serde::de::Error::custom(format!( 81 + "Missing FxEventData with id {}", 82 + id 83 + )) 84 + }) 85 + }) 86 + .collect::<Result<Vec<FxEventData>, _>>()?; 87 + 88 + Ok(FxEventGroup { 89 + filter: raw_group.filter, 90 + beat_dist_type: raw_group.beat_dist_type, 91 + beat_dist_value: raw_group.beat_dist_value, 92 + fx_dist_type: raw_group.fx_dist_type, 93 + fx_dist_value: raw_group.fx_dist_value, 94 + fx_dist_effect_first: raw_group.fx_dist_effect_first, 95 + fx_dist_easing: raw_group.fx_dist_easing, 96 + data, 97 + }) 98 + }) 99 + .collect::<Result<Vec<FxEventGroup>, _>>()?; 100 + 101 + Ok(FxEventBox { 102 + beat: raw_box.beat, 103 + group_id: raw_box.group_id, 104 + groups, 105 + }) 106 + }) 107 + .collect::<Result<Vec<FxEventBox>, _>>()?; 108 + 109 + Ok(FxEventContainer { event_boxes }) 110 + } 111 + } 112 + 113 + // Todo avoid allocations. 114 + impl Serialize for FxEventContainer { 115 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 116 + where 117 + S: Serializer, 118 + { 119 + let mut event_data: IndexSet<FxEventDataKey> = IndexSet::new(); 120 + 121 + // Todo Deduplicate. 122 + let raw_boxes = self 123 + .event_boxes 124 + .iter() 125 + .map(|event_box| { 126 + let groups_raw = event_box 127 + .groups 128 + .iter() 129 + .map(|event_group| { 130 + let mut ids = Vec::new(); 131 + 132 + for data in &event_group.data { 133 + let data_key: FxEventDataKey = data.into(); 134 + 135 + let index = event_data.get_index_of(&data_key).unwrap_or_else(|| { 136 + let index = event_data.len(); 137 + event_data.insert(data_key); 138 + index 139 + }); 140 + 141 + ids.push(index); 142 + } 143 + 144 + FxEventGroupRaw { 145 + filter: event_group.filter.clone(), 146 + beat_dist_type: event_group.beat_dist_type.clone(), 147 + beat_dist_value: event_group.beat_dist_value, 148 + fx_dist_type: event_group.fx_dist_type.clone(), 149 + fx_dist_value: event_group.fx_dist_value, 150 + fx_dist_effect_first: event_group.fx_dist_effect_first, 151 + fx_dist_easing: event_group.fx_dist_easing.clone(), 152 + data_ids: ids, 153 + } 154 + }) 155 + .collect::<Vec<FxEventGroupRaw>>(); 156 + 157 + FxEventBoxRaw { 158 + beat: event_box.beat, 159 + group_id: event_box.group_id, 160 + groups: groups_raw, 161 + } 162 + }) 163 + .collect::<Vec<FxEventBoxRaw>>(); 164 + 165 + let mut state = serializer.serialize_struct("FxEventContainer", 2)?; 166 + state.serialize_field("vfxEventBoxGroups", &raw_boxes)?; 167 + state.serialize_field( 168 + "_fxEventsCollection", 169 + &FxEventArrays { 170 + event_data: event_data.into_iter().map(FxEventData::from).collect(), 171 + }, 172 + )?; 173 + state.end() 174 + } 175 + } 176 + 177 + /// A collection of [`FxEventGroup`]s that share the same group ID and beat. 178 + /// 179 + /// Does not implement [`Serialize`] nor [`Deserialize`]. For more info, see the [module docs](super::fx). 180 + #[derive(Debug, Clone, PartialEq)] 181 + #[cfg_attr( 182 + feature = "bevy_reflect", 183 + derive(bevy_reflect::Reflect), 184 + reflect(Debug, Clone, PartialEq) 185 + )] 186 + pub struct FxEventBox { 187 + /// The time the event takes place. 188 + pub beat: f32, 189 + /// The ID of the collection of objects that this event effects. 190 + pub group_id: i32, 191 + pub groups: Vec<FxEventGroup>, 192 + } 193 + 194 + /// The raw JSON structure that uses [data IDs](FxEventGroupRaw::data_ids) rather than actual [event data](FxEventData). 195 + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 196 + struct FxEventBoxRaw { 197 + #[serde(rename = "b")] 198 + beat: f32, 199 + #[serde(rename = "g")] 200 + group_id: i32, 201 + #[serde(rename = "e")] 202 + groups: Vec<FxEventGroupRaw>, 203 + } 204 + 205 + impl Default for FxEventBox { 206 + fn default() -> Self { 207 + Self { 208 + beat: 0.0, 209 + group_id: 0, 210 + groups: vec![FxEventGroup::default()], 211 + } 212 + } 213 + } 214 + 215 + impl_timed!(FxEventBox::beat); 216 + impl_event_box!(FxEventBox, FxEventGroup, FxEventData); 217 + 218 + /// A collection of [`FxEventData`] that share the same [`Filter`] and distribution. 219 + /// 220 + /// Does not implement [`Serialize`] nor [`Deserialize`]. For more info, see the [module docs](super::fx). 221 + #[derive(Debug, Clone, PartialEq)] 222 + #[cfg_attr( 223 + feature = "bevy_reflect", 224 + derive(bevy_reflect::Reflect), 225 + reflect(Debug, Clone, PartialEq) 226 + )] 227 + pub struct FxEventGroup { 228 + pub filter: Filter, 229 + pub beat_dist_type: DistributionType, 230 + /// The strength of the beat distribution. Dependent on the [distribution type](Self::beat_dist_type). 231 + /// 232 + /// A value of zero will have no effect. 233 + pub beat_dist_value: f32, 234 + pub fx_dist_type: DistributionType, 235 + /// The strength of the brightness distribution. Dependent on the [distribution type](Self::fx_dist_type). 236 + /// 237 + /// A value of zero will have no effect. 238 + pub fx_dist_value: f32, 239 + /// Whether the first [`FxEventData`] of the group will be effected by brightness distribution. 240 + pub fx_dist_effect_first: LooseBool, 241 + pub fx_dist_easing: Option<Easing>, 242 + /// In the actual JSON structure, this is a list of indexes to a separate list of event data. 243 + /// For consistency, this is merged during parsing. 244 + pub data: Vec<FxEventData>, 245 + } 246 + 247 + /// The raw JSON structure that uses [data IDs](self::data_ids) rather than actual [event data](FxEventData). 248 + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 249 + struct FxEventGroupRaw { 250 + #[serde(rename = "f")] 251 + filter: Filter, 252 + #[serde(rename = "d")] 253 + beat_dist_type: DistributionType, 254 + #[serde(rename = "w")] 255 + beat_dist_value: f32, 256 + #[serde(rename = "t")] 257 + fx_dist_type: DistributionType, 258 + #[serde(rename = "s")] 259 + fx_dist_value: f32, 260 + #[serde(rename = "b")] 261 + fx_dist_effect_first: LooseBool, 262 + #[serde(rename = "i")] 263 + fx_dist_easing: Option<Easing>, 264 + #[serde(rename = "l")] 265 + data_ids: Vec<usize>, 266 + } 267 + 268 + impl Default for FxEventGroup { 269 + fn default() -> Self { 270 + Self { 271 + filter: Default::default(), 272 + beat_dist_type: Default::default(), 273 + beat_dist_value: 0.0, 274 + fx_dist_type: Default::default(), 275 + fx_dist_value: 0.0, 276 + fx_dist_effect_first: Default::default(), 277 + fx_dist_easing: Some(Easing::Linear), 278 + data: vec![FxEventData::default()], 279 + } 280 + } 281 + } 282 + 283 + impl_event_group!(FxEventGroup::get_fx_offset, FxEventData); 284 + 285 + impl FxEventGroup { 286 + /// Returns the FX value that the event will be offset for a given light ID. 287 + /// # Panics 288 + /// Will panic if the light ID is greater than or equal to the group size. 289 + #[deprecated(note = "Experimental. Does not consider random in filter calculations.")] 290 + #[allow(deprecated)] 291 + pub fn get_fx_offset(&self, light_id: i32, group_size: i32) -> f32 { 292 + self.fx_dist_type.compute_value_offset( 293 + light_id, 294 + group_size, 295 + &self.filter, 296 + self.fx_dist_value, 297 + self.data.last().map(|data| data.beat_offset), 298 + self.fx_dist_easing, 299 + ) 300 + } 301 + } 302 + 303 + /// The lowest-level group event type, which determines the base value of the event. 304 + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 305 + #[cfg_attr( 306 + feature = "bevy_reflect", 307 + derive(bevy_reflect::Reflect), 308 + reflect(Debug, Clone, PartialEq) 309 + )] 310 + pub struct FxEventData { 311 + /// The number of beats the event will be offset from the [`FxEventBox`]'s beat. 312 + #[serde(rename = "b")] 313 + pub beat_offset: f32, 314 + #[serde(rename = "p")] 315 + pub transition_type: TransitionType, 316 + #[serde(rename = "i")] 317 + pub easing: Easing, 318 + /// The base value of the effect. 319 + #[serde(rename = "v")] 320 + pub value: f32, 321 + } 322 + 323 + /// A `PartialEq` and `Hash` version of [`FxEventData`], allowing for deduplication. 324 + #[derive(Debug, Clone, Eq, PartialEq, Hash)] 325 + struct FxEventDataKey { 326 + beat_offset: OrderedFloat<f32>, 327 + transition_type: TransitionType, 328 + easing: Easing, 329 + value: OrderedFloat<f32>, 330 + } 331 + 332 + impl From<&FxEventData> for FxEventDataKey { 333 + fn from(value: &FxEventData) -> Self { 334 + Self { 335 + beat_offset: value.beat_offset.into(), 336 + transition_type: value.transition_type, 337 + easing: value.easing, 338 + value: value.value.into(), 339 + } 340 + } 341 + } 342 + 343 + impl From<FxEventDataKey> for FxEventData { 344 + fn from(value: FxEventDataKey) -> Self { 345 + Self { 346 + beat_offset: value.beat_offset.into(), 347 + transition_type: value.transition_type, 348 + easing: value.easing, 349 + value: value.value.into(), 350 + } 351 + } 352 + } 353 + 354 + impl Default for FxEventData { 355 + fn default() -> Self { 356 + Self { 357 + beat_offset: 0.0, 358 + transition_type: Default::default(), 359 + easing: Easing::default(), 360 + value: 1.0, 361 + } 362 + } 363 + } 364 + 365 + impl_event_data!(FxEventData); 366 + 367 + #[cfg(test)] 368 + mod tests { 369 + use super::*; 370 + use serde_json::{Value, json}; 371 + 372 + fn get_test_container() -> FxEventContainer { 373 + FxEventContainer { 374 + event_boxes: vec![FxEventBox { 375 + beat: 2.0, 376 + group_id: 0, 377 + groups: vec![get_test_group(), get_test_group()], 378 + }], 379 + } 380 + } 381 + 382 + fn get_test_group() -> FxEventGroup { 383 + FxEventGroup { 384 + filter: Default::default(), 385 + beat_dist_type: DistributionType::Wave, 386 + beat_dist_value: 1.0, 387 + fx_dist_type: DistributionType::Wave, 388 + fx_dist_value: 1.0, 389 + fx_dist_effect_first: LooseBool::True, 390 + fx_dist_easing: Some(Easing::None), 391 + data: vec![FxEventData { 392 + beat_offset: 0.0, 393 + transition_type: TransitionType::Transition, 394 + easing: Easing::Linear, 395 + value: 100.0, 396 + }], 397 + } 398 + } 399 + 400 + fn get_test_json() -> Value { 401 + json!( 402 + { 403 + "vfxEventBoxGroups": 404 + [ 405 + { 406 + "b": 2.0, 407 + "g": 0, 408 + "e": 409 + [ 410 + { 411 + "f": 412 + { 413 + "c": 0, 414 + "f": 1, 415 + "p": 1, 416 + "t": 0, 417 + "r": 0, 418 + "n": 0, 419 + "s": 0, 420 + "l": 1.0, 421 + "d": 0 422 + }, 423 + "w": 1.0, 424 + "d": 1, 425 + "s": 1.0, 426 + "t": 1, 427 + "b": 1, 428 + "i": -1, 429 + "l": 430 + [ 431 + 0 432 + ] 433 + }, 434 + { 435 + "f": 436 + { 437 + "c": 0, 438 + "f": 1, 439 + "p": 1, 440 + "t": 0, 441 + "r": 0, 442 + "n": 0, 443 + "s": 0, 444 + "l": 1.0, 445 + "d": 0 446 + }, 447 + "w": 1.0, 448 + "d": 1, 449 + "s": 1.0, 450 + "t": 1, 451 + "b": 1, 452 + "i": -1, 453 + "l": 454 + [ 455 + 0 456 + ] 457 + } 458 + ] 459 + } 460 + ], 461 + "_fxEventsCollection": 462 + { 463 + "_fl": 464 + [ 465 + { 466 + "b": 0.0, 467 + "p": 0, 468 + "i": 0, 469 + "v": 100.0 470 + } 471 + ] 472 + } 473 + } 474 + ) 475 + } 476 + 477 + #[test] 478 + fn test_deserialize() { 479 + let container: FxEventContainer = serde_json::from_value(get_test_json()).unwrap(); 480 + 481 + assert_eq!(container, get_test_container()); 482 + } 483 + 484 + #[test] 485 + fn test_serialize() { 486 + let out_json = serde_json::to_value(&get_test_container()).unwrap(); 487 + 488 + assert_eq!(out_json, get_test_json()); 489 + } 490 + 491 + #[test] 492 + fn test_round_trip() { 493 + let container: FxEventContainer = serde_json::from_value(get_test_json()).unwrap(); 494 + 495 + let out_json = serde_json::to_string_pretty(&container).unwrap(); 496 + 497 + let round_trip: FxEventContainer = serde_json::from_str(&out_json).unwrap(); 498 + 499 + assert_eq!(container, round_trip); 500 + } 501 + }
+2 -2
src/utils.rs
··· 13 13 ),+ $(,)? 14 14 } 15 15 ) => { 16 - #[derive(Debug, Clone, Eq, PartialEq)] 16 + #[derive(Debug, Clone, Eq, PartialEq, Hash)] 17 17 #[cfg_attr( 18 18 feature = "bevy_reflect", 19 19 derive(bevy_reflect::Reflect), ··· 82 82 ),+ $(,)? 83 83 } 84 84 ) => { 85 - #[derive(Debug, Clone, Eq, PartialEq)] 85 + #[derive(Debug, Clone, Eq, PartialEq, Hash)] 86 86 #[cfg_attr( 87 87 feature = "bevy_reflect", 88 88 derive(bevy_reflect::Reflect),