Beatsaber Rust Utilities: A Beatsaber V3 parsing library.
beatsaber
beatmap
1//! The advanced group lighting system events.
2
3pub mod color;
4pub mod fx;
5pub mod rotation;
6pub mod translation;
7
8#[doc(hidden)]
9pub use color::*;
10#[doc(hidden)]
11pub use fx::*;
12#[doc(hidden)]
13pub use rotation::*;
14#[doc(hidden)]
15pub use translation::*;
16
17use crate::difficulty::lightshow::filter::Filter;
18use crate::timing_traits::Timed;
19
20/// A collection of [`EventGroup`]s that share the same group ID and beat.
21pub trait EventBox: Timed {
22 type Group: EventGroup<Data = Self::Data>;
23 type Data: EventData;
24
25 fn get_groups(&self) -> &Vec<Self::Group>;
26}
27
28#[macro_export]
29#[doc(hidden)]
30macro_rules! impl_event_box {
31 ($ident:ident, $group:ident, $data:ident) => {
32 impl crate::difficulty::lightshow::group::EventBox for $ident {
33 type Group = $group;
34 type Data = $data;
35
36 fn get_groups(&self) -> &Vec<Self::Group> {
37 &self.groups
38 }
39 }
40 };
41}
42
43/// A collection of [`EventData`] that share the same [`Filter`] and distribution.
44pub trait EventGroup {
45 type Data: EventData;
46
47 fn get_filter(&self) -> &Filter;
48 fn get_data(&self) -> &Vec<Self::Data>;
49
50 /// Returns the number of beats that the event will be offset for a given light ID.
51 /// # Panics
52 /// Will panic if the light ID is greater than or equal to the group size.
53 #[deprecated(note = "Experimental. Does not consider random in filter calculations.")]
54 fn get_beat_offset(&self, light_id: i32, group_size: i32) -> f32;
55
56 /// Returns the value (i.e. brightness) that the event will be offset for a given light ID.
57 /// # Panics
58 /// Will panic if the light ID is greater than or equal to the group size.
59 #[deprecated(note = "Experimental. Does not consider random in filter calculations.")]
60 fn get_value_offset(&self, light_id: i32, group_size: i32) -> f32;
61
62 /// Returns the duration of the group in beats.
63 #[deprecated(note = "Experimental. Does not consider random in filter calculations.")]
64 fn get_duration(&self, group_size: i32) -> f32;
65}
66
67// Todo This macro could be a default trait implementation if other getters are added.
68#[macro_export]
69#[doc(hidden)]
70macro_rules! impl_event_group {
71 ($ident:ident::$value_offset:ident, $data:ident) => {
72 impl crate::difficulty::lightshow::group::EventGroup for $ident {
73 type Data = $data;
74
75 fn get_filter(&self) -> &Filter {
76 &self.filter
77 }
78
79 fn get_data(&self) -> &Vec<Self::Data> {
80 &self.data
81 }
82
83 #[allow(deprecated)]
84 fn get_beat_offset(&self, light_id: i32, group_size: i32) -> f32 {
85 self.beat_dist_type.compute_beat_offset(
86 light_id,
87 group_size,
88 &self.filter,
89 self.beat_dist_value,
90 self.data.last().map(|data| data.beat_offset),
91 None,
92 )
93 }
94
95 #[allow(deprecated)]
96 fn get_value_offset(&self, light_id: i32, group_size: i32) -> f32 {
97 self.$value_offset(light_id, group_size)
98 }
99
100 #[allow(deprecated)]
101 fn get_duration(&self, group_size: i32) -> f32 {
102 let filtered_size = self.filter.count_filtered(group_size);
103
104 if filtered_size == 0 {
105 return 0.0;
106 }
107
108 let Some(data) = self.get_data().last() else {
109 return 0.0;
110 };
111
112 match self.beat_dist_type {
113 DistributionType::Wave => {
114 if let Some(limit_behaviour) = self.filter.limit_behaviour
115 && !limit_behaviour.beat_enabled()
116 && let Some(limit_percent) = self.filter.limit_percent
117 && limit_percent > 0.0
118 {
119 (self.beat_dist_value * limit_percent).max(data.beat_offset)
120 } else {
121 self.beat_dist_value.max(data.beat_offset)
122 }
123 }
124 DistributionType::Step => {
125 data.beat_offset + self.beat_dist_value * filtered_size as f32
126 }
127 DistributionType::Undefined(_) => data.beat_offset,
128 }
129 }
130 }
131 };
132}
133
134/// The lowest-level group event type, which determines the base value of the event.
135pub trait EventData {
136 /// Returns the number of beats the event will be offset from the [`EventBox`]'s beat.
137 fn get_beat_offset(&self) -> f32;
138}
139
140#[macro_export]
141#[doc(hidden)]
142macro_rules! impl_event_data {
143 ($ident:ident) => {
144 impl crate::difficulty::lightshow::group::EventData for $ident {
145 fn get_beat_offset(&self) -> f32 {
146 self.beat_offset
147 }
148 }
149 };
150}
151
152#[allow(deprecated)]
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::{DistributionType, LimitBehaviour};
157
158 #[test]
159 fn get_duration_no_distribution() {
160 assert_eq!(ColorEventGroup::default().get_duration(12), 0.0);
161 }
162
163 #[test]
164 fn get_duration_wave() {
165 let group = ColorEventGroup {
166 beat_dist_type: DistributionType::Wave,
167 beat_dist_value: 12.0,
168 ..Default::default()
169 };
170
171 assert_eq!(group.get_duration(12), 12.0);
172 }
173
174 #[test]
175 fn get_duration_step() {
176 let group = ColorEventGroup {
177 beat_dist_type: DistributionType::Step,
178 beat_dist_value: 1.0,
179 ..Default::default()
180 };
181
182 assert_eq!(group.get_duration(12), 12.0);
183 }
184
185 #[test]
186 fn get_duration_wave_with_limit() {
187 let group = ColorEventGroup {
188 filter: Filter {
189 limit_behaviour: Some(LimitBehaviour::None),
190 limit_percent: Some(0.5),
191 ..Default::default()
192 },
193 beat_dist_type: DistributionType::Wave,
194 beat_dist_value: 12.0,
195 ..Default::default()
196 };
197
198 assert_eq!(group.get_duration(12), 6.0);
199 }
200
201 #[test]
202 fn get_duration_step_with_limit() {
203 let group = ColorEventGroup {
204 filter: Filter {
205 limit_behaviour: Some(LimitBehaviour::None),
206 limit_percent: Some(0.5),
207 ..Default::default()
208 },
209 beat_dist_type: DistributionType::Step,
210 beat_dist_value: 1.0,
211 ..Default::default()
212 };
213
214 assert_eq!(group.get_duration(12), 6.0);
215 }
216
217 #[test]
218 fn get_duration_wave_with_limit_adjusted() {
219 let group = ColorEventGroup {
220 filter: Filter {
221 limit_behaviour: Some(LimitBehaviour::Beat),
222 limit_percent: Some(0.5),
223 ..Default::default()
224 },
225 beat_dist_type: DistributionType::Wave,
226 beat_dist_value: 12.0,
227 ..Default::default()
228 };
229
230 assert_eq!(group.get_duration(12), 12.0);
231 }
232
233 #[test]
234 fn get_duration_step_with_limit_adjusted() {
235 let group = ColorEventGroup {
236 filter: Filter {
237 limit_behaviour: Some(LimitBehaviour::Beat),
238 limit_percent: Some(0.5),
239 ..Default::default()
240 },
241 beat_dist_type: DistributionType::Step,
242 beat_dist_value: 1.0,
243 ..Default::default()
244 };
245
246 assert_eq!(group.get_duration(12), 6.0);
247 }
248
249 #[test]
250 fn get_duration_step_with_limit_zero() {
251 let group = ColorEventGroup {
252 filter: Filter {
253 limit_percent: Some(0.0),
254 ..Default::default()
255 },
256 beat_dist_type: DistributionType::Step,
257 beat_dist_value: 1.0,
258 ..Default::default()
259 };
260
261 assert_eq!(group.get_duration(12), 12.0);
262 }
263}