111 lines
3.8 KiB
Python
111 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
import configparser
|
|
from pathlib import Path
|
|
|
|
DEFAULT_KEYBINDINGS: dict[str, list[str]] = {
|
|
"move_up": ["w", "ArrowUp"],
|
|
"move_down": ["s", "ArrowDown"],
|
|
"move_left": ["a", "ArrowLeft"],
|
|
"move_right": ["d", "ArrowRight"],
|
|
"leap": ["space"],
|
|
"swim_down": ["Control"],
|
|
"block": ["b"],
|
|
"activate_right_hand": ["mouse_left"],
|
|
"activate_left_hand": ["mouse_right"],
|
|
"activate_right_hand_2": ["shift+mouse_left"],
|
|
"activate_left_hand_2": ["shift+mouse_right"],
|
|
"select_ability_1": ["1"],
|
|
"select_ability_2": ["2"],
|
|
"select_ability_3": ["3"],
|
|
"select_ability_4": ["4"],
|
|
"select_ability_5": ["5"],
|
|
"select_ability_6": ["6"],
|
|
"select_ability_7": ["7"],
|
|
"select_ability_8": ["8"],
|
|
"select_ability_9": ["9"],
|
|
"select_ability_0": ["0"],
|
|
"activate_selected_ability": ["e"],
|
|
"pickup": ["f"],
|
|
"pause": ["Escape"],
|
|
"toggle_character_card": ["c"],
|
|
"cycle_character_tab": ["Tab"],
|
|
"toggle_dev_mode": ["F1"],
|
|
"dev_cycle_spawn": ["F2"],
|
|
"dev_spawn_object": ["F3"],
|
|
"dev_spawn_npc_menu": ["F4"],
|
|
"dev_cycle_turret": ["F5"],
|
|
"dev_place_turret": ["F6"],
|
|
"dev_edit_player_factions": ["F7"],
|
|
"dev_edit_target_factions": ["F8"],
|
|
}
|
|
|
|
# Human-friendly config names for keys that don't have a printable representation.
|
|
_KEY_ALIASES = {"space": " "}
|
|
|
|
|
|
def _normalize(key: str) -> str:
|
|
return _KEY_ALIASES.get(key, key)
|
|
|
|
|
|
class KeyBindings:
|
|
"""Action -> list of keys, loaded from an editable .conf file (arrays, so several keys
|
|
can share one action). Falls back to DEFAULT_KEYBINDINGS for anything the file omits.
|
|
"""
|
|
|
|
def __init__(self, bindings: dict[str, list[str]]):
|
|
self.bindings = bindings
|
|
self._action_by_key: dict[str, str] = {}
|
|
self._reindex()
|
|
|
|
def _reindex(self) -> None:
|
|
self._action_by_key = {}
|
|
for action, keys in self.bindings.items():
|
|
for key in keys:
|
|
self._action_by_key[_normalize(key)] = action
|
|
|
|
@classmethod
|
|
def load(cls, path: Path) -> "KeyBindings":
|
|
bindings = {action: list(keys) for action, keys in DEFAULT_KEYBINDINGS.items()}
|
|
if path.exists():
|
|
parser = configparser.ConfigParser()
|
|
parser.read(path)
|
|
for section in parser.sections():
|
|
for action, raw in parser.items(section):
|
|
keys = [k.strip() for k in raw.split(",") if k.strip()]
|
|
if keys:
|
|
bindings[action] = keys
|
|
return cls(bindings)
|
|
|
|
def action_for_key(self, key: str) -> str | None:
|
|
return self._action_by_key.get(key)
|
|
|
|
def keys_for_action(self, action: str) -> list[str]:
|
|
return self.bindings.get(action, [])
|
|
|
|
def rebind(self, action: str, key: str, index: int = 0) -> None:
|
|
"""Replaces the key at `index` in `action`'s bound-key list (default: its primary key)
|
|
with `key` - the "configure controls" menu's rebind action (engine/controls_menu.py).
|
|
Steals `key` away from any other action it was already bound to first, so two actions
|
|
can never end up racing for the same input.
|
|
"""
|
|
key = _normalize(key)
|
|
for other_action, keys in self.bindings.items():
|
|
if other_action != action and key in keys:
|
|
keys.remove(key)
|
|
keys = self.bindings.setdefault(action, [])
|
|
if index < len(keys):
|
|
keys[index] = key
|
|
else:
|
|
keys.append(key)
|
|
self._reindex()
|
|
|
|
def save(self, path: Path) -> None:
|
|
parser = configparser.ConfigParser()
|
|
parser.add_section("actions")
|
|
for action, keys in self.bindings.items():
|
|
parser.set("actions", action, ", ".join(keys))
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
with path.open("w") as handle:
|
|
parser.write(handle)
|