mirage-engine 0.1.1

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
//! Source of the gamepads. A backend only reports what a control reads;
//! what that means to an action is decided in shared code.

use crate::input::state::Reading;

#[cfg(not(target_arch = "wasm32"))]
use library as sys;
#[cfg(target_arch = "wasm32")]
use page as sys;

/// The pads a run reads, or none where there is nothing to read them with.
pub(crate) struct Pads(Option<sys::Pads>);

impl Pads {
    /// The pads the platform reports, which may be none of them yet: a pad
    /// is read wherever it is plugged in, not only at startup.
    pub(crate) fn open() -> Self {
        match sys::Pads::open() {
            Some(pads) => Self(Some(pads)),
            None => Self::silent(),
        }
    }

    /// No pads at all, for a session with no player.
    pub(crate) fn silent() -> Self {
        Self(None)
    }

    /// Reads every pad the platform reports into `reading`.
    pub(crate) fn poll(&mut self, reading: &mut Reading) {
        if let Some(pads) = &mut self.0 {
            pads.poll(reading);
        }
    }
}

#[cfg(not(target_arch = "wasm32"))]
mod library {
    use gilrs::ev::state::ButtonData;
    use gilrs::{Axis, Button, Gamepad, Gilrs};

    use crate::input::binding::{JoystickControl, Pad, PadAxis};
    use crate::input::state::Reading;

    /// The desktop's gamepad layer. Buffers each pad's state, kept current
    /// as its events arrive.
    pub(crate) struct Pads(Gilrs);

    impl Pads {
        pub(crate) fn open() -> Option<Self> {
            match Gilrs::new() {
                Ok(pads) => Some(Self(pads)),
                Err(error) => {
                    log::debug!("mirage-engine reads no gamepads on this machine: {error}");
                    None
                }
            }
        }

        pub(crate) fn poll(&mut self, reading: &mut Reading) {
            while self.0.next_event().is_some() {}

            for (_, pad) in self.0.gamepads() {
                for &button in Pad::ALL {
                    if pad.is_pressed(mapped(button)) {
                        reading.press_pad(button);
                    }
                }
                for &axis in PadAxis::ALL {
                    reading.push_pad(axis, lane(&pad, axis));
                }

                let state = pad.state();
                for (control, data) in state.buttons() {
                    if data.is_pressed() {
                        reading.press_joystick(JoystickControl::new(control.into_u32()));
                    }
                }
                for (control, data) in state.axes() {
                    reading.push_joystick(JoystickControl::new(control.into_u32()), data.value());
                }
            }
        }
    }

    /// The standard layout's button, as this backend spells it.
    fn mapped(button: Pad) -> Button {
        match button {
            Pad::South => Button::South,
            Pad::East => Button::East,
            Pad::West => Button::West,
            Pad::North => Button::North,
            Pad::LeftBumper => Button::LeftTrigger,
            Pad::RightBumper => Button::RightTrigger,
            Pad::LeftTrigger => Button::LeftTrigger2,
            Pad::RightTrigger => Button::RightTrigger2,
            Pad::Select => Button::Select,
            Pad::Start => Button::Start,
            Pad::Guide => Button::Mode,
            Pad::LeftStick => Button::LeftThumb,
            Pad::RightStick => Button::RightThumb,
            Pad::DPadUp => Button::DPadUp,
            Pad::DPadDown => Button::DPadDown,
            Pad::DPadLeft => Button::DPadLeft,
            Pad::DPadRight => Button::DPadRight,
        }
    }

    /// Distance one lane of the standard layout is pushed; a trigger reads
    /// off the button it also is.
    fn lane(pad: &Gamepad<'_>, axis: PadAxis) -> f32 {
        let trigger = |button| pad.button_data(button).map_or(0.0, ButtonData::value);
        match axis {
            PadAxis::LeftX => pad.value(Axis::LeftStickX),
            PadAxis::LeftY => pad.value(Axis::LeftStickY),
            PadAxis::RightX => pad.value(Axis::RightStickX),
            PadAxis::RightY => pad.value(Axis::RightStickY),
            PadAxis::LeftTrigger => trigger(Button::LeftTrigger2),
            PadAxis::RightTrigger => trigger(Button::RightTrigger2),
        }
    }
}

