Beatsaber Rust Utilities: A Beatsaber V3 parsing library.
beatsaber
beatmap
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}