f56a1eea3b
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>
1030 lines
37 KiB
Rust
1030 lines
37 KiB
Rust
//! [`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"));
|
||
}
|
||
}
|