From 52ac39072fb662e43976f7637f4ead9698e65a58 Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Wed, 30 Sep 2026 07:45:06 +0530 Subject: [PATCH] feat(input): implement dual-motor force feedback and trigger rumble manager --- crates/molotov-core/src/input/haptics.rs | 63 +++++++++++++++++++++++ crates/molotov-core/src/input/mod.rs | 1 + crates/molotov-core/tests/haptics_test.rs | 21 ++++++++ 3 files changed, 85 insertions(+) create mode 100644 crates/molotov-core/src/input/haptics.rs create mode 100644 crates/molotov-core/tests/haptics_test.rs diff --git a/crates/molotov-core/src/input/haptics.rs b/crates/molotov-core/src/input/haptics.rs new file mode 100644 index 0000000..d1d6028 --- /dev/null +++ b/crates/molotov-core/src/input/haptics.rs @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +use std::collections::HashMap; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct RumbleState { + pub low_freq_motor: u16, + pub high_freq_motor: u16, + pub left_trigger: u16, + pub right_trigger: u16, + pub duration_ms: u32, +} + +pub struct RumbleManager { + states: HashMap, +} + +impl Default for RumbleManager { + fn default() -> Self { + Self::new() + } +} + +impl RumbleManager { + pub fn new() -> Self { + Self { + states: HashMap::new(), + } + } + + pub fn rumble(&mut self, gamepad_id: u32, low_freq: f32, high_freq: f32, duration_ms: u32) { + let low = (low_freq.clamp(0.0, 1.0) * 65535.0) as u16; + let high = (high_freq.clamp(0.0, 1.0) * 65535.0) as u16; + + let entry = self.states.entry(gamepad_id).or_default(); + entry.low_freq_motor = low; + entry.high_freq_motor = high; + entry.duration_ms = duration_ms; + } + + pub fn rumble_triggers(&mut self, gamepad_id: u32, left: f32, right: f32, duration_ms: u32) { + let l = (left.clamp(0.0, 1.0) * 65535.0) as u16; + let r = (right.clamp(0.0, 1.0) * 65535.0) as u16; + + let entry = self.states.entry(gamepad_id).or_default(); + entry.left_trigger = l; + entry.right_trigger = r; + entry.duration_ms = duration_ms; + } + + pub fn stop(&mut self, gamepad_id: u32) { + if let Some(entry) = self.states.get_mut(&gamepad_id) { + entry.low_freq_motor = 0; + entry.high_freq_motor = 0; + entry.left_trigger = 0; + entry.right_trigger = 0; + entry.duration_ms = 0; + } + } + + pub fn get_state(&self, gamepad_id: u32) -> Option { + self.states.get(&gamepad_id).copied() + } +} diff --git a/crates/molotov-core/src/input/mod.rs b/crates/molotov-core/src/input/mod.rs index 5034c40..7349334 100644 --- a/crates/molotov-core/src/input/mod.rs +++ b/crates/molotov-core/src/input/mod.rs @@ -1,3 +1,4 @@ // SPDX-License-Identifier: GPL-3.0-or-later pub mod gamepad; +pub mod haptics; pub mod ime; diff --git a/crates/molotov-core/tests/haptics_test.rs b/crates/molotov-core/tests/haptics_test.rs new file mode 100644 index 0000000..55b4e49 --- /dev/null +++ b/crates/molotov-core/tests/haptics_test.rs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +use molotov_core::input::haptics::RumbleManager; + +#[test] +fn test_rumble_actuation_and_clamping() { + let mut manager = RumbleManager::new(); + let pad_id = 1; + + // Actuate rumble: low_freq = 0.5 (32767), high_freq = 1.0 (65535), duration = 250ms + manager.rumble(pad_id, 0.5, 1.0, 250); + let state = manager.get_state(pad_id).expect("Should have rumble state"); + assert_eq!(state.low_freq_motor, 32767); + assert_eq!(state.high_freq_motor, 65535); + assert_eq!(state.duration_ms, 250); + + // Stop rumble + manager.stop(pad_id); + let stopped = manager.get_state(pad_id).expect("Should have rumble state"); + assert_eq!(stopped.low_freq_motor, 0); + assert_eq!(stopped.high_freq_motor, 0); +}