1use crate::ease::Ease;
2
3pub const CLOCK_VERSION: u32 = 1;
4pub const CLOCK_BYTES: usize = 56;
5pub const SPEED_MIN: f32 = 0.05;
6pub const SPEED_MAX: f32 = 4.0;
7const RECOVER_SLICES: u32 = 16;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum ClockError {
11 Length { found: usize },
12 Version { found: u32 },
13 Invalid(&'static str),
14}
15
16impl std::fmt::Display for ClockError {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 match self {
19 Self::Length { found } => {
20 write!(f, "saved clock is {found} bytes, expected {CLOCK_BYTES}")
21 }
22 Self::Version { found } => write!(
23 f,
24 "saved clock is version {found}, this build reads {CLOCK_VERSION}"
25 ),
26 Self::Invalid(what) => write!(f, "saved clock has {what}"),
27 }
28 }
29}
30
31impl std::error::Error for ClockError {}
32
33#[derive(Clone, Copy, Debug, PartialEq)]
34pub struct HitStop {
35 pub floor: f32,
36 pub hold: f32,
37 pub recover: f32,
38 pub ease: Ease,
39}
40
41impl HitStop {
42 pub fn new(floor: f32, hold: f32, recover: f32) -> Self {
43 Self {
44 floor,
45 hold,
46 recover,
47 ease: Ease::QuadOut,
48 }
49 }
50
51 pub fn with_ease(mut self, ease: Ease) -> Self {
52 self.ease = ease;
53 self
54 }
55}
56
57#[derive(Clone, Copy, Debug, PartialEq)]
58pub struct Dip {
59 pub floor: f32,
60 pub hold: f32,
61 pub recover: f32,
62 pub ease: Ease,
63 pub elapsed: f64,
64}
65
66impl Dip {
67 pub fn length(&self) -> f64 {
68 f64::from(self.hold) + f64::from(self.recover)
69 }
70
71 pub fn scale_at(&self, elapsed: f64) -> f64 {
72 let hold = f64::from(self.hold);
73 let floor = f64::from(self.floor);
74 if elapsed < hold {
75 return floor;
76 }
77 let recover = f64::from(self.recover);
78 if recover <= 0.0 || elapsed >= hold + recover {
79 return 1.0;
80 }
81 let u = ((elapsed - hold) / recover) as f32;
82 floor + (1.0 - floor) * f64::from(self.ease.at(u))
83 }
84
85 fn integral(&self, from: f64, to: f64) -> f64 {
86 let hold = f64::from(self.hold);
87 let end = self.length();
88 let mut total = 0.0;
89 let held = (to.min(hold) - from).max(0.0);
90 total += held * f64::from(self.floor);
91 let a = from.max(hold);
92 let b = to.min(end);
93 if b > a {
94 let slices = f64::from(RECOVER_SLICES);
95 let width = (b - a) / slices;
96 let mut sum = self.scale_at(a) + self.scale_at(b);
97 for index in 1..RECOVER_SLICES {
98 let weight = if index % 2 == 1 { 4.0 } else { 2.0 };
99 sum += weight * self.scale_at(a + width * f64::from(index));
100 }
101 total += sum * width / 3.0;
102 }
103 total += (to - from.max(end)).max(0.0);
104 total
105 }
106}
107
108#[derive(Clone, Copy, Debug, Default, PartialEq)]
109pub struct Tick {
110 pub ticks: u32,
111 pub dt: f32,
112 pub paced: f32,
113 pub real: f32,
114 pub scale: f32,
115 pub alpha: f32,
116}
117
118#[derive(Clone, Copy, Debug, PartialEq)]
119pub struct Clock {
120 rate: f64,
121 speed: f32,
122 paused: bool,
123 dip: Option<Dip>,
124 time: f64,
125 ticks: u64,
126}
127
128impl Clock {
129 pub fn new(tick_rate: f64) -> Result<Self, ClockError> {
130 if !(tick_rate.is_finite() && tick_rate > 0.0) {
131 return Err(ClockError::Invalid(
132 "a tick rate that is not finite and positive",
133 ));
134 }
135 Ok(Self {
136 rate: tick_rate,
137 speed: 1.0,
138 paused: false,
139 dip: None,
140 time: 0.0,
141 ticks: 0,
142 })
143 }
144
145 pub fn tick_rate(&self) -> f64 {
146 self.rate
147 }
148
149 pub fn tick_dt(&self) -> f64 {
150 1.0 / self.rate
151 }
152
153 pub fn speed(&self) -> f32 {
154 self.speed
155 }
156
157 pub fn set_speed(&mut self, speed: f32) {
158 if speed.is_finite() {
159 self.speed = speed.clamp(SPEED_MIN, SPEED_MAX);
160 }
161 }
162
163 pub fn paused(&self) -> bool {
164 self.paused
165 }
166
167 pub fn pause(&mut self) {
168 self.paused = true;
169 }
170
171 pub fn resume(&mut self) {
172 self.paused = false;
173 }
174
175 pub fn hit_stop(&mut self, stop: HitStop) {
176 let floor = if stop.floor.is_finite() {
177 stop.floor.clamp(0.0, 1.0)
178 } else {
179 1.0
180 };
181 let hold = finite_time(stop.hold);
182 let recover = finite_time(stop.recover);
183 if floor >= 1.0 || hold + recover <= 0.0 {
184 return;
185 }
186 self.dip = Some(Dip {
187 floor,
188 hold,
189 recover,
190 ease: stop.ease,
191 elapsed: 0.0,
192 });
193 }
194
195 pub fn dip(&self) -> Option<Dip> {
196 self.dip
197 }
198
199 pub fn scale(&self) -> f32 {
200 if self.paused {
201 return 0.0;
202 }
203 let dip = self.dip.map_or(1.0, |dip| dip.scale_at(dip.elapsed));
204 (f64::from(self.speed) * dip) as f32
205 }
206
207 pub fn time(&self) -> f64 {
208 self.time
209 }
210
211 pub fn ticks(&self) -> u64 {
212 self.ticks
213 }
214
215 pub fn alpha(&self) -> f32 {
216 let due = self.time * self.rate - self.ticks as f64;
217 due.clamp(0.0, 1.0) as f32
218 }
219
220 pub fn advance(&mut self, real: f32) -> Tick {
221 let real = if real.is_finite() { real.max(0.0) } else { 0.0 };
222 if self.paused || real <= 0.0 {
223 return Tick {
224 ticks: 0,
225 dt: 0.0,
226 paced: 0.0,
227 real,
228 scale: self.scale(),
229 alpha: self.alpha(),
230 };
231 }
232 let seconds = f64::from(real);
233 let speed = f64::from(self.speed);
234 let scaled = match self.dip.as_mut() {
235 None => seconds,
236 Some(dip) => {
237 let from = dip.elapsed;
238 dip.elapsed += seconds;
239 dip.integral(from, dip.elapsed)
240 }
241 };
242 if self.dip.is_some_and(|dip| dip.elapsed >= dip.length()) {
243 self.dip = None;
244 }
245 let before = self.time;
246 self.time += speed * scaled;
247 let due = (self.time * self.rate).floor().max(0.0) as u64;
248 let ticks = due.saturating_sub(self.ticks);
249 self.ticks = self.ticks.max(due);
250 Tick {
251 ticks: u32::try_from(ticks).unwrap_or(u32::MAX),
252 dt: (self.time - before) as f32,
253 paced: (speed * seconds) as f32,
254 real,
255 scale: self.scale(),
256 alpha: self.alpha(),
257 }
258 }
259
260 pub fn sound_rate(&self, tie: f32) -> f32 {
261 let tie = if tie.is_finite() {
262 tie.clamp(0.0, 1.0)
263 } else {
264 0.0
265 };
266 (1.0 + tie * (self.scale() - 1.0)).clamp(1.0 / 16.0, 8.0)
267 }
268
269 pub fn save(&self) -> [u8; CLOCK_BYTES] {
270 let mut out = [0u8; CLOCK_BYTES];
271 let dip = self.dip.unwrap_or(Dip {
272 floor: 1.0,
273 hold: 0.0,
274 recover: 0.0,
275 ease: Ease::Linear,
276 elapsed: 0.0,
277 });
278 out[0..4].copy_from_slice(&CLOCK_VERSION.to_le_bytes());
279 out[4..12].copy_from_slice(&self.rate.to_le_bytes());
280 out[12..16].copy_from_slice(&self.speed.to_le_bytes());
281 out[16] = u8::from(self.paused);
282 out[17] = u8::from(self.dip.is_some());
283 out[18] = dip.ease as u8;
284 out[20..28].copy_from_slice(&self.time.to_le_bytes());
285 out[28..36].copy_from_slice(&self.ticks.to_le_bytes());
286 out[36..40].copy_from_slice(&dip.floor.to_le_bytes());
287 out[40..44].copy_from_slice(&dip.hold.to_le_bytes());
288 out[44..48].copy_from_slice(&dip.recover.to_le_bytes());
289 out[48..56].copy_from_slice(&dip.elapsed.to_le_bytes());
290 out
291 }
292
293 pub fn restore(bytes: &[u8]) -> Result<Self, ClockError> {
294 if bytes.len() != CLOCK_BYTES {
295 return Err(ClockError::Length { found: bytes.len() });
296 }
297 let u32_at = |at: usize| u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap());
298 let f32_at = |at: usize| f32::from_le_bytes(bytes[at..at + 4].try_into().unwrap());
299 let f64_at = |at: usize| f64::from_le_bytes(bytes[at..at + 8].try_into().unwrap());
300 let version = u32_at(0);
301 if version != CLOCK_VERSION {
302 return Err(ClockError::Version { found: version });
303 }
304 let rate = f64_at(4);
305 if !(rate.is_finite() && rate > 0.0) {
306 return Err(ClockError::Invalid(
307 "a tick rate that is not finite and positive",
308 ));
309 }
310 let speed = f32_at(12);
311 if !(SPEED_MIN..=SPEED_MAX).contains(&speed) {
312 return Err(ClockError::Invalid("a speed outside 0.05..=4"));
313 }
314 let flag = |at: usize, what| match bytes[at] {
315 0 => Ok(false),
316 1 => Ok(true),
317 _ => Err(ClockError::Invalid(what)),
318 };
319 let paused = flag(16, "a pause flag that is neither 0 nor 1")?;
320 let dipping = flag(17, "a dip flag that is neither 0 nor 1")?;
321 let ease = *Ease::ALL
322 .get(usize::from(bytes[18]))
323 .ok_or(ClockError::Invalid("an unknown easing"))?;
324 if bytes[19] != 0 {
325 return Err(ClockError::Invalid("a nonzero padding byte"));
326 }
327 let time = f64_at(20);
328 if !(time.is_finite() && time >= 0.0) {
329 return Err(ClockError::Invalid(
330 "a game time that is not finite and nonnegative",
331 ));
332 }
333 let ticks = u64::from_le_bytes(bytes[28..36].try_into().unwrap());
334 let dip = Dip {
335 floor: f32_at(36),
336 hold: f32_at(40),
337 recover: f32_at(44),
338 ease,
339 elapsed: f64_at(48),
340 };
341 let dip = if dipping {
342 let sane = (0.0..1.0).contains(&dip.floor)
343 && dip.hold.is_finite()
344 && dip.hold >= 0.0
345 && dip.recover.is_finite()
346 && dip.recover >= 0.0
347 && dip.hold + dip.recover > 0.0
348 && dip.elapsed.is_finite()
349 && dip.elapsed >= 0.0
350 && dip.elapsed < dip.length();
351 if !sane {
352 return Err(ClockError::Invalid("a dip out of range"));
353 }
354 Some(dip)
355 } else {
356 None
357 };
358 Ok(Self {
359 rate,
360 speed,
361 paused,
362 dip,
363 time,
364 ticks,
365 })
366 }
367}
368
369fn finite_time(seconds: f32) -> f32 {
370 if seconds.is_finite() {
371 seconds.max(0.0)
372 } else {
373 0.0
374 }
375}
376
377#[derive(Clone, Copy, Debug, PartialEq)]
378pub struct Steps {
379 rate: f64,
380 done: u64,
381}
382
383impl Steps {
384 pub fn new(rate: f64, clock: &Clock) -> Self {
385 let rate = if rate.is_finite() && rate > 0.0 {
386 rate
387 } else {
388 60.0
389 };
390 Self {
391 rate,
392 done: (clock.time() * rate).floor().max(0.0) as u64,
393 }
394 }
395
396 pub fn rate(&self) -> f64 {
397 self.rate
398 }
399
400 pub fn done(&self) -> u64 {
401 self.done
402 }
403
404 pub fn due(&mut self, clock: &Clock) -> u32 {
405 let due = (clock.time() * self.rate).floor().max(0.0) as u64;
406 let n = due.saturating_sub(self.done);
407 self.done = self.done.max(due);
408 u32::try_from(n).unwrap_or(u32::MAX)
409 }
410}
411
412#[cfg(test)]
413mod tests;