Skip to main content

pfx_core/
clock.rs

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;