ling-lang 2030.1.28

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)));
        }
    });
}