Skip to main content

shuyi_playdate/
input.rs

1//! The Playdate's buttons and crank as the engine's input: the SDK's button
2//! bits are one controller of [`PadKind::Playdate`], and the crank two more
3//! of its buttons, one press a notch. It only translates, as `shuyi-app`'s
4//! `pads.rs` does; the only file in this crate that names buttons.
5//!
6//! A reports `East` and B `West`, as `targets/playdate.ron` lists them
7//! (ADR-0008 §1).
8
9use alloc::vec::Vec;
10
11use shuyi_assets::keymap::{CrankDef, StickDef};
12use shuyi_ui::RawInputEvent;
13use shuyi_ui::input::Button;
14use shuyi_ui::input::crank::{Crank, STEPS_PER_TURN};
15use shuyi_ui::input::pad::{PadKind, PadState, Pads};
16
17/// The SDK's `PDButtons` bits (`pd_api_sys.h`; `shim/shim.c` checks this
18/// copy against the header), each with the binding position it reports.
19pub const BUTTONS: [(u32, Button); 6] = [
20    (0x01, Button::DpadLeft),
21    (0x02, Button::DpadRight),
22    (0x04, Button::DpadUp),
23    (0x08, Button::DpadDown),
24    (0x20, Button::East),
25    (0x10, Button::West),
26];
27
28/// The button of the system menu's item in `slot`, if there is one.
29pub fn menu_button(slot: u8) -> Option<Button> {
30    Button::system_menu(slot)
31}
32
33/// What the player's choice of the system menu's item in `slot` is: a press
34/// and a release of its button.
35pub fn menu_press(slot: u8) -> Option<[RawInputEvent; 2]> {
36    let button = menu_button(slot)?;
37    Some([
38        RawInputEvent::PadDown(button, PadKind::Playdate),
39        RawInputEvent::PadUp(button),
40    ])
41}
42
43/// The Playdate's one pad: never unplugged, no sticks.
44const PAD: usize = 0;
45
46/// The pad state of the buttons whose bits are set in `bits`.
47fn state(bits: u32) -> PadState {
48    PadState {
49        buttons: BUTTONS
50            .iter()
51            .filter(|&&(bit, _)| bits & bit != 0)
52            .map(|&(_, button)| button)
53            .collect(),
54        ..PadState::default()
55    }
56}
57
58/// Degrees in a whole turn.
59const DEGREES_PER_TURN: f32 = 360.0;
60
61/// The float the SDK gives for the crank, turned into whole steps of a turn
62/// here at the boundary: what rounds away is kept for the next frame, so a
63/// slow turn still gets there, and no float travels further.
64#[derive(Debug, Clone, Copy, Default, PartialEq)]
65struct Steps {
66    carry: f32,
67}
68
69impl Steps {
70    /// The whole steps in `degrees` and what the last call left. A reading
71    /// that isn't a number is no turn.
72    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
73    fn of(&mut self, degrees: f32) -> i32 {
74        if !degrees.is_finite() {
75            return 0;
76        }
77        let per_degree = STEPS_PER_TURN as f32 / DEGREES_PER_TURN;
78        let exact = degrees * per_degree + self.carry;
79        // `as` saturates, and `Crank::turn_steps` clamps.
80        let steps = exact as i32;
81        self.carry = exact - steps as f32;
82        steps
83    }
84}
85
86/// The Playdate's buttons and crank, read once per frame.
87#[derive(Debug, Clone)]
88pub struct PadInput {
89    pads: Pads,
90    crank: Crank,
91    steps: Steps,
92}
93
94impl PadInput {
95    /// Nothing held yet, a crank's notch `crank` long.
96    pub fn new(crank: CrankDef) -> Self {
97        Self {
98            // The Playdate has no stick, but `Pads` wants its thresholds.
99            pads: Pads::new(StickDef::default()),
100            crank: Crank::new(crank),
101            steps: Steps::default(),
102        }
103    }
104
105    /// This frame's events: the button releases, then presses, then the
106    /// crank's notches. `current` is the bits held now and `pushed` those
107    /// pressed since the last frame: a button pressed and let go between
108    /// two frames counts as held for this one, so a quick tap is never lost
109    /// while a frame takes long. `turned` is the crank's degrees since the
110    /// last frame.
111    pub fn poll(
112        &mut self,
113        current: u32,
114        pushed: u32,
115        turned: f32,
116        docked: bool,
117    ) -> Vec<RawInputEvent> {
118        let held = state(current | pushed);
119        let mut events =
120            RawInputEvent::from_pads(&mut self.pads, &[(PAD, PadKind::Playdate, held)]);
121        if docked {
122            self.steps = Steps::default();
123        }
124        let steps = self.steps.of(turned);
125        events.extend(RawInputEvent::from_crank_steps(
126            &mut self.crank,
127            steps,
128            docked,
129            PadKind::Playdate,
130        ));
131        events
132    }
133
134    /// The crank's notch from now on: the game's keymap file says it.
135    pub fn set_crank(&mut self, crank: CrankDef) {
136        self.crank = Crank::new(crank);
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use alloc::vec;
143
144    use shuyi_assets::target::TargetProfile;
145
146    use super::*;
147
148    const LEFT: u32 = 1;
149    const UP: u32 = 4;
150    const B: u32 = 16;
151    const A: u32 = 32;
152
153    fn pad() -> PadInput {
154        PadInput::new(CrankDef::default())
155    }
156
157    fn down(button: Button) -> RawInputEvent {
158        RawInputEvent::PadDown(button, PadKind::Playdate)
159    }
160
161    fn up(button: Button) -> RawInputEvent {
162        RawInputEvent::PadUp(button)
163    }
164
165    /// The presses and releases of `button`, `times` times.
166    fn notches(button: Button, times: usize) -> Vec<RawInputEvent> {
167        (0..times)
168            .flat_map(|_| [down(button), up(button)])
169            .collect()
170    }
171
172    #[test]
173    fn a_is_east_b_is_west_and_the_d_pad_is_the_d_pad() {
174        let mut p = pad();
175        assert_eq!(p.poll(A, A, 0.0, true), [down(Button::East)]);
176        assert_eq!(
177            p.poll(B | UP | LEFT, B | UP | LEFT, 0.0, true),
178            [
179                up(Button::East),
180                down(Button::West),
181                down(Button::DpadUp),
182                down(Button::DpadLeft),
183            ]
184        );
185        assert_eq!(state(0b11_1111).buttons.iter().count(), 6);
186        assert_eq!(state(1 << 6), PadState::default(), "no other bits");
187    }
188
189    /// Whatever the buttons report, the profile lists: its check and the
190    /// device agree.
191    #[test]
192    fn every_button_is_one_the_profile_lists() {
193        let profile = TargetProfile::from_source(
194            "targets/playdate.ron",
195            "playdate",
196            include_str!("../../../targets/playdate.ron"),
197        )
198        .unwrap_or_else(|e| panic!("{e:?}"));
199        for (_, button) in BUTTONS {
200            assert!(
201                profile.input.buttons.contains(&button),
202                "{button:?} isn't in {:?}",
203                profile.input.buttons
204            );
205        }
206    }
207
208    #[test]
209    fn a_held_button_is_one_press_until_it_is_let_go() {
210        let mut p = pad();
211        assert_eq!(p.poll(A, A, 0.0, true), [down(Button::East)]);
212        assert_eq!(p.poll(A, 0, 0.0, true), []);
213        assert_eq!(p.poll(0, 0, 0.0, true), [up(Button::East)]);
214        assert_eq!(p.poll(0, 0, 0.0, true), []);
215    }
216
217    #[test]
218    fn a_tap_between_two_frames_is_not_lost() {
219        let mut p = pad();
220        assert_eq!(p.poll(0, B, 0.0, true), [down(Button::West)]);
221        assert_eq!(p.poll(0, 0, 0.0, true), [up(Button::West)]);
222    }
223
224    /// The ticket: 90° at the default 45° a notch.
225    #[test]
226    fn a_crank_turn_of_90_degrees_gives_two_notches_forward() {
227        let mut p = pad();
228        assert_eq!(p.poll(0, 0, 90.0, false), notches(Button::CrankForward, 2));
229        assert_eq!(p.poll(0, 0, -45.0, false), notches(Button::CrankBack, 1));
230    }
231
232    /// A turn too small to be a step in any one frame still adds up.
233    #[test]
234    fn a_slow_turn_gets_there_in_the_same_notches() {
235        let mut p = pad();
236        let mut events = vec![];
237        for _ in 0..450 {
238            events.extend(p.poll(0, 0, 0.2, false));
239        }
240        assert_eq!(events, notches(Button::CrankForward, 2));
241    }
242
243    #[test]
244    fn a_docked_crank_gives_nothing_and_forgets_a_part_notch() {
245        let mut p = pad();
246        assert_eq!(p.poll(0, 0, 44.0, false), []);
247        assert_eq!(p.poll(0, 0, 90.0, true), []);
248        assert_eq!(p.poll(0, 0, 44.0, false), []);
249    }
250
251    #[test]
252    fn the_keymap_sets_the_notch() {
253        let mut p = pad();
254        p.set_crank(CrankDef { notch_degrees: 90 });
255        assert_eq!(p.poll(0, 0, 90.0, false), notches(Button::CrankForward, 1));
256    }
257
258    #[test]
259    fn a_broken_reading_is_no_turn() {
260        let mut p = pad();
261        assert_eq!(p.poll(0, 0, f32::NAN, false), []);
262        assert_eq!(p.poll(0, 0, f32::INFINITY, false), []);
263        assert_eq!(p.poll(0, 0, 45.0, false), notches(Button::CrankForward, 1));
264    }
265}