Files
Oxide/engine/src/input/action.rs
T
Homer Simpson 9eead719b0 Import Oxide engine (Stages 0–10) under MIT license
Full project snapshot migrated to new Gitea remote without history:
engine, editor, physics, script, examples, tests, docs, and assets.
Relicensed from GPLv3 to MIT and updated repo URLs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 20:41:02 +02:00

1030 lines
37 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! [`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<Binding>,
current: Vec<Binding>,
}
/// 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<String, Action>,
axes: BTreeMap<String, AxisAction>,
axes_2d: BTreeMap<String, Axis2DAction>,
}
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<I>(&mut self, name: impl Into<String>, defaults: I) -> &mut Self
where
I: IntoIterator<Item = Binding>,
{
let name = name.into();
let defaults: Vec<Binding> = 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<Item = &str> + '_ {
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<Binding>) {
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<String>, 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<Item = &str> + '_ {
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<String>,
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<Item = &str> + '_ {
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<String, Vec<Binding>>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
axes: BTreeMap<String, AxisBinding>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
axes_2d: BTreeMap<String, Axis2DBinding>,
}
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<Item = (&str, &[Binding])> {
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<Item = (&str, &AxisBinding)> {
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<Item = (&str, &Axis2DBinding)> {
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"));
}
}