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}