use std::collections::HashMap;
use std::collections::hash_map::Entry::{Occupied, Vacant};
use crate::input::action::{InputAction, InputActions};
use crate::input::binding::{
Axis2Binding, Axis2Source, AxisBinding, AxisSource, ButtonBinding, Deadzone, JoystickControl,
Key, Knobs, MouseButton, Pad, PadAxis, PointerDelta, Stick, WheelDelta,
};
use sealed::Binding;
const HEADER: &str = "mirage-engine bindings 1";
const BUTTON: &str = "button";
const AXIS: &str = "axis";
const AXIS2: &str = "axis2";
pub struct Table {
buttons: Seat<ButtonBinding>,
axes: Seat<AxisBinding>,
axes2: Seat<Axis2Binding>,
}
impl Table {
pub(crate) fn new<A: InputActions>(stored: Option<&str>) -> Self {
let mut table = Self {
buttons: Seat::of::<A::Button>(),
axes: Seat::of::<A::Axis>(),
axes2: Seat::of::<A::Axis2>(),
};
if let Some(text) = stored {
table.overlay(text);
}
table
}
pub(crate) fn resolve<A: InputAction>(&self, action: A) -> &[A::Binding] {
A::Binding::seat(self).entry(action)
}
pub(crate) fn rebind<A: InputAction>(
&mut self,
action: A,
bindings: Vec<A::Binding>,
) -> Rebound {
A::Binding::seat_mut(self).replace(action, bindings)
}
pub(crate) fn written(&self) -> String {
let mut out = String::from(HEADER);
out.push('\n');
self.buttons.write(BUTTON, &mut out);
self.axes.write(AXIS, &mut out);
self.axes2.write(AXIS2, &mut out);
out
}
fn overlay(&mut self, text: &str) {
let mut lines = text.lines();
if lines.next().map(str::trim) != Some(HEADER) {
log::debug!("the stored bindings are not this version's; the declared ones stand");
return;
}
for line in lines.filter(|line| !line.trim().is_empty()) {
if !self.take(line) {
log::debug!("a stored binding was dropped: {line}");
}
}
}
fn take(&mut self, line: &str) -> bool {
let mut words = Words::new(line);
let (Some(kind), Some(name)) = (words.word(), words.word()) else {
return false;
};
match kind {
BUTTON => self.buttons.take(name, &mut words),
AXIS => self.axes.take(name, &mut words),
AXIS2 => self.axes2.take(name, &mut words),
_ => false,
}
}
}
pub struct Seat<B> {
order: Vec<&'static str>,
entries: HashMap<&'static str, Entry<B>>,
}
impl<B: Binding> Seat<B> {
fn of<A: InputAction<Binding = B>>() -> Self {
let mut filled = Self {
order: Vec::new(),
entries: HashMap::new(),
};
for action in A::all() {
filled.seat(action);
}
filled
}
fn seat<A: InputAction<Binding = B>>(&mut self, action: A) -> &mut Entry<B> {
match self.entries.entry(action.name()) {
Occupied(seated) => seated.into_mut(),
Vacant(empty) => {
self.order.push(action.name());
let declared = action.defaults();
empty.insert(Entry {
live: declared.clone(),
declared,
})
}
}
}
fn entry<A: InputAction<Binding = B>>(&self, action: A) -> &[B] {
match self.entries.get(action.name()) {
Some(entry) => &entry.live,
None => &[],
}
}
fn replace<A: InputAction<Binding = B>>(&mut self, action: A, bindings: Vec<B>) -> Rebound {
let entry = self.seat(action);
if entry.live == bindings {
return Rebound::Unchanged;
}
entry.live = bindings;
Rebound::Changed
}
fn write(&self, kind: &str, out: &mut String) {
for name in &self.order {
let Some(entry) = self.entries.get(name).filter(|entry| entry.rebound()) else {
continue;
};
out.push_str(kind);
out.push(' ');
out.push_str(name);
for binding in &entry.live {
out.push(' ');
binding.write(out);
}
out.push('\n');
}
}
fn take(&mut self, name: &str, words: &mut Words<'_>) -> bool {
let Some(entry) = self.entries.get_mut(name) else {
return false;
};
let read = core::iter::from_fn(|| words.more().then(|| B::read(words)));
let Some(bindings) = read.collect::<Option<Vec<B>>>() else {
return false;
};
entry.live = bindings;
true
}
}
struct Entry<B> {
declared: Vec<B>,
live: Vec<B>,
}
impl<B: PartialEq> Entry<B> {
fn rebound(&self) -> bool {
self.live != self.declared
}
}
pub struct Words<'a>(core::iter::Peekable<core::str::SplitAsciiWhitespace<'a>>);
impl<'a> Words<'a> {
fn new(line: &'a str) -> Self {
Self(line.split_ascii_whitespace().peekable())
}
pub(crate) fn word(&mut self) -> Option<&'a str> {
self.0.next()
}
pub(crate) fn control(&mut self) -> Option<JoystickControl> {
self.word()?.parse().ok().map(JoystickControl::new)
}
pub(crate) fn number(&mut self) -> Option<f32> {
self.word()?.parse().ok()
}
fn more(&mut self) -> bool {
self.0.peek().is_some()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum Rebound {
Changed,
Unchanged,
}
impl Binding for ButtonBinding {
fn seat(table: &Table) -> &Seat<Self> {
&table.buttons
}
fn seat_mut(table: &mut Table) -> &mut Seat<Self> {
&mut table.buttons
}
fn write(&self, out: &mut String) {
match self {
Self::Key(key) => named(out, "key", key.token()),
Self::Mouse(button) => named(out, "mouse", button.token()),
Self::Pad(button) => named(out, "pad", button.token()),
Self::Joystick(control) => named(out, "joystick", &control.to_string()),
}
}
fn read(words: &mut Words<'_>) -> Option<Self> {
match words.word()? {
"key" => Key::from_token(words.word()?).map(Self::Key),
"mouse" => MouseButton::from_token(words.word()?).map(Self::Mouse),
"pad" => Pad::from_token(words.word()?).map(Self::Pad),
"joystick" => Some(Self::Joystick(words.control()?)),
_ => None,
}
}
}
impl Binding for AxisBinding {
fn seat(table: &Table) -> &Seat<Self> {
&table.axes
}
fn seat_mut(table: &mut Table) -> &mut Seat<Self> {
&mut table.axes
}
fn write(&self, out: &mut String) {
match &self.source {
AxisSource::Pad(axis) => named(out, "padaxis", axis.token()),
AxisSource::Joystick(control) => named(out, "joyaxis", &control.to_string()),
AxisSource::Pointer(lane) => named(out, "pointer_delta", lane.token()),
AxisSource::Wheel(lane) => named(out, "wheel_delta", lane.token()),
AxisSource::Buttons { negative, positive } => {
out.push_str("pair ");
negative.write(out);
out.push(' ');
positive.write(out);
}
}
out.push(' ');
write_knobs(self.knobs, out);
}
fn read(words: &mut Words<'_>) -> Option<Self> {
let source = match words.word()? {
"padaxis" => AxisSource::Pad(PadAxis::from_token(words.word()?)?),
"joyaxis" => AxisSource::Joystick(words.control()?),
"pointer_delta" => AxisSource::Pointer(PointerDelta::from_token(words.word()?)?),
"wheel_delta" => AxisSource::Wheel(WheelDelta::from_token(words.word()?)?),
"pair" => AxisSource::Buttons {
negative: ButtonBinding::read(words)?,
positive: ButtonBinding::read(words)?,
},
_ => return None,
};
Some(Self {
source,
knobs: read_knobs(words)?,
})
}
}
impl Binding for Axis2Binding {
fn seat(table: &Table) -> &Seat<Self> {
&table.axes2
}
fn seat_mut(table: &mut Table) -> &mut Seat<Self> {
&mut table.axes2
}
fn write(&self, out: &mut String) {
match &self.source {
Axis2Source::Stick(stick) => named(out, "stick", stick.token()),
Axis2Source::Pointer => out.push_str("pointer"),
Axis2Source::Wheel => out.push_str("wheel"),
Axis2Source::Buttons {
left,
right,
down,
up,
} => {
out.push_str("quad");
for button in [left, right, down, up] {
out.push(' ');
button.write(out);
}
}
}
out.push(' ');
write_knobs(self.knobs, out);
}
fn read(words: &mut Words<'_>) -> Option<Self> {
let source = match words.word()? {
"stick" => Axis2Source::Stick(Stick::from_token(words.word()?)?),
"pointer" => Axis2Source::Pointer,
"wheel" => Axis2Source::Wheel,
"quad" => Axis2Source::Buttons {
left: ButtonBinding::read(words)?,
right: ButtonBinding::read(words)?,
down: ButtonBinding::read(words)?,
up: ButtonBinding::read(words)?,
},
_ => return None,
};
Some(Self {
source,
knobs: read_knobs(words)?,
})
}
}
fn named(out: &mut String, kind: &str, control: &str) {
out.push_str(kind);
out.push(' ');
out.push_str(control);
}
fn write_knobs(knobs: Knobs, out: &mut String) {
out.push_str(&knobs.deadzone.get().to_string());
out.push(' ');
out.push_str(&knobs.scale.to_string());
out.push_str(match knobs.inverted {
true => " inverted",
false => " plain",
});
}
fn read_knobs(words: &mut Words<'_>) -> Option<Knobs> {
let (deadzone, scale) = (Deadzone::new(words.number()?), words.number()?);
let inverted = match words.word()? {
"inverted" => true,
"plain" => false,
_ => return None,
};
Some(Knobs::stored(deadzone, scale, inverted))
}
pub(crate) mod sealed {
use super::{Seat, Table, Words};
pub trait Binding: Copy + PartialEq + Sized + 'static {
fn seat(table: &Table) -> &Seat<Self>;
fn seat_mut(table: &mut Table) -> &mut Seat<Self>;
fn write(&self, out: &mut String);
fn read(words: &mut Words<'_>) -> Option<Self>;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::input::action::{
InputAxis2Action, InputAxisAction, InputButtonAction, NoInputAxes2,
};
use crate::input::binding::{ButtonAxis, ButtonAxis2, Stick};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Fire {
Shoot,
Pause,
}
impl Fire {
fn declared(self) -> Vec<ButtonBinding> {
match self {
Self::Shoot => vec![Key::Space.into(), Pad::South.into()],
Self::Pause => vec![Key::Escape.into()],
}
}
}
impl InputButtonAction for Fire {
fn bindings(&self) -> Vec<ButtonBinding> {
self.declared()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Lean {
Roll,
}
impl Lean {
fn declared(self) -> Vec<AxisBinding> {
vec![
AxisBinding::pad(PadAxis::LeftX),
AxisBinding::from(ButtonAxis {
negative: Key::A,
positive: Key::D,
}),
]
}
}
impl InputAxisAction for Lean {
fn bindings(&self) -> Vec<AxisBinding> {
self.declared()
}
}
macro_rules! vocabulary {
($name:ident, $binding:ty, $($variant:ident),*) => {
impl InputAction for $name {
type Binding = $binding;
fn defaults(&self) -> Vec<$binding> {
self.declared()
}
fn all() -> Vec<Self> {
vec![$(Self::$variant),*]
}
fn name(&self) -> &'static str {
match self {
$(Self::$variant => stringify!($variant),)*
}
}
fn from_name(name: &str) -> Option<Self> {
match name {
$(stringify!($variant) => Some(Self::$variant),)*
_ => None,
}
}
}
};
}
vocabulary!(Fire, ButtonBinding, Shoot, Pause);
vocabulary!(Lean, AxisBinding, Roll);
struct Controls;
impl InputActions for Controls {
type Button = Fire;
type Axis = Lean;
type Axis2 = NoInputAxes2;
}
fn table(stored: Option<&str>) -> Table {
Table::new::<Controls>(stored)
}
#[test]
fn an_action_starts_at_what_its_vocabulary_declares() {
let table = table(None);
assert_eq!(table.resolve(Fire::Shoot), &Fire::Shoot.declared());
assert_eq!(
table.written(),
format!("{HEADER}\n"),
"and nothing is kept"
);
}
#[test]
fn a_rebind_to_what_an_action_already_reads_through_changes_nothing() {
let mut table = table(None);
assert_eq!(
table.rebind(Fire::Shoot, Fire::Shoot.declared()),
Rebound::Unchanged
);
assert_eq!(
table.rebind(Fire::Shoot, vec![Key::Tab.into()]),
Rebound::Changed
);
assert_eq!(
table.rebind(Fire::Shoot, vec![Key::Tab.into()]),
Rebound::Unchanged,
"the same again, declared or not"
);
}
#[test]
fn a_rebind_replaces_one_action_and_is_the_only_thing_kept() {
let mut table = table(None);
assert_eq!(
table.rebind(Fire::Pause, vec![Key::P.into(), Pad::Start.into()]),
Rebound::Changed
);
assert_eq!(
table.resolve(Fire::Pause),
&[ButtonBinding::Key(Key::P), ButtonBinding::Pad(Pad::Start)]
);
assert_eq!(table.resolve(Fire::Shoot), &Fire::Shoot.declared());
assert_eq!(
table.written(),
format!("{HEADER}\nbutton Pause key P pad Start\n")
);
}
#[test]
fn what_the_store_kept_is_laid_over_the_declarations_and_reads_back_the_same() {
let mut written = table(None);
written.rebind(
Fire::Shoot,
vec![
Pad::East.into(),
ButtonBinding::Joystick(JoystickControl::new(9)),
],
);
written.rebind(
Lean::Roll,
vec![
AxisBinding::pad(PadAxis::RightX).deadzone(0.25).invert(),
AxisBinding::from(ButtonAxis {
negative: Key::Left,
positive: Key::Right,
})
.scale(0.5),
AxisBinding::wheel(WheelDelta::Up).scale(0.125),
AxisBinding::pointer_delta(PointerDelta::Up).invert(),
AxisBinding::joystick(JoystickControl::new(3)),
],
);
let kept = written.written();
let read = table(Some(&kept));
assert_eq!(read.resolve(Fire::Shoot), written.resolve(Fire::Shoot));
assert_eq!(read.resolve(Lean::Roll), written.resolve(Lean::Roll));
assert_eq!(read.written(), kept, "and writes the same file again");
}
#[test]
fn a_stored_wheel_binding_names_its_lane_and_reads_that_lane_back() {
let mut written = table(None);
written.rebind(Lean::Roll, vec![AxisBinding::wheel(WheelDelta::Sideways)]);
let kept = written.written();
assert!(
kept.contains("wheel_delta Sideways"),
"the line names the lane it reads: {kept}"
);
assert_eq!(
table(Some(&kept)).resolve(Lean::Roll),
&[AxisBinding::wheel(WheelDelta::Sideways)]
);
assert_eq!(
table(Some(&kept.replace("Sideways", "Up"))).resolve(Lean::Roll),
&[AxisBinding::wheel(WheelDelta::Up)],
"and a line naming the other lane reads back as that one"
);
}
#[test]
fn a_vector_binding_reads_back_through_the_store_too() {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Walk {
Move,
}
vocabulary!(Walk, Axis2Binding, Move);
impl Walk {
fn declared(self) -> Vec<Axis2Binding> {
vec![Axis2Binding::stick(Stick::Left)]
}
}
impl InputAxis2Action for Walk {
fn bindings(&self) -> Vec<Axis2Binding> {
self.declared()
}
}
struct Feet;
impl InputActions for Feet {
type Button = crate::input::action::NoInputButtons;
type Axis = crate::input::action::NoInputAxes;
type Axis2 = Walk;
}
let mut written = Table::new::<Feet>(None);
written.rebind(
Walk::Move,
vec![
Axis2Binding::from(ButtonAxis2 {
left: Key::A,
right: Key::D,
down: Key::S,
up: Key::W,
}),
Axis2Binding::stick(Stick::Right).invert(),
Axis2Binding::pointer().scale(0.01),
Axis2Binding::wheel().scale(0.5),
],
);
let kept = written.written();
let read = Table::new::<Feet>(Some(&kept));
assert_eq!(read.resolve(Walk::Move), written.resolve(Walk::Move));
}
#[test]
fn a_stored_name_no_action_answers_to_is_dropped_and_the_rest_still_read() {
let kept = format!(
"{HEADER}\n\
button Duck key C\n\
button Pause key P\n"
);
let table = table(Some(&kept));
assert_eq!(table.resolve(Fire::Pause), &[ButtonBinding::Key(Key::P)]);
assert_eq!(table.resolve(Fire::Shoot), &Fire::Shoot.declared());
}
#[test]
fn a_store_that_reads_as_nothing_leaves_every_declaration_standing() {
let declared = Fire::Shoot.declared();
for broken in [
"",
"nonsense",
"mirage-engine bindings 2\nbutton Shoot key P\n",
&format!("{HEADER}\nbutton Shoot key Nonexistent\n"),
&format!("{HEADER}\nbutton Shoot key\n"),
&format!("{HEADER}\nbutton Shoot key P junk\n"),
&format!("{HEADER}\naxis Roll padaxis LeftX 0.1\n"),
&format!("{HEADER}\naxis Roll padaxis LeftX 0.1 1 sideways\n"),
] {
let table = table(Some(broken));
assert_eq!(
table.resolve(Fire::Shoot),
&declared,
"`{broken}` left the declaration alone"
);
}
}
#[test]
fn a_stored_knob_reads_back_clamped_the_way_a_binding_sets_it() {
let kept = format!("{HEADER}\naxis Roll padaxis LeftX 2 1 plain\n");
let numberless = table(Some(&format!(
"{HEADER}\naxis Roll padaxis LeftX NaN 1 plain\n"
)));
let table = table(Some(&kept));
let [deep] = table.resolve(Lean::Roll) else {
panic!("the stored line bound one control");
};
let deepest = AxisBinding::pad(PadAxis::LeftX).deadzone(2.0);
assert_eq!(
deep.knobs.deadzone, deepest.knobs.deadzone,
"as deep as a game could have set it and no deeper"
);
assert_eq!(
deep.resolve(0.0),
0.0,
"so a control at rest still reads as nothing"
);
let [broken] = numberless.resolve(Lean::Roll) else {
panic!("the stored line bound one control");
};
assert_eq!(
broken.knobs.deadzone.get(),
0.0,
"and a deadzone that is not a number deadens nothing"
);
}
#[test]
fn a_stored_table_mangled_any_which_way_still_reads_as_one_this_run_can_use() {
let mut written = table(None);
written.rebind(
Fire::Shoot,
vec![
Pad::East.into(),
ButtonBinding::Joystick(JoystickControl::new(9)),
],
);
written.rebind(
Lean::Roll,
vec![AxisBinding::pad(PadAxis::RightX).deadzone(0.25).invert()],
);
let kept = written.written();
for mangled in crate::platform::manglings(&kept) {
let mut read = table(Some(&mangled));
read.rebind(Fire::Pause, vec![Key::P.into()]);
assert_eq!(
read.resolve(Fire::Pause),
&[ButtonBinding::Key(Key::P)],
"over {mangled:?}"
);
assert_eq!(
table(Some(&read.written())).written(),
read.written(),
"and what it writes reads back the same, over {mangled:?}"
);
}
}
#[test]
fn an_action_bound_to_nothing_is_kept_as_nothing() {
let mut table = table(None);
table.rebind(Fire::Shoot, Vec::new());
let kept = table.written();
assert_eq!(kept, format!("{HEADER}\nbutton Shoot\n"));
assert!(
Table::new::<Controls>(Some(&kept))
.resolve(Fire::Shoot)
.is_empty()
);
}
}