ling-lang 2030.1.33

Ling - The Omniglot Systems Language
Documentation
// src/runtime/gamepad.rs — native gamepad input + rumble via gilrs (XInput /
// DInput / evdev). Single-threaded: the game loop pumps `poll()` each frame,
// then queries `button()` / `axis()`. `rumble()` builds a force-feedback effect
// whose lifetime we own until it expires.
//
//   gamepad_poll()                 — pump events (call once per frame)
//   gamepad_button(name) → 0/1     — a b x y l1 r1 l2 r2 l3 r3 start select
//                                     dup ddown dleft dright
//   gamepad_axis(name)   → -1..1   — lx ly rx ry l2 r2
//   gamepad_rumble(low, high, ms)  — low/high motor 0..1 for ms milliseconds

use std::cell::RefCell;
use std::time::{Duration, Instant};

use gilrs::ff::{BaseEffect, BaseEffectType, Effect, EffectBuilder, Replay, Ticks};
use gilrs::{Axis as GAxis, Button as GButton, GamepadId, Gilrs};

struct Pad {
    gilrs: Gilrs,
    active: Option<GamepadId>,
    effects: Vec<(Effect, Instant)>, // keep effects alive until they expire
    start_edge: bool,                // Start rising-edge this frame (Start acts as Enter)
    start_prev: bool,
}

thread_local! {
    static PAD: RefCell<Option<Pad>> = const { RefCell::new(None) };
}

fn ensure(p: &mut Option<Pad>) {
    if p.is_none() {
        if let Ok(gilrs) = Gilrs::new() {
            let active = gilrs.gamepads().next().map(|(id, _)| id);
            *p = Some(Pad {
                gilrs,
                active,
                effects: Vec::new(),
                start_edge: false,
                start_prev: false,
            });
        }
    }
}

pub fn poll() {
    PAD.with(|cell| {
        let mut g = cell.borrow_mut();
        ensure(&mut g);
        let Some(pad) = g.as_mut() else { return };
        while let Some(ev) = pad.gilrs.next_event() {
            pad.gilrs.update(&ev);
            // latch onto the most recently active gamepad
            pad.active = Some(ev.id);
        }
        if pad.active.is_none() {
            pad.active = pad.gilrs.gamepads().next().map(|(id, _)| id);
        }
        // Start rising-edge (Start behaves like Enter)
        let cur_start = pad
            .active
            .map(|id| pad.gilrs.gamepad(id).is_pressed(GButton::Start))
            .unwrap_or(false);
        pad.start_edge = cur_start && !pad.start_prev;
        pad.start_prev = cur_start;
        // drop finished rumble effects
        let now = Instant::now();
        pad.effects.retain(|(_, exp)| *exp > now);
    });
}

/// True for the frame Start was just pressed (the host ORs this into `key_pressed("enter")`).
pub fn start_edge() -> bool {
    PAD.with(|cell| {
        cell.borrow()
            .as_ref()
            .map(|p| p.start_edge)
            .unwrap_or(false)
    })
}

/// Human-readable list of every gamepad gilrs sees (name, connection, rumble,
/// mapping source). For diagnosing controllers that enumerate oddly.
pub fn list() -> String {
    let mut out = String::new();
    PAD.with(|cell| {
        let mut g = cell.borrow_mut();
        ensure(&mut g);
        match g.as_ref() {
            Some(pad) => {
                let mut n = 0;
                for (_, gp) in pad.gilrs.gamepads() {
                    n += 1;
                    out.push_str(&format!(
                        "{n}: \"{}\" connected={} rumble={} map={:?}\n",
                        gp.name(),
                        gp.is_connected(),
                        gp.is_ff_supported(),
                        gp.mapping_source()
                    ));
                }
                if n == 0 {
                    out.push_str(
                        "(no gamepads seen by gilrs — try the controller's PC/X-input mode)\n",
                    );
                }
            },
            None => out.push_str("(gilrs failed to initialise)\n"),
        }
    });
    out
}