#[cfg(target_arch = "wasm32")]
mod page {
    use wasm_bindgen::JsCast;
    use web_sys::{Gamepad, GamepadButton, GamepadMappingType};

    use crate::input::binding::{JoystickControl, Pad, PadAxis};
    use crate::input::state::Reading;

    /// Position of the standard layout's buttons in the browser's own list.
    const MAPPED: [Pad; 17] = [
        Pad::South,
        Pad::East,
        Pad::West,
        Pad::North,
        Pad::LeftBumper,
        Pad::RightBumper,
        Pad::LeftTrigger,
        Pad::RightTrigger,
        Pad::Select,
        Pad::Start,
        Pad::LeftStick,
        Pad::RightStick,
        Pad::DPadUp,
        Pad::DPadDown,
        Pad::DPadLeft,
        Pad::DPadRight,
        Pad::Guide,
    ];

    /// Position of the standard layout's lanes, and the direction the
    /// browser counts each by, against the engine's positive up.
    const LANES: [(PadAxis, f32); 4] = [
        (PadAxis::LeftX, 1.0),
        (PadAxis::LeftY, -1.0),
        (PadAxis::RightX, 1.0),
        (PadAxis::RightY, -1.0),
    ];

    /// Position of the standard layout's triggers, and how far they report
    /// being pulled.
    const TRIGGERS: [(usize, PadAxis); 2] = [(6, PadAxis::LeftTrigger), (7, PadAxis::RightTrigger)];

    /// The browser's gamepad list, which is read afresh every frame.
    pub(crate) struct Pads;

    impl Pads {
        pub(crate) fn open() -> Option<Self> {
            web_sys::window().map(|_| Self)
        }

        pub(crate) fn poll(&mut self, reading: &mut Reading) {
            let Some(window) = web_sys::window() else {
                return;
            };
            let Ok(pads) = window.navigator().get_gamepads() else {
                return;
            };

            for value in pads.iter() {
                let Ok(pad) = value.dyn_into::<Gamepad>() else {
                    continue;
                };
                if !pad.connected() {
                    continue;
                }
                read(&pad, reading);
            }
        }
    }

    fn read(pad: &Gamepad, reading: &mut Reading) {
        let buttons: Vec<(bool, f32)> = pad
            .buttons()
            .iter()
            .map(|value| match value.dyn_into::<GamepadButton>() {
                Ok(button) => (button.pressed(), button.value() as f32),
                Err(_) => (false, 0.0),
            })
            .collect();
        let axes: Vec<f32> = pad
            .axes()
            .iter()
            .map(|value| value.as_f64().unwrap_or_default() as f32)
            .collect();

        for (control, &(pressed, _)) in buttons.iter().enumerate() {
            if pressed {
                reading.press_joystick(JoystickControl::new(control as u32));
            }
        }
        for (control, &value) in axes.iter().enumerate() {
            reading.push_joystick(JoystickControl::new(control as u32), value);
        }

        if pad.mapping() != GamepadMappingType::Standard {
            return;
        }
        for (&button, &(pressed, _)) in MAPPED.iter().zip(buttons.iter()) {
            if pressed {
                reading.press_pad(button);
            }
        }
        for (&(axis, sign), &value) in LANES.iter().zip(axes.iter()) {
            reading.push_pad(axis, value * sign);
        }
        for (at, axis) in TRIGGERS {
            let pulled = buttons.get(at).map_or(0.0, |&(_, value)| value);
            reading.push_pad(axis, pulled);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn opening_the_platforms_pads_reads_whatever_is_plugged_in_and_no_more() {
        let mut reading = Reading::default();

        Pads::open().poll(&mut reading);
        Pads::silent().poll(&mut reading);
    }
}