//! [`ActionMap`] — named actions ↔ physical [`Binding`]s, with defaults, //! runtime remapping, and RON-persistable user overrides. //! //! Game code addresses actions by name (`"Jump"`, `"Fire"`, …) and never //! the physical key, so the user-facing settings screen can rebind any //! action without touching gameplay code. Each action carries: //! //! - **`defaults`** — the code-defined initial bindings registered when //! the action is created. They never change after registration. //! - **`current`** — the bindings actually queried each frame, initially a //! clone of `defaults`. The "Restore defaults" button copies `defaults` //! back over `current`. //! //! Persistence saves only `current`. On load, the program first registers //! actions with their defaults from code, then applies the loaded overrides //! on top — unknown actions in the saved file are skipped (so removing an //! action in code never breaks an old settings file). //! //! # Multi-bind and one-key-many-actions //! //! An action can list more than one binding (e.g. `Jump` → `[Space, Mouse4]`) //! and one physical input can drive more than one action (e.g. `Space` → //! `Jump` and `Confirm` simultaneously). Edge semantics combine bindings //! with OR logic but with hysteresis applied at the action level, not the //! binding level: an already-engaged multi-bind action does not re-fire //! `action_pressed` when a *second* binding goes down, and does not fire //! `action_released` until **every** binding has been released. See //! [`ActionMap::action_pressed`] / [`action_released`](ActionMap::action_released) //! for the precise definition. //! //! ``` //! use oxide_engine::input::{ActionMap, Binding, InputState}; //! use oxide_engine::winit::keyboard::KeyCode; //! //! let mut actions = ActionMap::new(); //! actions.register("Jump", [Binding::Key(KeyCode::Space)]); //! //! let mut input = InputState::new(); //! input.press_key(KeyCode::Space); //! assert!(actions.action_pressed("Jump", &input)); //! assert!(actions.action_held("Jump", &input)); //! //! // Runtime remap. Game code keeps querying "Jump" and is unaffected. //! actions.set_bindings("Jump", vec![Binding::Key(KeyCode::KeyW)]); //! assert!(!actions.action_held("Jump", &input)); // Space is no longer "Jump" //! ``` use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use super::{Axis2DBinding, AxisBinding, Binding, InputState}; use crate::math::Vec2; /// One named action and its two binding lists (immutable defaults + /// runtime-mutable current). #[derive(Debug, Clone)] struct Action { defaults: Vec, current: Vec, } /// One named 1D axis with default + current bindings. #[derive(Debug, Clone)] struct AxisAction { defaults: AxisBinding, current: AxisBinding, } /// One named 2D axis with default + current bindings. #[derive(Debug, Clone)] struct Axis2DAction { defaults: Axis2DBinding, current: Axis2DBinding, } /// Maps named actions to physical [`Binding`]s, with separate default and /// current binding lists per action and RON-persistable user overrides. /// /// Three action kinds live in the same map under disjoint name spaces: /// **buttons** (one-shot events, [`register`](Self::register) / /// [`action_pressed`](Self::action_pressed)), **1D axes** (float values /// composed from + / − binding sets, [`register_axis`](Self::register_axis) / /// [`axis`](Self::axis)), and **2D axes** (a pair of 1D axes returning a /// [`Vec2`], [`register_axis_2d`](Self::register_axis_2d) / /// [`axis_2d`](Self::axis_2d)). The same string name can be reused across /// kinds without conflict — `Move` can be both a 2D axis and a button if /// that's what a project wants. /// /// See the [module docs](self) for the rationale behind defaults vs current /// bindings and the action-level edge hysteresis. #[derive(Debug, Default, Clone)] pub struct ActionMap { actions: BTreeMap, axes: BTreeMap, axes_2d: BTreeMap, } impl ActionMap { /// A new empty map. Register actions with [`register`](Self::register). pub fn new() -> Self { Self::default() } /// Registers an action `name` with its code-defined default bindings. /// The current bindings start as a clone of the defaults. /// /// If `name` is already registered, the existing `current` bindings are /// preserved (so a programmer adding a new default binding mid-project /// does not overwrite a user's remap), but the `defaults` list is /// replaced — restoring defaults from this point on uses the new list. pub fn register(&mut self, name: impl Into, defaults: I) -> &mut Self where I: IntoIterator, { let name = name.into(); let defaults: Vec = defaults.into_iter().collect(); self.actions .entry(name) .and_modify(|a| a.defaults = defaults.clone()) .or_insert_with(|| Action { current: defaults.clone(), defaults, }); self } /// Removes the action. Returns `true` if it existed. pub fn unregister(&mut self, name: &str) -> bool { self.actions.remove(name).is_some() } /// `true` if `name` is registered. pub fn has(&self, name: &str) -> bool { self.actions.contains_key(name) } /// Iterates the registered action names in sorted order. pub fn actions(&self) -> impl Iterator + '_ { self.actions.keys().map(String::as_str) } /// The current bindings driving `name`, or `&[]` if unregistered. pub fn bindings(&self, name: &str) -> &[Binding] { self.actions .get(name) .map(|a| a.current.as_slice()) .unwrap_or(&[]) } /// The code-defined default bindings for `name`, or `&[]` if unregistered. pub fn defaults(&self, name: &str) -> &[Binding] { self.actions .get(name) .map(|a| a.defaults.as_slice()) .unwrap_or(&[]) } /// Replaces the current bindings for `name`. No-op if unregistered. pub fn set_bindings(&mut self, name: &str, bindings: Vec) { if let Some(action) = self.actions.get_mut(name) { action.current = bindings; } } /// Appends one binding to `name`'s current list (no-op if unregistered; /// duplicates are skipped so adding the same binding twice is idempotent). pub fn add_binding(&mut self, name: &str, binding: Binding) { if let Some(action) = self.actions.get_mut(name) { if !action.current.contains(&binding) { action.current.push(binding); } } } /// Removes one binding from `name`'s current list. Returns whether it was /// present. No-op (and `false`) if the action is unregistered. pub fn remove_binding(&mut self, name: &str, binding: Binding) -> bool { let Some(action) = self.actions.get_mut(name) else { return false; }; let before = action.current.len(); action.current.retain(|b| *b != binding); action.current.len() != before } /// Empties `name`'s current list (so the action becomes unbindable until /// new bindings are set or defaults are restored). pub fn clear_bindings(&mut self, name: &str) { if let Some(action) = self.actions.get_mut(name) { action.current.clear(); } } /// Resets `name`'s current bindings back to its defaults. pub fn restore_defaults(&mut self, name: &str) { if let Some(action) = self.actions.get_mut(name) { action.current = action.defaults.clone(); } } /// Resets every action's current bindings back to its defaults — across /// all three action kinds (buttons, 1D axes, 2D axes). pub fn restore_all_defaults(&mut self) { for action in self.actions.values_mut() { action.current = action.defaults.clone(); } for axis in self.axes.values_mut() { axis.current = axis.defaults.clone(); } for axis in self.axes_2d.values_mut() { axis.current = axis.defaults.clone(); } } // --- 1D axes ---------------------------------------------------------- /// Registers a 1D axis `name` with code-defined default bindings. As /// with buttons, re-registering preserves the user's current bindings /// but updates the defaults list. pub fn register_axis(&mut self, name: impl Into, defaults: AxisBinding) -> &mut Self { let name = name.into(); self.axes .entry(name) .and_modify(|a| a.defaults = defaults.clone()) .or_insert_with(|| AxisAction { current: defaults.clone(), defaults, }); self } /// Removes the 1D axis `name`. Returns whether it existed. pub fn unregister_axis(&mut self, name: &str) -> bool { self.axes.remove(name).is_some() } /// `true` if a 1D axis `name` is registered. pub fn has_axis(&self, name: &str) -> bool { self.axes.contains_key(name) } /// Iterates registered 1D axis names in sorted order. pub fn axes(&self) -> impl Iterator + '_ { self.axes.keys().map(String::as_str) } /// The current bindings for `name`, or `None` if unregistered. pub fn axis_bindings(&self, name: &str) -> Option<&AxisBinding> { self.axes.get(name).map(|a| &a.current) } /// The default bindings for `name`, or `None` if unregistered. pub fn axis_defaults(&self, name: &str) -> Option<&AxisBinding> { self.axes.get(name).map(|a| &a.defaults) } /// Replaces the current bindings for the 1D axis `name`. No-op if /// unregistered. pub fn set_axis_bindings(&mut self, name: &str, bindings: AxisBinding) { if let Some(axis) = self.axes.get_mut(name) { axis.current = bindings; } } /// Resets axis `name`'s current bindings back to its defaults. pub fn restore_axis_defaults(&mut self, name: &str) { if let Some(axis) = self.axes.get_mut(name) { axis.current = axis.defaults.clone(); } } /// Evaluates 1D axis `name` against `input`. Returns 0.0 for /// unregistered axes. pub fn axis(&self, name: &str, input: &InputState) -> f32 { self.axes .get(name) .map(|a| a.current.value(input)) .unwrap_or(0.0) } // --- 2D axes ---------------------------------------------------------- /// Registers a 2D axis `name` with code-defined default bindings. pub fn register_axis_2d( &mut self, name: impl Into, defaults: Axis2DBinding, ) -> &mut Self { let name = name.into(); self.axes_2d .entry(name) .and_modify(|a| a.defaults = defaults.clone()) .or_insert_with(|| Axis2DAction { current: defaults.clone(), defaults, }); self } /// Removes the 2D axis `name`. Returns whether it existed. pub fn unregister_axis_2d(&mut self, name: &str) -> bool { self.axes_2d.remove(name).is_some() } /// `true` if a 2D axis `name` is registered. pub fn has_axis_2d(&self, name: &str) -> bool { self.axes_2d.contains_key(name) } /// Iterates registered 2D axis names in sorted order. pub fn axes_2d(&self) -> impl Iterator + '_ { self.axes_2d.keys().map(String::as_str) } /// The current bindings for `name`, or `None` if unregistered. pub fn axis_2d_bindings(&self, name: &str) -> Option<&Axis2DBinding> { self.axes_2d.get(name).map(|a| &a.current) } /// The default bindings for `name`, or `None` if unregistered. pub fn axis_2d_defaults(&self, name: &str) -> Option<&Axis2DBinding> { self.axes_2d.get(name).map(|a| &a.defaults) } /// Replaces the current bindings for the 2D axis `name`. No-op if /// unregistered. pub fn set_axis_2d_bindings(&mut self, name: &str, bindings: Axis2DBinding) { if let Some(axis) = self.axes_2d.get_mut(name) { axis.current = bindings; } } /// Resets 2D axis `name`'s current bindings back to its defaults. pub fn restore_axis_2d_defaults(&mut self, name: &str) { if let Some(axis) = self.axes_2d.get_mut(name) { axis.current = axis.defaults.clone(); } } /// Evaluates 2D axis `name` against `input`. Returns [`Vec2::ZERO`] for /// unregistered axes. Diagonals are unnormalized — see [`Axis2DBinding`]. pub fn axis_2d(&self, name: &str, input: &InputState) -> Vec2 { self.axes_2d .get(name) .map(|a| a.current.value(input)) .unwrap_or(Vec2::ZERO) } // --- Action edge semantics -------------------------------------------- /// `true` if `name`'s `pressed` edge fired this frame. /// /// Defined so the edge fires only when the action *transitions* from /// not-held to held: any current binding became pressed this frame and /// no current binding was held going into the frame. A second binding /// going down while the action is already engaged does **not** retrigger /// the edge. /// /// Returns `false` for unregistered actions. pub fn action_pressed(&self, name: &str, input: &InputState) -> bool { let Some(action) = self.actions.get(name) else { return false; }; let any_pressed = action.current.iter().any(|b| b.pressed(input)); let any_held_before = action.current.iter().any(|b| b.held_before_frame(input)); any_pressed && !any_held_before } /// `true` if `name`'s `released` edge fired this frame. /// /// Defined so the edge fires only when the action *transitions* from /// held to not-held: any current binding was released this frame and no /// current binding remains held. Releasing one binding of a multi-bind /// action while another is still held does **not** trigger the edge. /// /// Returns `false` for unregistered actions. pub fn action_released(&self, name: &str, input: &InputState) -> bool { let Some(action) = self.actions.get(name) else { return false; }; let any_released = action.current.iter().any(|b| b.released(input)); let any_held_now = action.current.iter().any(|b| b.held(input)); any_released && !any_held_now } /// `true` if any current binding of `name` is held right now. /// /// Returns `false` for unregistered actions. pub fn action_held(&self, name: &str, input: &InputState) -> bool { let Some(action) = self.actions.get(name) else { return false; }; action.current.iter().any(|b| b.held(input)) } // --- Persistence ------------------------------------------------------ /// Captures the **current** (possibly remapped) bindings as a /// serializable [`ActionOverrides`] across all three action kinds. /// /// Defaults are deliberately excluded: defaults live in code (the /// program calls [`register`](Self::register) / /// [`register_axis`](Self::register_axis) / /// [`register_axis_2d`](Self::register_axis_2d) at startup), so on /// load the program re-registers actions and then applies the saved /// overrides on top. That keeps the saved file small and lets the /// program evolve its default bindings without invalidating user data. pub fn overrides(&self) -> ActionOverrides { ActionOverrides { bindings: self .actions .iter() .map(|(name, a)| (name.clone(), a.current.clone())) .collect(), axes: self .axes .iter() .map(|(name, a)| (name.clone(), a.current.clone())) .collect(), axes_2d: self .axes_2d .iter() .map(|(name, a)| (name.clone(), a.current.clone())) .collect(), } } /// Applies saved [`ActionOverrides`] on top of the currently-registered /// actions. Unknown action names (in any kind) are skipped — removing /// an action from code never breaks an old settings file — and /// registered actions absent from `overrides` keep whatever current /// bindings they already have. pub fn apply_overrides(&mut self, overrides: &ActionOverrides) { for (name, bindings) in &overrides.bindings { if let Some(action) = self.actions.get_mut(name) { action.current = bindings.clone(); } } for (name, axis) in &overrides.axes { if let Some(entry) = self.axes.get_mut(name) { entry.current = axis.clone(); } } for (name, axis) in &overrides.axes_2d { if let Some(entry) = self.axes_2d.get_mut(name) { entry.current = axis.clone(); } } } } /// A serializable snapshot of an [`ActionMap`]'s current bindings — across /// all three action kinds (buttons, 1D axes, 2D axes). /// /// Round-trips through RON for storage in the settings framework, but is /// also useful as a standalone copy/paste payload (export bindings from one /// install, import on another). /// /// Missing kind sub-maps deserialize as empty (via `#[serde(default)]`), so /// an older settings file that only stored button overrides still loads /// cleanly after axes are added to a project. #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ActionOverrides { #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] bindings: BTreeMap>, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] axes: BTreeMap, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] axes_2d: BTreeMap, } impl ActionOverrides { /// A new empty overrides set. pub fn new() -> Self { Self::default() } /// `true` if no overrides are stored in any kind. pub fn is_empty(&self) -> bool { self.bindings.is_empty() && self.axes.is_empty() && self.axes_2d.is_empty() } /// Total number of override entries across all kinds. pub fn len(&self) -> usize { self.bindings.len() + self.axes.len() + self.axes_2d.len() } /// The override button bindings for `name`, or `&[]` if none. pub fn get(&self, name: &str) -> &[Binding] { self.bindings.get(name).map(Vec::as_slice).unwrap_or(&[]) } /// The override 1D axis bindings for `name`, or `None`. pub fn get_axis(&self, name: &str) -> Option<&AxisBinding> { self.axes.get(name) } /// The override 2D axis bindings for `name`, or `None`. pub fn get_axis_2d(&self, name: &str) -> Option<&Axis2DBinding> { self.axes_2d.get(name) } /// Iterates `(button action name, bindings)` pairs in name-sorted order. pub fn iter(&self) -> impl Iterator { self.bindings .iter() .map(|(k, v)| (k.as_str(), v.as_slice())) } /// Iterates `(1D axis name, bindings)` pairs in name-sorted order. pub fn iter_axes(&self) -> impl Iterator { self.axes.iter().map(|(k, v)| (k.as_str(), v)) } /// Iterates `(2D axis name, bindings)` pairs in name-sorted order. pub fn iter_axes_2d(&self) -> impl Iterator { self.axes_2d.iter().map(|(k, v)| (k.as_str(), v)) } } #[cfg(test)] mod tests { use super::*; use winit::event::MouseButton; use winit::keyboard::KeyCode; fn jump_only() -> ActionMap { let mut m = ActionMap::new(); m.register("Jump", [Binding::Key(KeyCode::Space)]); m } #[test] fn default_binding_drives_action() { let actions = jump_only(); let mut input = InputState::new(); input.press_key(KeyCode::Space); assert!(actions.action_pressed("Jump", &input)); assert!(actions.action_held("Jump", &input)); assert!(!actions.action_released("Jump", &input)); input.end_frame(); assert!(!actions.action_pressed("Jump", &input)); assert!(actions.action_held("Jump", &input)); input.release_key(KeyCode::Space); assert!(actions.action_released("Jump", &input)); assert!(!actions.action_held("Jump", &input)); } #[test] fn unregistered_action_returns_false() { let actions = ActionMap::new(); let input = InputState::new(); assert!(!actions.action_pressed("Nope", &input)); assert!(!actions.action_held("Nope", &input)); assert!(!actions.action_released("Nope", &input)); assert_eq!(actions.bindings("Nope"), &[] as &[Binding]); } #[test] fn multi_bind_first_press_fires_edge_second_does_not() { let mut actions = ActionMap::new(); actions.register( "Jump", [Binding::Key(KeyCode::Space), Binding::Key(KeyCode::KeyJ)], ); let mut input = InputState::new(); // Press Space — fires pressed edge. input.press_key(KeyCode::Space); assert!(actions.action_pressed("Jump", &input)); assert!(actions.action_held("Jump", &input)); input.end_frame(); // Now press J while Space still held — must NOT re-fire pressed. input.press_key(KeyCode::KeyJ); assert!( !actions.action_pressed("Jump", &input), "already-engaged multi-bind must not re-fire pressed" ); assert!(actions.action_held("Jump", &input)); } #[test] fn multi_bind_release_one_keeps_action_held() { let mut actions = ActionMap::new(); actions.register( "Jump", [Binding::Key(KeyCode::Space), Binding::Key(KeyCode::KeyJ)], ); let mut input = InputState::new(); input.press_key(KeyCode::Space); input.press_key(KeyCode::KeyJ); input.end_frame(); input.release_key(KeyCode::Space); assert!( !actions.action_released("Jump", &input), "another binding is still held — must not fire released" ); assert!(actions.action_held("Jump", &input)); input.end_frame(); input.release_key(KeyCode::KeyJ); assert!(actions.action_released("Jump", &input)); assert!(!actions.action_held("Jump", &input)); } #[test] fn one_key_drives_multiple_actions_simultaneously() { let mut actions = ActionMap::new(); actions.register("Jump", [Binding::Key(KeyCode::Space)]); actions.register("Confirm", [Binding::Key(KeyCode::Space)]); let mut input = InputState::new(); input.press_key(KeyCode::Space); assert!(actions.action_pressed("Jump", &input)); assert!(actions.action_pressed("Confirm", &input)); assert!(actions.action_held("Jump", &input)); assert!(actions.action_held("Confirm", &input)); } #[test] fn runtime_remap_changes_behavior_without_renaming_action() { let mut actions = jump_only(); let mut input = InputState::new(); // Initially Space → Jump. input.press_key(KeyCode::Space); assert!(actions.action_pressed("Jump", &input)); // Remap Jump to W. Game code still queries "Jump". input.end_frame(); actions.set_bindings("Jump", vec![Binding::Key(KeyCode::KeyW)]); assert!( !actions.action_held("Jump", &input), "Space must no longer drive Jump after remap" ); input.press_key(KeyCode::KeyW); assert!(actions.action_pressed("Jump", &input)); } #[test] fn add_binding_is_idempotent_and_remove_binding_returns_presence() { let mut actions = jump_only(); actions.add_binding("Jump", Binding::Key(KeyCode::KeyJ)); actions.add_binding("Jump", Binding::Key(KeyCode::KeyJ)); // dup ignored assert_eq!( actions.bindings("Jump"), &[Binding::Key(KeyCode::Space), Binding::Key(KeyCode::KeyJ)] ); assert!(actions.remove_binding("Jump", Binding::Key(KeyCode::KeyJ))); assert!(!actions.remove_binding("Jump", Binding::Key(KeyCode::KeyJ))); assert_eq!(actions.bindings("Jump"), &[Binding::Key(KeyCode::Space)]); } #[test] fn restore_defaults_undoes_runtime_remap() { let mut actions = jump_only(); actions.set_bindings("Jump", vec![Binding::Key(KeyCode::KeyW)]); assert_eq!(actions.bindings("Jump"), &[Binding::Key(KeyCode::KeyW)]); actions.restore_defaults("Jump"); assert_eq!(actions.bindings("Jump"), &[Binding::Key(KeyCode::Space)]); } #[test] fn re_registering_preserves_remap_but_updates_defaults() { let mut actions = jump_only(); actions.set_bindings("Jump", vec![Binding::Key(KeyCode::KeyW)]); // A later code change adds a second default binding. actions.register( "Jump", [ Binding::Key(KeyCode::Space), Binding::Mouse(MouseButton::Other(4)), ], ); // Current bindings (the user's remap) are preserved… assert_eq!(actions.bindings("Jump"), &[Binding::Key(KeyCode::KeyW)]); // …but restoring defaults now picks up the new code-defined list. actions.restore_defaults("Jump"); assert_eq!( actions.bindings("Jump"), &[ Binding::Key(KeyCode::Space), Binding::Mouse(MouseButton::Other(4)), ] ); } #[test] fn overrides_round_trip_through_ron() { let mut actions = ActionMap::new(); actions.register("Jump", [Binding::Key(KeyCode::Space)]); actions.register("Fire", [Binding::Mouse(MouseButton::Left)]); actions.set_bindings("Jump", vec![Binding::Key(KeyCode::KeyW)]); let overrides = actions.overrides(); let serialized = ron::to_string(&overrides).unwrap(); let parsed: ActionOverrides = ron::from_str(&serialized).unwrap(); assert_eq!(parsed, overrides); // Applying the round-tripped overrides on a freshly-registered map // recreates the remap — and unknown actions in the file are skipped. let mut fresh = ActionMap::new(); fresh.register("Jump", [Binding::Key(KeyCode::Space)]); // Note: "Fire" is intentionally NOT registered in `fresh`. fresh.apply_overrides(&parsed); assert_eq!(fresh.bindings("Jump"), &[Binding::Key(KeyCode::KeyW)]); assert!(!fresh.has("Fire"), "unknown action stayed unregistered"); } #[test] fn apply_overrides_skips_unregistered_actions() { let mut actions = jump_only(); let mut overrides = ActionOverrides::new(); overrides .bindings .insert("Phantom".into(), vec![Binding::Key(KeyCode::KeyX)]); overrides .bindings .insert("Jump".into(), vec![Binding::Key(KeyCode::KeyW)]); actions.apply_overrides(&overrides); assert_eq!(actions.bindings("Jump"), &[Binding::Key(KeyCode::KeyW)]); assert!(!actions.has("Phantom")); } #[test] fn unregister_drops_the_action() { let mut actions = jump_only(); assert!(actions.unregister("Jump")); assert!(!actions.has("Jump")); assert!(!actions.unregister("Jump")); } #[test] fn clear_bindings_makes_action_unfireable_until_restored() { let mut actions = jump_only(); actions.clear_bindings("Jump"); let mut input = InputState::new(); input.press_key(KeyCode::Space); assert!(!actions.action_pressed("Jump", &input)); assert!(!actions.action_held("Jump", &input)); actions.restore_defaults("Jump"); assert!(actions.action_pressed("Jump", &input)); } // --- Piece 3: 1D/2D axes through ActionMap ---------------------------- fn move_x() -> ActionMap { let mut m = ActionMap::new(); m.register_axis( "MoveX", AxisBinding::new([Binding::Key(KeyCode::KeyD)], [Binding::Key(KeyCode::KeyA)]), ); m } #[test] fn axis_returns_zero_when_unregistered_or_idle() { let map = ActionMap::new(); let input = InputState::new(); assert_eq!(map.axis("MoveX", &input), 0.0); assert_eq!(map.axis_2d("Move", &input), Vec2::ZERO); let map = move_x(); let input = InputState::new(); assert_eq!(map.axis("MoveX", &input), 0.0); } #[test] fn axis_resolves_positive_and_negative_directions() { let map = move_x(); let mut input = InputState::new(); input.press_key(KeyCode::KeyD); assert_eq!(map.axis("MoveX", &input), 1.0); input.release_key(KeyCode::KeyD); input.press_key(KeyCode::KeyA); assert_eq!(map.axis("MoveX", &input), -1.0); } #[test] fn axis_remap_changes_binding_without_renaming() { let mut map = move_x(); map.set_axis_bindings( "MoveX", AxisBinding::new( [Binding::Key(KeyCode::ArrowRight)], [Binding::Key(KeyCode::ArrowLeft)], ), ); let mut input = InputState::new(); input.press_key(KeyCode::KeyD); assert_eq!( map.axis("MoveX", &input), 0.0, "old binding no longer drives the axis" ); input.press_key(KeyCode::ArrowRight); assert_eq!(map.axis("MoveX", &input), 1.0); map.restore_axis_defaults("MoveX"); let mut input = InputState::new(); input.press_key(KeyCode::KeyD); assert_eq!(map.axis("MoveX", &input), 1.0); } #[test] fn axis_2d_resolves_wasd() { let mut map = ActionMap::new(); map.register_axis_2d( "Move", Axis2DBinding::new( [Binding::Key(KeyCode::KeyD)], [Binding::Key(KeyCode::KeyA)], [Binding::Key(KeyCode::KeyW)], [Binding::Key(KeyCode::KeyS)], ), ); let mut input = InputState::new(); input.press_key(KeyCode::KeyW); input.press_key(KeyCode::KeyA); assert_eq!(map.axis_2d("Move", &input), Vec2::new(-1.0, 1.0)); } #[test] fn restore_all_defaults_covers_buttons_and_axes() { let mut map = ActionMap::new(); map.register("Jump", [Binding::Key(KeyCode::Space)]); map.register_axis( "MoveX", AxisBinding::new([Binding::Key(KeyCode::KeyD)], [Binding::Key(KeyCode::KeyA)]), ); map.register_axis_2d( "Move", Axis2DBinding::new( [Binding::Key(KeyCode::KeyD)], [Binding::Key(KeyCode::KeyA)], [Binding::Key(KeyCode::KeyW)], [Binding::Key(KeyCode::KeyS)], ), ); // Remap all three kinds. map.set_bindings("Jump", vec![Binding::Key(KeyCode::KeyJ)]); map.set_axis_bindings( "MoveX", AxisBinding::new( [Binding::Key(KeyCode::ArrowRight)], [Binding::Key(KeyCode::ArrowLeft)], ), ); map.set_axis_2d_bindings( "Move", Axis2DBinding::new( [Binding::Key(KeyCode::ArrowRight)], [Binding::Key(KeyCode::ArrowLeft)], [Binding::Key(KeyCode::ArrowUp)], [Binding::Key(KeyCode::ArrowDown)], ), ); map.restore_all_defaults(); assert_eq!(map.bindings("Jump"), &[Binding::Key(KeyCode::Space)]); assert_eq!( map.axis_bindings("MoveX").unwrap().positive, vec![Binding::Key(KeyCode::KeyD)] ); assert_eq!( map.axis_2d_bindings("Move").unwrap().y.positive, vec![Binding::Key(KeyCode::KeyW)] ); } #[test] fn three_action_kinds_share_a_name_without_conflict() { // Buttons, 1D axes, and 2D axes have disjoint namespaces. let mut map = ActionMap::new(); map.register("Move", [Binding::Key(KeyCode::KeyM)]); map.register_axis( "Move", AxisBinding::new([Binding::Key(KeyCode::KeyD)], [Binding::Key(KeyCode::KeyA)]), ); map.register_axis_2d( "Move", Axis2DBinding::new( [Binding::Key(KeyCode::ArrowRight)], [Binding::Key(KeyCode::ArrowLeft)], [Binding::Key(KeyCode::ArrowUp)], [Binding::Key(KeyCode::ArrowDown)], ), ); let mut input = InputState::new(); input.press_key(KeyCode::KeyM); input.press_key(KeyCode::KeyD); input.press_key(KeyCode::ArrowUp); assert!(map.action_held("Move", &input)); assert_eq!(map.axis("Move", &input), 1.0); assert_eq!(map.axis_2d("Move", &input), Vec2::new(0.0, 1.0)); } #[test] fn overrides_round_trip_includes_axes() { let mut map = ActionMap::new(); map.register("Jump", [Binding::Key(KeyCode::Space)]); map.register_axis( "MoveX", AxisBinding::new([Binding::Key(KeyCode::KeyD)], [Binding::Key(KeyCode::KeyA)]), ); map.register_axis_2d( "Move", Axis2DBinding::new( [Binding::Key(KeyCode::KeyD)], [Binding::Key(KeyCode::KeyA)], [Binding::Key(KeyCode::KeyW)], [Binding::Key(KeyCode::KeyS)], ), ); // Remap each kind. map.set_bindings("Jump", vec![Binding::Key(KeyCode::KeyJ)]); map.set_axis_bindings( "MoveX", AxisBinding::new( [Binding::Key(KeyCode::ArrowRight)], [Binding::Key(KeyCode::ArrowLeft)], ), ); map.set_axis_2d_bindings( "Move", Axis2DBinding::new( [Binding::Key(KeyCode::ArrowRight)], [Binding::Key(KeyCode::ArrowLeft)], [Binding::Key(KeyCode::ArrowUp)], [Binding::Key(KeyCode::ArrowDown)], ), ); let overrides = map.overrides(); let s = ron::to_string(&overrides).unwrap(); let parsed: ActionOverrides = ron::from_str(&s).unwrap(); assert_eq!(parsed, overrides); assert_eq!(overrides.len(), 3); assert!(!overrides.is_empty()); // Replay on a freshly-registered map. let mut fresh = ActionMap::new(); fresh.register("Jump", [Binding::Key(KeyCode::Space)]); fresh.register_axis( "MoveX", AxisBinding::new([Binding::Key(KeyCode::KeyD)], [Binding::Key(KeyCode::KeyA)]), ); fresh.register_axis_2d( "Move", Axis2DBinding::new( [Binding::Key(KeyCode::KeyD)], [Binding::Key(KeyCode::KeyA)], [Binding::Key(KeyCode::KeyW)], [Binding::Key(KeyCode::KeyS)], ), ); fresh.apply_overrides(&parsed); assert_eq!(fresh.bindings("Jump"), &[Binding::Key(KeyCode::KeyJ)]); assert_eq!( fresh.axis_bindings("MoveX").unwrap().positive, vec![Binding::Key(KeyCode::ArrowRight)] ); assert_eq!( fresh.axis_2d_bindings("Move").unwrap().x.negative, vec![Binding::Key(KeyCode::ArrowLeft)] ); } #[test] fn old_settings_file_without_axes_loads_cleanly() { // A legacy file that only stored button overrides — no `axes` or // `axes_2d` fields. Must still load, leaving registered axes at // their defaults. let legacy_ron = r#"(bindings: {"Jump": [Key(KeyW)]})"#; let parsed: ActionOverrides = ron::from_str(legacy_ron).unwrap(); assert_eq!(parsed.len(), 1); assert_eq!(parsed.get_axis("MoveX"), None); let mut map = ActionMap::new(); map.register("Jump", [Binding::Key(KeyCode::Space)]); map.register_axis( "MoveX", AxisBinding::new([Binding::Key(KeyCode::KeyD)], [Binding::Key(KeyCode::KeyA)]), ); map.apply_overrides(&parsed); assert_eq!(map.bindings("Jump"), &[Binding::Key(KeyCode::KeyW)]); // Axis bindings untouched — still defaults. assert_eq!( map.axis_bindings("MoveX").unwrap().positive, vec![Binding::Key(KeyCode::KeyD)] ); } #[test] fn unregister_axis_drops_each_kind_independently() { let mut map = ActionMap::new(); map.register_axis("MoveX", AxisBinding::new([Binding::Key(KeyCode::KeyD)], [])); assert!(map.has_axis("MoveX")); assert!(map.unregister_axis("MoveX")); assert!(!map.has_axis("MoveX")); assert!(!map.unregister_axis("MoveX")); } }