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.

at dev 574 lines 19 kB view raw
1//! Controls which light IDs are affected by an event. 2 3use crate::loose_bool::LooseBool; 4use loose_enum::loose_enum; 5use serde::{Deserialize, Serialize}; 6 7/// Controls which light IDs are affected by an event. 8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 9#[cfg_attr( 10 feature = "bevy_reflect", 11 derive(bevy_reflect::Reflect), 12 reflect(Debug, Clone, PartialEq) 13)] 14pub struct Filter { 15 // V3.0: 16 /// Controls how [`parameter1`](Self::parameter1) and [`parameter2`](Self::parameter2) are used. 17 #[serde(rename = "f")] 18 pub filter_type: FilterType, 19 /// Dependent on the [`FilterType`]. 20 #[serde(rename = "p")] 21 pub parameter1: i32, 22 /// Dependent on the [`FilterType`]. 23 #[serde(rename = "t")] 24 pub parameter2: i32, 25 /// If true, the filter will start at the end of a group and work backwards. 26 #[serde(rename = "r")] 27 pub reverse: LooseBool, 28 // V3.1: 29 /// > Only present in difficulty file V3.1 or higher. 30 /// 31 /// Chunks will divide the group into multiple chunks, which will each behave as a single object. 32 /// 33 /// To see this in practice, check out [this video](https://youtube.com/watch?v=NJPPBvyHJjg&t=197). 34 #[serde(rename = "c")] 35 pub chunks: Option<i32>, 36 /// > Only present in difficulty file V3.1 or higher. 37 #[serde(rename = "n")] 38 pub random_behaviour: Option<RandomBehaviour>, 39 /// > Only present in difficulty file V3.1 or higher. 40 #[serde(rename = "s")] 41 pub random_seed: Option<i32>, 42 /// > Only present in difficulty file V3.1 or higher. 43 /// 44 /// Determines how [the limit](Filter::limit_percent) behaves. This is applied *after* the [`FilterType`] behaviour. 45 /// 46 /// To see this in practice, check out [this video](https://youtube.com/watch?v=NJPPBvyHJjg&t=338). 47 #[serde(rename = "d")] 48 pub limit_behaviour: Option<LimitBehaviour>, 49 /// > Only present in difficulty file V3.1 or higher. 50 /// 51 /// A value from 0.0 to 1.0 which represents the percent of lights that will be effected, 52 /// and the behaviour is dependent on [`LimitBehaviour`]. 53 #[serde(rename = "l")] 54 pub limit_percent: Option<f32>, 55} 56 57impl Default for Filter { 58 fn default() -> Self { 59 Self { 60 filter_type: FilterType::default(), 61 parameter1: 1, 62 parameter2: 0, 63 reverse: LooseBool::False, 64 chunks: Some(0), 65 random_behaviour: Some(RandomBehaviour::None), 66 random_seed: Some(0), 67 limit_behaviour: Some(LimitBehaviour::None), 68 limit_percent: Some(1.0), 69 } 70 } 71} 72 73impl Filter { 74 /// Returns true if the light ID is in the filter. 75 /// # Undefined 76 /// If the [`FilterType`] is `Undefined` then the result will be `true`. 77 /// # Panics 78 /// Will panic if the light ID is greater than or equal to the group size. 79 #[must_use] 80 #[inline] 81 #[deprecated(note = "Experimental. Does not consider random in calculations.")] 82 pub fn is_in_filter(&self, mut light_id: i32, mut group_size: i32) -> bool { 83 assert!(light_id < group_size); 84 85 if let Some(limit) = self.limit_percent 86 && limit > 0.0 87 && light_id >= (group_size as f32 * limit) as i32 88 { 89 return false; 90 } 91 92 if self.reverse.is_true() { 93 light_id = group_size - light_id - 1; 94 } 95 96 if let Some(chunks) = self.chunks 97 && chunks > 0 98 && chunks < group_size 99 { 100 light_id = (light_id as f32 / (group_size as f32 / chunks as f32)) as i32; 101 group_size = chunks; 102 } 103 104 match self.filter_type { 105 FilterType::Division => { 106 let start = self.parameter2 * group_size / self.parameter1.max(1); 107 let end = (self.parameter2 + 1) * group_size / self.parameter1.max(1); 108 light_id >= start && light_id < end.max(start + 1) 109 } 110 FilterType::StepAndOffset => { 111 let offset_light_id = light_id - self.parameter1; 112 offset_light_id % self.parameter2.max(1) == 0 && offset_light_id >= 0 113 } 114 FilterType::Undefined(_) => true, 115 } 116 } 117 118 #[allow(deprecated)] 119 /// Returns the number of light chunks effected by the filter, but before applying the limit. 120 /// This is required for distribution calculations. 121 /// 122 /// Also see [`count_filtered`](Self::count_filtered). 123 /// # Undefined 124 /// If the [`FilterType`] is `Undefined` then the result will be the same as `group_size`. 125 #[must_use] 126 #[inline] 127 #[deprecated(note = "Experimental. Does not consider random in calculations.")] 128 pub(crate) fn count_filtered_without_limit(&self, mut group_size: i32) -> i32 { 129 if let Some(chunks) = self.chunks 130 && chunks > 0 131 && chunks < group_size 132 { 133 group_size = chunks; 134 } 135 136 match self.filter_type { 137 FilterType::Division => { 138 let start = self.parameter2 * group_size / self.parameter1.max(1); 139 let end = (self.parameter2 + 1) * group_size / self.parameter1.max(1); 140 end.max(start + 1) - start 141 } 142 FilterType::StepAndOffset => { 143 group_size / self.parameter2.max(1) - self.parameter1 / self.parameter2.max(1) 144 } 145 FilterType::Undefined(_) => group_size, 146 } 147 } 148 149 #[allow(deprecated)] 150 /// Returns the number of light chunks effected by the filter. 151 /// 152 /// Also see [`count_filtered_without_limit`](Self::count_filtered_without_limit). 153 /// # Undefined 154 /// If the [`FilterType`] is `Undefined` then the result will be the same as `group_size`. 155 #[must_use] 156 #[inline] 157 #[deprecated(note = "Experimental. Does not consider random in calculations.")] 158 #[allow(deprecated)] 159 pub fn count_filtered(&self, group_size: i32) -> i32 { 160 let filtered = self.count_filtered_without_limit(group_size); 161 162 if let Some(limit) = self.limit_percent 163 && limit > 0.0 164 { 165 (filtered as f32 * limit) as i32 166 } else { 167 filtered 168 } 169 } 170 171 #[allow(deprecated)] 172 /// Returns the light chunk ID relative to the [filtered count](Self::count_filtered). 173 /// # Undefined 174 /// If the [`FilterType`] is `Undefined` then the result will be the same as `light_id`. 175 /// # Panics 176 /// Will panic if the light ID is greater than or equal to the group size. 177 // Todo what is the behaviour when the light ID is not in the filter? 178 #[must_use] 179 #[inline] 180 #[deprecated(note = "Experimental. Does not consider random in calculations.")] 181 pub fn get_relative_index(&self, mut light_id: i32, mut group_size: i32) -> i32 { 182 assert!(light_id < group_size); 183 184 if self.reverse.is_true() { 185 light_id = group_size - light_id; 186 } 187 188 if let Some(chunks) = self.chunks 189 && chunks > 0 190 && chunks < group_size 191 { 192 light_id = (light_id as f32 / (group_size as f32 / chunks as f32)) as i32; 193 group_size = chunks; 194 } 195 196 match self.filter_type { 197 FilterType::Division => { 198 let start = self.parameter2 * group_size / self.parameter1.max(1); 199 light_id - start 200 } 201 FilterType::StepAndOffset => { 202 let offset_light_id = light_id - self.parameter1; 203 offset_light_id / self.parameter2.max(1) 204 } 205 FilterType::Undefined(_) => group_size, 206 } 207 } 208} 209 210loose_enum! { 211 /// Controls how a [`Filter`]'s [`parameter1`](Filter::parameter1) 212 /// and [`parameter2`](Filter::parameter2) values are used. 213 #[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Hash)] 214 #[cfg_attr( 215 feature = "bevy_reflect", 216 derive(bevy_reflect::Reflect), 217 reflect(Debug, Clone, PartialEq) 218 )] 219 pub enum FilterType: i32 { 220 /// Splits the group up into equal sections and selects one. 221 /// - [`parameter1`](Filter::parameter1) determines the number of sections. 222 /// It will be rounded up to the nearest multiple of the group size. 223 /// - [`parameter2`](Filter::parameter2) determines the section to select, starting at 0. 224 #[default] 225 Division = 1, 226 /// Alternates selecting and not selecting lights. 227 /// - [`parameter1`](Filter::parameter1) is the index of the first light that will be selected, starting at 0. 228 /// - [`parameter2`](Filter::parameter2) determines the number of IDs to move forward before selecting another light. 229 StepAndOffset = 2, 230 } 231} 232 233loose_enum!( 234 #[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Hash)] 235 #[cfg_attr( 236 feature = "bevy_reflect", 237 derive(bevy_reflect::Reflect), 238 reflect(Debug, Clone, PartialEq) 239 )] 240 pub enum RandomBehaviour: i32 { 241 #[default] 242 None = 0, 243 KeepOrder = 1, 244 RandomElements = 2, 245 } 246); 247 248loose_enum!( 249 /// Controls whether to extend wave distributions so they match the duration before the limit was applied. 250 /// 251 /// To see this in practice, check out [this video](https://youtube.com/watch?v=NJPPBvyHJjg&t=338). 252 /// 253 /// Includes the option to only enable for beat distribution and not value distribution, and vice versa. 254 #[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Hash)] 255 #[cfg_attr( 256 feature = "bevy_reflect", 257 derive(bevy_reflect::Reflect), 258 reflect(Debug, Clone, PartialEq) 259 )] 260 pub enum LimitBehaviour: i32 { 261 #[default] 262 None = 0, 263 Beat = 1, 264 Value = 2, 265 Both = 3, 266 } 267); 268 269impl LimitBehaviour { 270 /// Returns true if beat limiting is enabled, that is either `Beat` or `Both`. 271 pub fn beat_enabled(&self) -> bool { 272 matches!(self, LimitBehaviour::Beat | LimitBehaviour::Both) 273 } 274 275 /// Returns true if value limiting is enabled, that is either `Value` or `Both`. 276 pub fn value_enabled(&self) -> bool { 277 matches!(self, LimitBehaviour::Value | LimitBehaviour::Both) 278 } 279} 280 281#[allow(deprecated)] 282#[cfg(test)] 283mod tests { 284 use super::*; 285 286 #[test] 287 fn division_first_half() { 288 let filter = Filter { 289 filter_type: FilterType::Division, 290 parameter1: 2, 291 parameter2: 0, 292 ..Default::default() 293 }; 294 295 assert!((0..6).all(|i| filter.is_in_filter(i, 12))); 296 assert!((6..12).all(|i| !filter.is_in_filter(i, 12))); 297 assert_eq!(filter.count_filtered(12), 6); 298 assert!((0..6).all(|i| filter.get_relative_index(i, 12) == i)); 299 } 300 301 #[test] 302 fn division_second_half() { 303 let filter = Filter { 304 filter_type: FilterType::Division, 305 parameter1: 2, 306 parameter2: 1, 307 ..Default::default() 308 }; 309 310 assert!((0..6).all(|i| !filter.is_in_filter(i, 12))); 311 assert!((6..12).all(|i| filter.is_in_filter(i, 12))); 312 assert_eq!(filter.count_filtered(12), 6); 313 assert!((6..12).all(|i| filter.get_relative_index(i, 12) == i - 6)); 314 } 315 316 #[test] 317 fn division_first_half_rev() { 318 let filter = Filter { 319 filter_type: FilterType::Division, 320 parameter1: 2, 321 parameter2: 0, 322 reverse: LooseBool::True, 323 ..Default::default() 324 }; 325 326 assert!((0..6).all(|i| !filter.is_in_filter(i, 12))); 327 assert!((6..12).all(|i| filter.is_in_filter(i, 12))); 328 assert_eq!(filter.count_filtered(12), 6); 329 assert!((6..12).all(|i| filter.get_relative_index(i, 12) == 12 - i)); 330 } 331 332 #[test] 333 fn division_second_half_rev() { 334 let filter = Filter { 335 filter_type: FilterType::Division, 336 parameter1: 2, 337 parameter2: 1, 338 reverse: LooseBool::True, 339 ..Default::default() 340 }; 341 342 assert!((0..6).all(|i| filter.is_in_filter(i, 12))); 343 assert!((6..12).all(|i| !filter.is_in_filter(i, 12))); 344 assert_eq!(filter.count_filtered(12), 6); 345 assert!((0..6).all(|i| filter.get_relative_index(i, 12) == 6 - i)); 346 } 347 348 #[test] 349 fn division_select_all() { 350 let filter = Filter { 351 filter_type: FilterType::Division, 352 parameter1: 1, 353 parameter2: 0, 354 ..Default::default() 355 }; 356 357 assert!((0..12).all(|i| filter.is_in_filter(i, 12))); 358 assert_eq!(filter.count_filtered(12), 12); 359 assert!((0..12).all(|i| filter.get_relative_index(i, 12) == i)); 360 } 361 362 #[test] 363 fn division_larger_than_group_size() { 364 for i in 0..12 { 365 let filter = Filter { 366 filter_type: FilterType::Division, 367 parameter1: 12, 368 parameter2: i, 369 ..Default::default() 370 }; 371 372 let expected_id = match i { 373 0 => 0, 374 1 => 0, 375 2 => 1, 376 3 => 2, 377 4 => 2, 378 5 => 3, 379 6 => 4, 380 7 => 4, 381 8 => 5, 382 9 => 6, 383 10 => 6, 384 11 => 7, 385 _ => unreachable!(), 386 }; 387 388 assert!(filter.is_in_filter(expected_id, 8)); 389 assert!( 390 (0..8) 391 .filter(|x| *x != expected_id) 392 .all(|i| !filter.is_in_filter(i, 8)) 393 ); 394 assert_eq!(filter.count_filtered(8), 1); 395 assert_eq!(filter.get_relative_index(expected_id, 8), 0); 396 } 397 } 398 399 #[test] 400 fn step_select_all() { 401 let filter = Filter { 402 filter_type: FilterType::StepAndOffset, 403 parameter1: 0, 404 parameter2: 1, 405 ..Default::default() 406 }; 407 408 assert!((0..12).all(|i| filter.is_in_filter(i, 12))); 409 assert_eq!(filter.count_filtered(12), 12); 410 assert!((0..12).all(|i| filter.get_relative_index(i, 12) == i)); 411 } 412 413 #[test] 414 fn step_start_index() { 415 for outer in 0..12 { 416 let filter = Filter { 417 filter_type: FilterType::StepAndOffset, 418 parameter1: outer, 419 parameter2: 1, 420 ..Default::default() 421 }; 422 423 assert!((0..outer).all(|i| !filter.is_in_filter(i, 12))); 424 assert!((outer..12).all(|i| filter.is_in_filter(i, 12))); 425 assert_eq!(filter.count_filtered(12), 12 - outer); 426 assert!((outer..12).all(|i| filter.get_relative_index(i, 12) == i - outer)); 427 } 428 } 429 430 #[test] 431 fn step_every_other() { 432 let filter = Filter { 433 filter_type: FilterType::StepAndOffset, 434 parameter1: 0, 435 parameter2: 2, 436 ..Default::default() 437 }; 438 439 for i in 0..12 { 440 assert_eq!(filter.is_in_filter(i, 12), i % 2 == 0); 441 442 if i % 2 == 0 { 443 assert_eq!(filter.get_relative_index(i, 12), i / 2); 444 } 445 } 446 assert_eq!(filter.count_filtered(12), 6); 447 } 448 449 #[test] 450 fn step_every_other_offset() { 451 let filter = Filter { 452 filter_type: FilterType::StepAndOffset, 453 parameter1: 1, 454 parameter2: 2, 455 ..Default::default() 456 }; 457 458 for i in 0..12 { 459 assert_eq!(filter.is_in_filter(i, 12), i % 2 != 0); 460 461 if i % 2 != 0 { 462 assert_eq!(filter.get_relative_index(i, 12), i / 2); 463 } 464 } 465 assert_eq!(filter.count_filtered(12), 6); 466 } 467 468 #[test] 469 fn chunks_of_two() { 470 let filter = Filter { 471 chunks: Some(6), 472 ..Default::default() 473 }; 474 475 assert!((0..12).all(|i| filter.is_in_filter(i, 12))); 476 assert_eq!(filter.count_filtered(12), 6); 477 assert!((0..6).all(|i| { 478 filter.get_relative_index(i * 2, 12) == i 479 && filter.get_relative_index(i * 2 + 1, 12) == i 480 })); 481 } 482 483 #[test] 484 fn chunks_of_six() { 485 let filter = Filter { 486 chunks: Some(2), 487 ..Default::default() 488 }; 489 490 assert!((0..12).all(|i| filter.is_in_filter(i, 12))); 491 assert_eq!(filter.count_filtered(12), 2); 492 assert!((0..6).all(|i| filter.get_relative_index(i, 12) == 0)); 493 assert!((0..6).all(|i| filter.get_relative_index(i + 6, 12) == 1)); 494 } 495 496 #[test] 497 fn chunks_out_of_bounds() { 498 let filter = Filter { 499 chunks: Some(24), 500 ..Default::default() 501 }; 502 503 assert!((0..12).all(|i| filter.is_in_filter(i, 12))); 504 assert_eq!(filter.count_filtered(12), 12); 505 assert!((0..12).all(|i| filter.get_relative_index(i, 12) == i)); 506 } 507 508 #[test] 509 fn chunks_non_factor() { 510 let filter = Filter { 511 chunks: Some(3), 512 ..Default::default() 513 }; 514 515 assert!((0..8).all(|i| filter.is_in_filter(i, 8))); 516 assert_eq!(filter.count_filtered(8), 3); 517 assert!((0..3).all(|i| filter.get_relative_index(i, 8) == 0)); 518 assert!((3..6).all(|i| filter.get_relative_index(i, 8) == 1)); 519 assert!((6..8).all(|i| filter.get_relative_index(i, 8) == 2)); 520 } 521 522 #[test] 523 fn limit() { 524 let filter = Filter { 525 limit_percent: Some(0.5), 526 ..Default::default() 527 }; 528 529 assert!((0..6).all(|i| filter.is_in_filter(i, 12))); 530 assert!((6..12).all(|i| !filter.is_in_filter(i, 12))); 531 assert_eq!(filter.count_filtered(12), 6); 532 assert_eq!(filter.count_filtered_without_limit(12), 12); 533 assert!((0..6).all(|i| filter.get_relative_index(i, 12) == i)); 534 } 535 536 #[test] 537 fn limit_non_factor_none() { 538 let filter = Filter { 539 limit_percent: Some(0.01), 540 ..Default::default() 541 }; 542 543 assert!((0..8).all(|i| !filter.is_in_filter(i, 8))); 544 assert_eq!(filter.count_filtered(8), 0); 545 assert_eq!(filter.count_filtered_without_limit(8), 8); 546 } 547 548 #[test] 549 fn limit_non_factor_all_but_one() { 550 let filter = Filter { 551 limit_percent: Some(0.9), 552 ..Default::default() 553 }; 554 555 assert!((0..7).all(|i| filter.is_in_filter(i, 8))); 556 assert!(!filter.is_in_filter(7, 8)); 557 assert_eq!(filter.count_filtered(8), 7); 558 assert_eq!(filter.count_filtered_without_limit(8), 8); 559 assert!((0..7).all(|i| filter.get_relative_index(i, 8) == i)); 560 } 561 562 #[test] 563 fn limit_zero() { 564 let filter = Filter { 565 limit_percent: Some(0.0), 566 ..Default::default() 567 }; 568 569 assert!((0..12).all(|i| filter.is_in_filter(i, 12))); 570 assert_eq!(filter.count_filtered(12), 12); 571 assert_eq!(filter.count_filtered_without_limit(12), 12); 572 assert!((0..12).all(|i| filter.get_relative_index(i, 12) == i)); 573 } 574}