ferravex_engine/
input.rs

1//! Platform-independent action mapping. Edges survive until a simulation tick consumes them.
2//! At most 256 actions, 64 bindings per action, and 1024 active physical values.
3use std::collections::BTreeMap;
4const MAX_ACTIONS: usize = 256;
5const MAX_BINDINGS: usize = 64;
6const MAX_ACTIVE_CODES: usize = 1024;
7#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
8pub enum InputCode {
9    Key(u16),
10    Mouse(u8),
11    PadButton(u8),
12    PadAxis(u8),
13    Touch(u8),
14}
15#[derive(Clone, Copy, Debug)]
16pub struct Binding {
17    pub code: InputCode,
18    pub scale: f32,
19    pub deadzone: f32,
20}
21#[derive(Clone, Copy, Default, Debug, PartialEq)]
22pub struct Action {
23    pub value: f32,
24    pub pressed: bool,
25    pub released: bool,
26}
27struct Entry {
28    bindings: Box<[Binding]>,
29    state: Action,
30}
31#[derive(Default)]
32pub struct ActionMap {
33    entries: Vec<Entry>,
34    values: BTreeMap<InputCode, f32>,
35}
36impl ActionMap {
37    /// Registers trusted configuration. Use try_action for externally supplied mappings.
38    pub fn action(&mut self, bindings: Vec<Binding>) -> usize {
39        self.try_action(bindings)
40            .expect("action or binding capacity exceeded")
41    }
42    pub fn try_action(&mut self, bindings: Vec<Binding>) -> Option<usize> {
43        if self.entries.len() >= MAX_ACTIONS || bindings.len() > MAX_BINDINGS {
44            return None;
45        }
46        let id = self.entries.len();
47        self.entries.push(Entry {
48            bindings: bindings.into_boxed_slice(),
49            state: Action::default(),
50        });
51        self.recompute();
52        Some(id)
53    }
54    /// Invalid ids or oversized replacements leave the old mapping untouched.
55    pub fn rebind(&mut self, id: usize, bindings: Vec<Binding>) -> bool {
56        if bindings.len() > MAX_BINDINGS {
57            return false;
58        }
59        let Some(entry) = self.entries.get_mut(id) else {
60            return false;
61        };
62        entry.bindings = bindings.into_boxed_slice();
63        self.recompute();
64        true
65    }
66    pub fn feed(&mut self, code: InputCode, value: f32) {
67        let value = if value.is_finite() {
68            value.clamp(-1.0, 1.0)
69        } else {
70            0.0
71        };
72        if value == 0.0 {
73            self.values.remove(&code);
74        } else {
75            if self.values.len() >= MAX_ACTIVE_CODES && !self.values.contains_key(&code) {
76                return;
77            }
78            self.values.insert(code, value);
79        }
80        self.recompute();
81    }
82    fn recompute(&mut self) {
83        for entry in &mut self.entries {
84            // Wider accumulation avoids inf + -inf from finite, extreme binding scales.
85            let value = entry
86                .bindings
87                .iter()
88                .map(|binding| {
89                    let raw = f64::from(self.values.get(&binding.code).copied().unwrap_or(0.0));
90                    let dead = if binding.deadzone.is_finite() {
91                        f64::from(binding.deadzone.clamp(0.0, 0.99))
92                    } else {
93                        0.0
94                    };
95                    if raw.abs() <= dead || !binding.scale.is_finite() {
96                        0.0
97                    } else {
98                        raw.signum() * (raw.abs() - dead) / (1.0 - dead) * f64::from(binding.scale)
99                    }
100                })
101                .sum::<f64>()
102                .clamp(-1.0, 1.0) as f32;
103            let down = value.abs() > 0.001;
104            let was = entry.state.value.abs() > 0.001;
105            entry.state.pressed |= down && !was;
106            entry.state.released |= !down && was;
107            entry.state.value = value;
108        }
109    }
110    /// Invalid handles have a neutral state; action ids are local to this map.
111    pub fn peek(&self, id: usize) -> Action {
112        self.entries
113            .get(id)
114            .map_or(Action::default(), |entry| entry.state)
115    }
116    pub fn consume_tick(&mut self, id: usize) -> Action {
117        let Some(entry) = self.entries.get_mut(id) else {
118            return Action::default();
119        };
120        let state = entry.state;
121        entry.state.pressed = false;
122        entry.state.released = false;
123        state
124    }
125    pub fn lose_focus(&mut self) {
126        self.values.clear();
127        self.recompute();
128    }
129}
130#[cfg(test)]
131mod tests {
132    use super::*;
133    fn binding(code: u16, scale: f32) -> Binding {
134        Binding {
135            code: InputCode::Key(code),
136            scale,
137            deadzone: 0.0,
138        }
139    }
140    #[test]
141    fn quick_tap_survives_no_simulation_and_edges_fire_once() {
142        let mut map = ActionMap::default();
143        let jump = map.action(vec![binding(32, 1.0)]);
144        map.feed(InputCode::Key(32), 1.0);
145        map.feed(InputCode::Key(32), 0.0);
146        assert_eq!(
147            map.consume_tick(jump),
148            Action {
149                value: 0.0,
150                pressed: true,
151                released: true
152            }
153        );
154        assert_eq!(map.consume_tick(jump), Action::default());
155        map.feed(InputCode::Key(32), 1.0);
156        map.lose_focus();
157        assert_eq!(map.peek(jump).value, 0.0);
158    }
159    #[test]
160    fn finite_extreme_scales_and_nonfinite_inputs_cannot_poison_actions() {
161        let mut map = ActionMap::default();
162        let id = map.action(vec![
163            binding(1, f32::MAX),
164            binding(1, f32::MAX),
165            binding(1, -f32::MAX),
166            binding(1, -f32::MAX),
167        ]);
168        map.feed(InputCode::Key(1), 1.0);
169        assert_eq!(map.peek(id).value, 0.0);
170        assert!(map.rebind(id, vec![binding(1, 1.0)]));
171        assert_eq!(map.peek(id).value, 1.0);
172        map.feed(InputCode::Key(1), f32::NAN);
173        assert_eq!(map.peek(id).value, 0.0);
174    }
175    #[test]
176    fn released_codes_free_capacity_and_registration_observes_held_input() {
177        let mut map = ActionMap::default();
178        for code in 0..1100 {
179            map.feed(InputCode::Key(code), 1.0);
180            map.feed(InputCode::Key(code), 0.0);
181        }
182        map.feed(InputCode::Key(1200), 1.0);
183        let id = map.action(vec![binding(1200, 1.0)]);
184        assert!(map.peek(id).pressed);
185        assert_eq!(map.peek(id).value, 1.0);
186        assert_eq!(map.values.len(), 1);
187    }
188    #[test]
189    fn rebind_and_handle_limits_are_transactional() {
190        let mut map = ActionMap::default();
191        let id = map.action(vec![binding(1, 1.0)]);
192        assert!(!map.rebind(id, vec![binding(2, 1.0); MAX_BINDINGS + 1]));
193        map.feed(InputCode::Key(1), 1.0);
194        assert_eq!(map.peek(id).value, 1.0);
195        assert!(
196            map.try_action(vec![binding(2, 1.0); MAX_BINDINGS + 1])
197                .is_none()
198        );
199        assert!(!map.rebind(999, vec![]));
200        assert_eq!(map.consume_tick(999), Action::default());
201    }
202}