codecraft 0.1.1

A minimalist 3D game engine built on parts of Bevy (ECS, color) with wgpu and winit: OpenPBR materials, clustered lighting, an immediate-mode UI, audio and gamepad haptics
Documentation
//! Game controllers, and a readout for seeing what one is sending.
//!
//! [`ps`] does the talking to the device. What is added here is the part a
//! game touches: the pad's state as an ECS resource, republished every frame
//! by the host app, and [`HIDDebugOverlay`] to put it on screen while you are
//! working out which stick is which.
pub mod output;
pub mod overlay;
pub mod ps;

pub use output::{Bus, Feedback, Trigger};
pub use overlay::{HIDDebugOverlay, HidDebugLine, HidPlugin, update_hid_overlay_system};

use glam::Vec2;

use crate::ecs::Resource;

/// A finger on the touchpad, in units anything downstream can use: `at` runs
/// 0 to 1 across the pad and down it, so a gesture reads the same whichever
/// pad sent it. The id is carried through from [`ps::Touch`], because telling
/// a moved finger from a new one is the whole of a drag.
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct Finger {
    pub id: u8,
    pub at: Vec2,
}

/// What the pad was sending as of this frame.
///
/// Present whether or not a controller is plugged in — `connected` says which
/// — so nothing has to branch on the resource existing.
#[derive(Resource, Clone, Copy, Default, Debug)]
pub struct GamepadState {
    pub connected: bool,
    pub model: Option<ps::Model>,
    pub state: ps::State,
}

impl GamepadState {
    /// Whether a button is held. Always false with no pad attached, so a
    /// caller can read it without checking first.
    pub fn held(&self, mask: u32) -> bool {
        self.connected && self.state.held(mask)
    }

    /// Both fingers the touchpad can track, in the slots it reports them in,
    /// as fractions of the pad.
    ///
    /// All empty with no pad, nothing touching, or a report too short to
    /// carry the touchpad at all — a caller only ever has to handle "not
    /// touching". Two of them at once is a pinch; see
    /// [`crate::OrbitCamera::touchpad`].
    pub fn fingers(&self) -> [Option<Finger>; 2] {
        let Some(model) = self.model.filter(|_| self.connected) else {
            return [None, None];
        };
        let size = model.touch_resolution();
        self.state.touch.map(|touch| {
            touch.map(|touch| Finger {
                id: touch.id,
                at: Vec2::new(touch.x as f32, touch.y as f32) / size,
            })
        })
    }

    /// The finger a one-finger drag should follow: the first slot with
    /// something in it. See [`ps::State::touch`] for why it is that one.
    pub fn finger(&self) -> Option<Finger> {
        let [first, second] = self.fingers();
        first.or(second)
    }

    /// The pad's name, or why there isn't one.
    pub fn name(&self) -> &'static str {
        match self.model {
            Some(model) if self.connected => model.name(),
            _ => "NOT CONNECTED",
        }
    }
}

/// Every controller plugged in this frame, in the order they were found.
///
/// A resource, republished each frame by the host app, so a scene can hand a
/// pad to each player without knowing anything about devices. Pads come and go
/// while the game runs -- see [`ps::Hub`] -- so this is read fresh every frame
/// rather than counted once at setup: a second player who plugs in mid-game
/// appears here, and one who unplugs stops appearing.
///
/// [`GamepadState`] is the first of these, for everything that only wants
/// "the" controller.
#[derive(Resource, Clone, Default, Debug)]
pub struct Gamepads(pub Vec<GamepadState>);

impl Gamepads {
    /// The pad a given player is holding, or a disconnected one if they have
    /// not got a controller.
    ///
    /// Always answers, so a scene can drive player two the same way it drives
    /// player one and simply get no input until a second pad arrives -- rather
    /// than branching on how many are plugged in.
    pub fn player(&self, index: usize) -> GamepadState {
        self.0.get(index).copied().unwrap_or_default()
    }

    /// How many are plugged in.
    pub fn len(&self) -> usize {
        self.0.len()
    }

    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }
}

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

    fn connected() -> GamepadState {
        GamepadState {
            connected: true,
            model: Some(ps::Model::DualSense),
            state: ps::State::default(),
        }
    }

    /// A seat with nobody in it answers like a seat with an unplugged pad, so
    /// a two-player scene drives both seats the same way and simply gets no
    /// input for the empty one.
    #[test]
    fn a_seat_with_no_pad_reads_as_disconnected() {
        let pads = Gamepads(vec![connected()]);

        assert!(pads.player(0).connected, "the one pad there is");
        assert!(!pads.player(1).connected, "and nothing in the second seat");
        assert!(!pads.player(9).connected, "or any seat past the end");
    }

    /// Which is what lets a second controller arrive mid-game: the seat was
    /// always there, it just had nothing in it.
    #[test]
    fn a_pad_plugged_in_later_fills_the_next_seat() {
        let mut pads = Gamepads(vec![connected()]);
        assert_eq!(pads.len(), 1);
        assert!(!pads.player(1).connected);

        pads.0.push(connected());
        assert!(
            pads.player(1).connected,
            "the second seat should be live without the scene doing anything",
        );
    }

    #[test]
    fn no_pads_at_all_is_not_a_special_case() {
        let pads = Gamepads::default();
        assert!(pads.is_empty());
        assert!(!pads.player(0).connected);
        assert_eq!(pads.player(0).name(), "NOT CONNECTED");
    }
}