/// True if ANY button on the active pad is down — works even when the mapping
/// is missing (falls back to raw button codes), so it confirms the pad is read.
pub fn any_button() -> bool {
    PAD.with(|cell| {
        let g = cell.borrow();
        let Some(pad) = g.as_ref() else { return false };
        let Some(id) = pad.active else { return false };
        let gp = pad.gilrs.gamepad(id);
        // mapped buttons
        for b in [
            GButton::South,
            GButton::East,
            GButton::West,
            GButton::North,
            GButton::LeftTrigger,
            GButton::RightTrigger,
            GButton::LeftTrigger2,
            GButton::RightTrigger2,
            GButton::Start,
            GButton::Select,
            GButton::DPadUp,
            GButton::DPadDown,
            GButton::DPadLeft,
            GButton::DPadRight,
        ] {
            if gp.is_pressed(b) {
                return true;
            }
        }
        false
    })
}

fn btn(name: &str) -> Option<GButton> {
    Some(match name {
        "a" => GButton::South,
        "b" => GButton::East,
        "x" => GButton::West,
        "y" => GButton::North,
        "l1" => GButton::LeftTrigger,
        "r1" => GButton::RightTrigger,
        "l2" => GButton::LeftTrigger2,
        "r2" => GButton::RightTrigger2,
        "l3" => GButton::LeftThumb,
        "r3" => GButton::RightThumb,
        "start" => GButton::Start,
        "select" => GButton::Select,
        "dup" => GButton::DPadUp,
        "ddown" => GButton::DPadDown,
        "dleft" => GButton::DPadLeft,
        "dright" => GButton::DPadRight,
        _ => return None,
    })
}

pub fn button(name: &str) -> bool {
    let Some(b) = btn(name) else { return false };
    PAD.with(|cell| {
        let g = cell.borrow();
        let Some(pad) = g.as_ref() else { return false };
        let Some(id) = pad.active else { return false };
        pad.gilrs.gamepad(id).is_pressed(b)
    })
}

pub fn axis(name: &str) -> f32 {
    let a = match name {
        "lx" => GAxis::LeftStickX,
        "ly" => GAxis::LeftStickY,
        "rx" => GAxis::RightStickX,
        "ry" => GAxis::RightStickY,
        "l2" => GAxis::LeftZ,
        "r2" => GAxis::RightZ,
        _ => return 0.0,
    };
    PAD.with(|cell| {
        let g = cell.borrow();
        let Some(pad) = g.as_ref() else { return 0.0 };
        let Some(id) = pad.active else { return 0.0 };
        pad.gilrs.gamepad(id).value(a)
    })
}

pub fn rumble(low: f32, high: f32, ms: u32) {
    let lo = (low.clamp(0.0, 1.0) * 65535.0) as u16;
    let hi = (high.clamp(0.0, 1.0) * 65535.0) as u16;
    PAD.with(|cell| {
        let mut g = cell.borrow_mut();
        ensure(&mut g);
        let Some(pad) = g.as_mut() else { return };
        let Some(id) = pad.active else { return };
        if !pad.gilrs.gamepad(id).is_ff_supported() {
            return;
        }
        let play = Replay { play_for: Ticks::from_ms(ms), ..Default::default() };
        let built = EffectBuilder::new()
            .add_effect(BaseEffect {
                kind: BaseEffectType::Strong { magnitude: hi },
                scheduling: play,
                ..Default::default()
            })
            .add_effect(BaseEffect {
                kind: BaseEffectType::Weak { magnitude: lo },
                scheduling: play,
                ..Default::default()
            })
            .gamepads(&[id])
            .finish(&mut pad.gilrs);
        if let Ok(effect) = built {
            let _ = effect.play();
            pad.effects.push((
                effect,
                Instant::now() + Duration::from_millis(ms as u64 + 50),
            ));
        }
    });
}