Skip to main content
← PSoXide SDK guides and examples · SDK f2fec1c301f1 · All Cargo features enabled; check feature gates before use.

psx_pad/
tracker.rs

1// SPDX-License-Identifier: GPL-2.0-or-later
2//! Frame-to-frame button tracking: edges, auto-repeat, handoff suppression.
3//!
4//! Every project grew this trio by hand: gameplay wants just-pressed edges
5//! (`(new ^ prev) & new`), menu cursors want delay-then-rate auto-repeat,
6//! and anything that switches contexts (a collection launcher starting a
7//! game, a menu opening over gameplay) wants buttons already held at the
8//! handoff ignored until re-pressed. One tracker, fed the polled button
9//! mask once per frame, answers all three.
10//!
11//! ```ignore
12//! let mut pad = PadTracker::new();
13//! loop {
14//!     pad.update(poll_port1().buttons);
15//!     if pad.just_pressed(button::CROSS) { jump(); }
16//!     if pad.repeats(button::DOWN, 15, 4) { cursor_down(); }
17//! }
18//! ```
19
20/// Frame-to-frame tracker over an active-high 16-bit button mask
21/// (the [`PadState::buttons`](crate::PadState) representation).
22#[derive(Copy, Clone)]
23pub struct PadTracker {
24    held: u16,
25    prev: u16,
26    /// Buttons held across a context handoff; masked out of every query
27    /// until released once. See [`PadTracker::prime`].
28    suppressed: u16,
29    /// Frames each button has been continuously held (saturating).
30    hold_frames: [u8; 16],
31}
32
33impl PadTracker {
34    /// A tracker with nothing held and nothing suppressed.
35    pub const fn new() -> Self {
36        Self {
37            held: 0,
38            prev: 0,
39            suppressed: 0,
40            hold_frames: [0; 16],
41        }
42    }
43
44    /// Feed the current frame's polled buttons. Call exactly once per
45    /// polled frame, before any queries.
46    pub fn update(&mut self, buttons: u16) {
47        self.prev = self.held;
48        self.held = buttons;
49        // A suppressed button stays suppressed only while it is still held;
50        // releasing it re-arms it as a normal button.
51        self.suppressed &= buttons;
52        for (i, frames) in self.hold_frames.iter_mut().enumerate() {
53            if buttons & (1 << i) != 0 {
54                *frames = frames.saturating_add(1);
55            } else {
56                *frames = 0;
57            }
58        }
59    }
60
61    /// Any button in `mask` is currently held (and not suppressed).
62    pub fn is_held(&self, mask: u16) -> bool {
63        self.held & !self.suppressed & mask != 0
64    }
65
66    /// Any button in `mask` went from released to held this frame.
67    pub fn just_pressed(&self, mask: u16) -> bool {
68        self.held & !self.prev & !self.suppressed & mask != 0
69    }
70
71    /// Any button in `mask` went from held to released this frame.
72    pub fn just_released(&self, mask: u16) -> bool {
73        self.prev & !self.held & mask != 0
74    }
75
76    /// Delay-then-rate auto-repeat for menu cursors: true on the initial
77    /// press, again `delay` frames later, then every `rate` frames while
78    /// held (PICO-8's cadence is `delay = 15, rate = 4`). A `rate` of 0 is
79    /// treated as 1 (every frame).
80    pub fn repeats(&self, mask: u16, delay: u8, rate: u8) -> bool {
81        let rate = rate.max(1) as u32;
82        let delay = delay as u32;
83        let live = self.held & !self.suppressed & mask;
84        for i in 0..16 {
85            if live & (1 << i) == 0 {
86                continue;
87            }
88            let held_for = self.hold_frames[i] as u32;
89            if held_for == 1 {
90                return true; // fresh press
91            }
92            if held_for > delay && (held_for - delay - 1).is_multiple_of(rate) {
93                return true;
94            }
95        }
96        false
97    }
98
99    /// Suppress every button held right now until it is released and pressed
100    /// again. Call at a context handoff (launcher starts a game, pause menu
101    /// closes) so a button held across the transition does not fire in the
102    /// new context.
103    pub fn prime(&mut self) {
104        self.suppressed = self.held;
105    }
106}
107
108impl Default for PadTracker {
109    fn default() -> Self {
110        Self::new()
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    const A: u16 = 1 << 0;
119    const B: u16 = 1 << 1;
120
121    #[test]
122    fn edges() {
123        let mut t = PadTracker::new();
124        t.update(A);
125        assert!(t.just_pressed(A));
126        assert!(t.is_held(A));
127        assert!(!t.just_released(A));
128        t.update(A);
129        assert!(!t.just_pressed(A)); // still held, no new edge
130        assert!(t.is_held(A));
131        t.update(0);
132        assert!(t.just_released(A));
133        assert!(!t.is_held(A));
134    }
135
136    #[test]
137    fn repeat_cadence() {
138        // delay 3, rate 2: fires on the press (frame 1), first repeat 3
139        // frames later (frame 4), then every 2 frames (6, 8, 10, ...).
140        let mut t = PadTracker::new();
141        let mut fired = [false; 10];
142        for slot in fired.iter_mut() {
143            t.update(A);
144            *slot = t.repeats(A, 3, 2);
145        }
146        assert_eq!(
147            fired,
148            [true, false, false, true, false, true, false, true, false, true]
149        );
150    }
151
152    #[test]
153    fn repeat_rate_zero_means_every_frame_after_delay() {
154        let mut t = PadTracker::new();
155        t.update(A); // frame 1: fresh press
156        assert!(t.repeats(A, 1, 0));
157        t.update(A); // frame 2: past delay, rate 0 -> every frame
158        assert!(t.repeats(A, 1, 0));
159        t.update(A);
160        assert!(t.repeats(A, 1, 0));
161    }
162
163    #[test]
164    fn prime_suppresses_until_released() {
165        let mut t = PadTracker::new();
166        t.update(A | B);
167        t.prime();
168        t.update(A | B);
169        assert!(!t.is_held(A) && !t.just_pressed(A));
170        assert!(!t.repeats(A, 2, 2));
171        // Release A, keep B held: A re-arms, B stays suppressed.
172        t.update(B);
173        t.update(A | B);
174        assert!(t.just_pressed(A));
175        assert!(t.is_held(A));
176        assert!(!t.is_held(B));
177    }
178
179    #[test]
180    fn independent_buttons() {
181        let mut t = PadTracker::new();
182        t.update(A);
183        t.update(A | B);
184        assert!(!t.just_pressed(A));
185        assert!(t.just_pressed(B));
186        assert!(t.is_held(A | B));
187    }
188}