Mi2dRPGamEng/engine/config.py

109 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"],
}
# 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)