use bevy::prelude::KeyCode;
pub const BOUND_SEATS: usize = 2;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Action {
MoveUp,
MoveDown,
MoveLeft,
MoveRight,
PlaceUp,
PlaceDown,
PlaceLeft,
PlaceRight,
Remove,
ClearAll,
}
impl Action {
pub const ALL: [Action; 10] = [
Action::MoveUp,
Action::MoveDown,
Action::MoveLeft,
Action::MoveRight,
Action::PlaceUp,
Action::PlaceDown,
Action::PlaceLeft,
Action::PlaceRight,
Action::Remove,
Action::ClearAll,
];
pub fn index(self) -> usize {
Action::ALL
.iter()
.position(|&a| a == self)
.expect("every action is in ALL")
}
pub fn group(self) -> ActionGroup {
match self {
Action::MoveUp | Action::MoveDown | Action::MoveLeft | Action::MoveRight => {
ActionGroup::Move
}
Action::PlaceUp | Action::PlaceDown | Action::PlaceLeft | Action::PlaceRight => {
ActionGroup::Place
}
Action::Remove | Action::ClearAll => ActionGroup::Posts,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ActionGroup {
Move,
Place,
Posts,
}
pub const ACTIONS: usize = Action::ALL.len();
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct SeatBinds(pub [KeyCode; ACTIONS]);
impl SeatBinds {
pub fn key(&self, action: Action) -> KeyCode {
self.0[action.index()]
}
pub fn set(&mut self, action: Action, key: KeyCode) {
self.0[action.index()] = key;
}
pub fn default_for(seat: usize) -> SeatBinds {
if seat == 0 {
SeatBinds([
KeyCode::KeyW,
KeyCode::KeyS,
KeyCode::KeyA,
KeyCode::KeyD,
KeyCode::ArrowUp,
KeyCode::ArrowDown,
KeyCode::ArrowLeft,
KeyCode::ArrowRight,
KeyCode::Space,
KeyCode::ShiftLeft,
])
} else {
SeatBinds([
KeyCode::KeyI,
KeyCode::KeyK,
KeyCode::KeyJ,
KeyCode::KeyL,
KeyCode::Numpad8,
KeyCode::Numpad5,
KeyCode::Numpad4,
KeyCode::Numpad6,
KeyCode::Numpad0,
KeyCode::NumpadDecimal,
])
}
}
}
pub fn default_binds() -> [SeatBinds; BOUND_SEATS] {
std::array::from_fn(SeatBinds::default_for)
}
pub fn conflict(binds: &[SeatBinds; BOUND_SEATS], key: KeyCode) -> Option<(usize, Action)> {
for (seat, seat_binds) in binds.iter().enumerate() {
for action in Action::ALL {
if seat_binds.key(action) == key {
return Some((seat, action));
}
}
}
None
}
pub fn all_distinct(binds: &[SeatBinds; BOUND_SEATS]) -> bool {
let mut names: Vec<String> = binds
.iter()
.flat_map(|seat| seat.0.iter().copied().map(key_name))
.collect();
let total = names.len();
names.sort();
names.dedup();
names.len() == total
}
pub const GLOBAL_KEYS: [KeyCode; 6] = [
KeyCode::KeyM,
KeyCode::KeyH,
KeyCode::KeyN,
KeyCode::KeyP,
KeyCode::KeyR,
KeyCode::KeyC,
];
pub const GLOBAL_LETTERS: [char; 6] = ['M', 'H', 'N', 'P', 'R', 'C'];
pub fn global_key_from_name(name: &str) -> Option<KeyCode> {
GLOBAL_KEYS.into_iter().find(|&key| key_name(key) == name)
}
pub fn any_key_from_name(name: &str) -> Option<KeyCode> {
key_from_name(name).or_else(|| global_key_from_name(name))
}
pub fn bindable(key: KeyCode) -> bool {
!GLOBAL_KEYS.contains(&key) && BINDABLE.contains(&key)
}
pub(crate) fn all_bindable() -> impl Iterator<Item = KeyCode> {
BINDABLE
.iter()
.copied()
.filter(|key| !GLOBAL_KEYS.contains(key))
}
const BINDABLE: [KeyCode; 81] = [
KeyCode::KeyA,
KeyCode::KeyB,
KeyCode::KeyC,
KeyCode::KeyD,
KeyCode::KeyE,
KeyCode::KeyF,
KeyCode::KeyG,
KeyCode::KeyH,
KeyCode::KeyI,
KeyCode::KeyJ,
KeyCode::KeyK,
KeyCode::KeyL,
KeyCode::KeyM,
KeyCode::KeyN,
KeyCode::KeyO,
KeyCode::KeyP,
KeyCode::KeyQ,
KeyCode::KeyR,
KeyCode::KeyS,
KeyCode::KeyT,
KeyCode::KeyU,
KeyCode::KeyV,
KeyCode::KeyW,
KeyCode::KeyX,
KeyCode::KeyY,
KeyCode::KeyZ,
KeyCode::Digit0,
KeyCode::Digit1,
KeyCode::Digit2,
KeyCode::Digit3,
KeyCode::Digit4,
KeyCode::Digit5,
KeyCode::Digit6,
KeyCode::Digit7,
KeyCode::Digit8,
KeyCode::Digit9,
KeyCode::ArrowUp,
KeyCode::ArrowDown,
KeyCode::ArrowLeft,
KeyCode::ArrowRight,
KeyCode::Numpad0,
KeyCode::Numpad1,
KeyCode::Numpad2,
KeyCode::Numpad3,
KeyCode::Numpad4,
KeyCode::Numpad5,
KeyCode::Numpad6,
KeyCode::Numpad7,
KeyCode::Numpad8,
KeyCode::Numpad9,
KeyCode::NumpadAdd,
KeyCode::NumpadSubtract,
KeyCode::NumpadMultiply,
KeyCode::NumpadDivide,
KeyCode::NumpadDecimal,
KeyCode::Space,
KeyCode::Tab,
KeyCode::Backspace,
KeyCode::Insert,
KeyCode::Delete,
KeyCode::Home,
KeyCode::End,
KeyCode::PageUp,
KeyCode::PageDown,
KeyCode::ShiftLeft,
KeyCode::ShiftRight,
KeyCode::ControlLeft,
KeyCode::ControlRight,
KeyCode::AltLeft,
KeyCode::AltRight,
KeyCode::Comma,
KeyCode::Period,
KeyCode::Slash,
KeyCode::Semicolon,
KeyCode::Quote,
KeyCode::BracketLeft,
KeyCode::BracketRight,
KeyCode::Backslash,
KeyCode::Minus,
KeyCode::Equal,
KeyCode::Backquote,
];
pub fn key_name(key: KeyCode) -> String {
format!("{key:?}")
}
pub fn key_from_name(name: &str) -> Option<KeyCode> {
all_bindable().find(|&key| key_name(key) == name)
}
pub fn key_label(key: KeyCode) -> String {
let name = key_name(key);
for (spelling, cap) in [
("Comma", ","),
("Period", "."),
("Slash", "/"),
("Backslash", "\\"),
("Semicolon", ";"),
("Quote", "'"),
("BracketLeft", "["),
("BracketRight", "]"),
("Minus", "-"),
("Equal", "="),
("Backquote", "`"),
] {
if name == spelling {
return cap.to_string();
}
}
for (prefix, cap) in [
("Key", ""),
("Digit", ""),
("Arrow", ""),
("Numpad", "Num "),
] {
if let Some(rest) = name.strip_prefix(prefix) {
return format!("{cap}{rest}");
}
}
for side in ["Left", "Right"] {
if let Some(rest) = name.strip_suffix(side) {
return format!("{side} {rest}");
}
}
for (run_on, spaced) in [("PageUp", "Page Up"), ("PageDown", "Page Down")] {
if name == run_on {
return spaced.to_string();
}
}
name
}
pub fn to_text(binds: &[SeatBinds; BOUND_SEATS]) -> String {
let mut out = String::new();
for (seat, seat_binds) in binds.iter().enumerate() {
let names: Vec<String> = seat_binds.0.iter().copied().map(key_name).collect();
out.push_str(&format!("keys_p{}: {}\n", seat + 1, names.join(" ")));
}
out
}
const RETIRED: [(&str, KeyCode); 1] = [("NumpadEnter", KeyCode::NumpadDecimal)];
#[cfg(test)]
pub fn parse_seat(value: &str) -> Option<SeatBinds> {
parse_seat_moved(value).map(|(seat, _)| seat)
}
pub fn parse_seat_moved(value: &str) -> Option<(SeatBinds, Vec<usize>)> {
let names: Vec<&str> = value.split_whitespace().collect();
if names.len() != ACTIONS {
return None;
}
let mut keys = [KeyCode::Space; ACTIONS];
let mut moved = Vec::new();
for (slot, name) in names.iter().enumerate() {
keys[slot] = match key_from_name(name) {
Some(key) => key,
None => {
moved.push(slot);
RETIRED
.iter()
.find(|(retired, _)| retired == name)
.map(|&(_, key)| key)?
}
};
}
for &slot in &moved {
let taken = |key: KeyCode, keys: &[KeyCode; ACTIONS]| {
keys.iter()
.enumerate()
.any(|(at, &k)| at != slot && k == key)
};
if taken(keys[slot], &keys) {
keys[slot] = BINDABLE
.into_iter()
.find(|&key| !taken(key, &keys))
.expect("more bindable keys than actions");
}
}
let mut seen: Vec<String> = keys.iter().copied().map(key_name).collect();
seen.sort();
seen.dedup();
(seen.len() == ACTIONS).then_some((SeatBinds(keys), moved))
}
pub fn rehome(binds: &mut [SeatBinds; BOUND_SEATS], seat: usize, slot: usize) {
let key = binds[seat].0[slot];
let held = |key: KeyCode, binds: &[SeatBinds; BOUND_SEATS]| {
binds.iter().enumerate().any(|(s, seat_binds)| {
seat_binds
.0
.iter()
.enumerate()
.any(|(at, &k)| (s, at) != (seat, slot) && k == key)
})
};
if held(key, binds) {
let free = BINDABLE
.into_iter()
.find(|&key| !held(key, binds))
.expect("more bindable keys than actions");
binds[seat].0[slot] = free;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_names_round_trip() {
for key in all_bindable() {
assert_eq!(key_from_name(&key_name(key)), Some(key), "{key:?}");
}
assert_eq!(key_from_name("NoSuchKey"), None);
assert!(!bindable(KeyCode::Escape));
assert_eq!(key_from_name("Escape"), None);
}
#[test]
fn global_keys_are_not_bindable() {
for key in GLOBAL_KEYS {
assert!(!bindable(key), "{key:?} is read whatever the bindings say");
assert_eq!(key_from_name(&key_name(key)), None, "{key:?}");
}
assert!(all_bindable().all(|key| !GLOBAL_KEYS.contains(&key)));
for seat in default_binds() {
for action in Action::ALL {
assert!(bindable(seat.key(action)), "{action:?}");
}
}
}
#[test]
fn menu_keys_are_not_bindable() {
for key in crate::app::menu_ui::CLOSERS {
assert!(!bindable(key), "{key:?}");
assert_eq!(key_from_name(&key_name(key)), None, "{key:?}");
}
}
#[test]
fn a_saved_num_enter_moves_to_the_new_stock_key() {
let old = "KeyU KeyK KeyJ KeyL Numpad8 Numpad5 Numpad4 Numpad6 Numpad0 NumpadEnter";
let seat = parse_seat(old).expect("still loads");
assert_eq!(seat.key(Action::ClearAll), KeyCode::NumpadDecimal);
assert_eq!(seat.key(Action::MoveUp), KeyCode::KeyU, "the rest is kept");
assert_eq!(
default_binds()[1].key(Action::ClearAll),
KeyCode::NumpadDecimal
);
}
#[test]
fn a_moved_num_enter_gives_way_to_a_key_already_held() {
let old = "KeyU KeyK KeyJ KeyL Numpad8 Numpad5 Numpad4 Numpad6 NumpadDecimal NumpadEnter";
let (seat, moved) = parse_seat_moved(old).expect("still loads");
assert_eq!(moved, vec![9]);
assert_eq!(seat.key(Action::Remove), KeyCode::NumpadDecimal, "kept");
assert_ne!(seat.key(Action::ClearAll), KeyCode::NumpadDecimal);
let old = "KeyU KeyK KeyJ KeyL Numpad8 Numpad5 Numpad4 Numpad6 Numpad0 NumpadEnter";
let (p2, moved) = parse_seat_moved(old).expect("still loads");
let mut p1 = default_binds()[0];
p1.0[0] = KeyCode::NumpadDecimal;
let mut binds = [p1, p2];
assert!(!all_distinct(&binds), "the clash the move made");
for slot in moved {
rehome(&mut binds, 1, slot);
}
assert!(all_distinct(&binds), "{binds:?}");
assert_eq!(binds[0].0[0], KeyCode::NumpadDecimal, "P1 keeps its key");
assert_eq!(binds[1].key(Action::MoveUp), KeyCode::KeyU, "P2 its others");
}
#[test]
fn labels_read_like_keycaps() {
assert_eq!(key_label(KeyCode::KeyW), "W");
assert_eq!(key_label(KeyCode::Digit4), "4");
assert_eq!(key_label(KeyCode::ArrowUp), "Up");
assert_eq!(key_label(KeyCode::Numpad8), "Num 8");
assert_eq!(key_label(KeyCode::ShiftLeft), "Left Shift");
assert_eq!(key_label(KeyCode::ControlRight), "Right Control");
assert_eq!(key_label(KeyCode::Backquote), "`");
assert_eq!(key_label(KeyCode::PageUp), "Page Up");
}
#[test]
fn the_default_layout_has_no_clashes() {
let binds = default_binds();
for seat in 0..BOUND_SEATS {
for action in Action::ALL {
assert_eq!(
conflict(&binds, binds[seat].key(action)),
Some((seat, action)),
"{seat} {action:?} is claimed by someone else"
);
}
}
assert_eq!(conflict(&binds, KeyCode::F5), None);
}
#[test]
fn bindings_round_trip_through_text() {
let mut binds = default_binds();
binds[0].set(Action::Remove, KeyCode::Backquote);
binds[1].set(Action::MoveUp, KeyCode::Home);
let text = to_text(&binds);
for (seat, line) in text.lines().enumerate() {
let (_, value) = line.split_once(':').expect("key: value");
assert_eq!(parse_seat(value), Some(binds[seat]));
}
}
#[test]
fn broken_binding_lines_are_refused() {
assert_eq!(parse_seat("KeyW KeyS"), None, "too few keys");
assert_eq!(parse_seat("Bogus ".repeat(10).trim()), None, "unknown key");
let doubled = "KeyW KeyW KeyA KeyD ArrowUp ArrowDown ArrowLeft ArrowRight Space ShiftLeft";
assert_eq!(parse_seat(doubled), None, "one key, two jobs");
}
}