diff --git a/src/crimson/input_codes.py b/src/crimson/input_codes.py index 6e1ef373c..296f55bec 100644 --- a/src/crimson/input_codes.py +++ b/src/crimson/input_codes.py @@ -4,7 +4,6 @@ import msgspec -from grim.config import CrimsonConfig, default_player_keybind_block from grim.raylib_api import rl INPUT_CODE_UNBOUND = 0x17E @@ -234,7 +233,7 @@ def _axis_value_from_code(key_code: int, *, player_index: int) -> float: return 0.0 -def input_axis_value_for_player(key_code: int, *, player_index: int) -> float: +def input_axis_value(key_code: int, *, player_index: int = 0) -> float: return _axis_value_from_code(int(key_code), player_index=int(player_index)) @@ -372,20 +371,12 @@ def input_code_name(key_code: int) -> str: return f"KEY_{key_code:04X}" -def input_code_is_down(key_code: int) -> bool: - return input_code_is_down_for_player(int(key_code), player_index=0) - - -def input_code_is_pressed(key_code: int) -> bool: - return input_code_is_pressed_for_player(int(key_code), player_index=0) - - -def input_code_is_down_for_player(key_code: int, *, player_index: int) -> bool: +def input_code_is_down(key_code: int, *, player_index: int = 0) -> bool: down = _digital_down_for_player(int(key_code), player_index=int(player_index)) return _PRESSED_STATE.mark_down(player_index=int(player_index), key_code=int(key_code), is_down=down) -def input_code_is_pressed_for_player(key_code: int, *, player_index: int) -> bool: +def input_code_is_pressed(key_code: int, *, player_index: int = 0) -> bool: code = int(key_code) player_idx = int(player_index) if code == 0x109: @@ -440,69 +431,25 @@ def capture_first_pressed_input_code( value = float(rl.get_gamepad_axis_movement(gamepad, axis)) if abs(value) >= float(axis_threshold): return int(code) - return None -def _parse_keybinds_blob(blob: bytes | bytearray | None) -> tuple[int, ...]: - if blob is None: - return () - if not isinstance(blob, (bytes, bytearray)): - return () - if len(blob) != 0x80: - return () - out: list[int] = [] - for offset in range(0, 0x80, 4): - out.append(int.from_bytes(blob[offset : offset + 4], "little")) - return tuple(out) - - -def config_keybinds(config: CrimsonConfig | None) -> tuple[int, ...]: - if config is None: - return () - values: list[int] = [] - for player_index in range(2): - values.extend(int(value) for value in config.controls.player(player_index).keybinds) - return tuple(values) - - -def config_keybinds_for_player(config: CrimsonConfig | None, *, player_index: int) -> tuple[int, ...]: - if config is None: - return () - return tuple(int(value) for value in config.controls.player(player_index).keybinds) - - -def player_fire_keybind(config: CrimsonConfig | None, *, player_index: int) -> int: - idx = max(0, min(3, int(player_index))) - keybinds = config_keybinds_for_player(config, player_index=idx) - if len(keybinds) >= 5: - return int(keybinds[4]) - return int(default_player_keybind_block(idx)[4]) - - -def player_move_fire_keybinds(config: CrimsonConfig | None, *, player_index: int) -> tuple[int, int, int, int, int]: - idx = max(0, min(3, int(player_index))) - keybinds = config_keybinds_for_player(config, player_index=idx) - if len(keybinds) >= 5: - return player_move_fire_binds(keybinds, 0) - defaults = tuple(int(value) for value in default_player_keybind_block(idx)) - return int(defaults[0]), int(defaults[1]), int(defaults[2]), int(defaults[3]), int(defaults[4]) - - -def _input_primary_any_down(config: CrimsonConfig | None, *, player_count: int) -> bool: - if input_code_is_down_for_player(0x100, player_index=0): +def _input_primary_any_down(*, fire_codes: Sequence[int], player_count: int) -> bool: + if input_code_is_down(0x100, player_index=0): return True count = max(1, min(4, int(player_count))) + if len(fire_codes) < count: + raise ValueError(f"fire_codes must provide at least {count} entries, got {len(fire_codes)}") for player_index in range(count): - fire_key = player_fire_keybind(config, player_index=player_index) - if input_code_is_down_for_player(fire_key, player_index=player_index): + fire_key = int(fire_codes[player_index]) + if input_code_is_down(fire_key, player_index=player_index): return True return False -def input_primary_is_down(config: CrimsonConfig | None, *, player_count: int) -> bool: - down = _input_primary_any_down(config, player_count=player_count) +def input_primary_is_down(*, fire_codes: Sequence[int], player_count: int) -> bool: + down = _input_primary_any_down(fire_codes=fire_codes, player_count=player_count) _PRESSED_STATE.mark_down( player_index=_PRIMARY_EDGE_SENTINEL_PLAYER, key_code=_PRIMARY_EDGE_SENTINEL_KEY, @@ -511,26 +458,10 @@ def input_primary_is_down(config: CrimsonConfig | None, *, player_count: int) -> return bool(down) -def input_primary_just_pressed(config: CrimsonConfig | None, *, player_count: int) -> bool: - down = _input_primary_any_down(config, player_count=player_count) +def input_primary_just_pressed(*, fire_codes: Sequence[int], player_count: int) -> bool: + down = _input_primary_any_down(fire_codes=fire_codes, player_count=player_count) return _PRESSED_STATE.is_pressed( player_index=_PRIMARY_EDGE_SENTINEL_PLAYER, key_code=_PRIMARY_EDGE_SENTINEL_KEY, is_down=down, ) - - -def player_move_fire_binds(keybinds: Sequence[int], player_index: int) -> tuple[int, int, int, int, int]: - """Return (up, down, left, right, fire) key codes for a player. - - The classic config packs keybind blocks in 0x10-int strides; the first five entries - are used by `ui_render_keybind_help` (Up/Down/Left/Right/Fire). - """ - - base = int(player_index) * 0x10 - values = [INPUT_CODE_UNBOUND, INPUT_CODE_UNBOUND, INPUT_CODE_UNBOUND, INPUT_CODE_UNBOUND, INPUT_CODE_UNBOUND] - for idx in range(5): - src = base + idx - if 0 <= src < len(keybinds): - values[idx] = int(keybinds[src]) - return values[0], values[1], values[2], values[3], values[4] diff --git a/src/crimson/local_input.py b/src/crimson/local_input.py index 05e1439c9..d949a6965 100644 --- a/src/crimson/local_input.py +++ b/src/crimson/local_input.py @@ -6,20 +6,18 @@ import msgspec -from grim.config import CrimsonConfig, default_player_keybind_block +from grim.config import CrimsonConfig from grim.geom import Vec2 from grim.raylib_api import rl from .aim_constants import _AIM_JOYSTICK_TURN_RATE, _AIM_KEYBOARD_TURN_RATE from .aim_schemes import AimScheme from .input_codes import ( - config_keybinds_for_player, - input_axis_value_for_player, - input_code_is_down_for_player, - input_code_is_pressed_for_player, + input_axis_value, + input_code_is_down, + input_code_is_pressed, ) from .movement_controls import MovementControlType -from .screens.panels.controls_labels import controls_method_values from .sim.input import PlayerInput from .sim.state_types import PlayerState @@ -35,18 +33,6 @@ _COMPUTER_AIM_TRACK_GAIN = 6.0 _COMPUTER_AUTO_FIRE_DISTANCE = 128.0 -_MOVE_SLOT_UP = 0 -_MOVE_SLOT_DOWN = 1 -_MOVE_SLOT_LEFT = 2 -_MOVE_SLOT_RIGHT = 3 -_FIRE_SLOT = 4 -_AIM_LEFT_SLOT = 7 -_AIM_RIGHT_SLOT = 8 -_AIM_AXIS_Y_SLOT = 9 -_AIM_AXIS_X_SLOT = 10 -_MOVE_AXIS_Y_SLOT = 11 -_MOVE_AXIS_X_SLOT = 12 - _ALT_MOVE_KEY_UP = 0xC8 _ALT_MOVE_KEY_DOWN = 0xD0 _ALT_MOVE_KEY_LEFT = 0xCB @@ -119,19 +105,11 @@ def _resolve_static_move_vector( return move -def _load_player_bind_block(config: CrimsonConfig | None, *, player_index: int) -> tuple[int, ...]: - binds = config_keybinds_for_player(config, player_index=int(player_index)) - if len(binds) >= 16: - return tuple(int(v) for v in binds[:16]) - return tuple(int(v) for v in default_player_keybind_block(int(player_index))) - +def _config_player_count(config: CrimsonConfig) -> int: + return max(1, int(config.gameplay.player_count)) -def _config_player_count(config: CrimsonConfig | None) -> int: - value = config.gameplay.player_count if config is not None else 1 - return max(1, value) - -def _single_player_alt_keys_enabled(config: CrimsonConfig | None, *, player_index: int) -> bool: +def _single_player_alt_keys_enabled(config: CrimsonConfig, *, player_index: int) -> bool: return int(player_index) == 0 and _config_player_count(config) == 1 @@ -139,26 +117,26 @@ def _key_down_with_single_player_alt( primary_key: int, *, alt_key: int, - config: CrimsonConfig | None, + config: CrimsonConfig, player_index: int, ) -> bool: - if input_code_is_down_for_player(primary_key, player_index=int(player_index)): + if input_code_is_down(primary_key, player_index=int(player_index)): return True if _single_player_alt_keys_enabled(config, player_index=int(player_index)): - return input_code_is_down_for_player(int(alt_key), player_index=int(player_index)) + return input_code_is_down(int(alt_key), player_index=int(player_index)) return False def _aim_pov_left_active(*, player_index: int, preserve_bugs: bool) -> bool: # Native `input_aim_pov_left_active` always reads joystick POV index 0. pov_index = 0 if preserve_bugs else int(player_index) - return input_code_is_down_for_player(_AIM_POV_LEFT_CODE, player_index=pov_index) + return input_code_is_down(_AIM_POV_LEFT_CODE, player_index=pov_index) def _aim_pov_right_active(*, player_index: int, preserve_bugs: bool) -> bool: # Native `input_aim_pov_right_active` always reads joystick POV index 0. pov_index = 0 if preserve_bugs else int(player_index) - return input_code_is_down_for_player(_AIM_POV_RIGHT_CODE, player_index=pov_index) + return input_code_is_down(_AIM_POV_RIGHT_CODE, player_index=pov_index) def clear_input_edges(inputs: Sequence[PlayerInput]) -> list[PlayerInput]: @@ -267,25 +245,12 @@ def _state_for_player(self, player_index: int, *, player: PlayerState | None = N state.aim_heading = float(player.aim_heading) return state - @staticmethod - def _reload_key(config: CrimsonConfig | None) -> int: - if config is None: - return 0x102 - return config.controls.reload_key - - @staticmethod - def _safe_controls_modes(config: CrimsonConfig | None, *, player_index: int) -> tuple[AimScheme, MovementControlType]: - if config is None: - return AimScheme.MOUSE, MovementControlType.STATIC - aim_scheme, move_mode = controls_method_values(config.controls, player_index=int(player_index)) - return aim_scheme, move_mode - def build_player_input( self, *, player_index: int, player: PlayerState, - config: CrimsonConfig | None, + config: CrimsonConfig, mouse_screen: Vec2, mouse_world: Vec2, screen_center: Vec2, @@ -294,21 +259,16 @@ def build_player_input( ) -> PlayerInput: idx = max(0, min(3, int(player_index))) state = self._state_for_player(idx, player=player) - binds = _load_player_bind_block(config, player_index=idx) - aim_scheme, move_mode_type = self._safe_controls_modes(config, player_index=idx) - reload_key = self._reload_key(config) - - up_key = int(binds[_MOVE_SLOT_UP]) - down_key = int(binds[_MOVE_SLOT_DOWN]) - left_key = int(binds[_MOVE_SLOT_LEFT]) - right_key = int(binds[_MOVE_SLOT_RIGHT]) - fire_key = int(binds[_FIRE_SLOT]) - aim_left_key = int(binds[_AIM_LEFT_SLOT]) - aim_right_key = int(binds[_AIM_RIGHT_SLOT]) - aim_axis_y = int(binds[_AIM_AXIS_Y_SLOT]) - aim_axis_x = int(binds[_AIM_AXIS_X_SLOT]) - move_axis_y = int(binds[_MOVE_AXIS_Y_SLOT]) - move_axis_x = int(binds[_MOVE_AXIS_X_SLOT]) + binds = config.controls.player(idx) + aim_scheme = binds.aim_scheme + move_mode_type = binds.movement + reload_key = config.controls.reload_code + + move_forward_key, move_backward_key, turn_left_key, turn_right_key = binds.move_codes + fire_key = binds.fire_code + aim_left_key, aim_right_key = binds.keyboard_aim_codes + aim_axis_y, aim_axis_x = binds.aim_axis_codes + move_axis_y, move_axis_x = binds.move_axis_codes move_vec = Vec2() move_forward_pressed: bool | None = None @@ -349,25 +309,25 @@ def build_player_input( move_vec = move_dir elif move_mode_type is MovementControlType.RELATIVE: move_forward_pressed = _key_down_with_single_player_alt( - up_key, + move_forward_key, alt_key=_ALT_MOVE_KEY_UP, config=config, player_index=idx, ) move_backward_pressed = _key_down_with_single_player_alt( - down_key, + move_backward_key, alt_key=_ALT_MOVE_KEY_DOWN, config=config, player_index=idx, ) turn_left_pressed = _key_down_with_single_player_alt( - left_key, + turn_left_key, alt_key=_ALT_MOVE_KEY_LEFT, config=config, player_index=idx, ) turn_right_pressed = _key_down_with_single_player_alt( - right_key, + turn_right_key, alt_key=_ALT_MOVE_KEY_RIGHT, config=config, player_index=idx, @@ -377,11 +337,11 @@ def build_player_input( float(move_backward_pressed) - float(move_forward_pressed), ) elif move_mode_type is MovementControlType.DUAL_ACTION_PAD: - axis_y = -input_axis_value_for_player(move_axis_y, player_index=idx) - axis_x = -input_axis_value_for_player(move_axis_x, player_index=idx) + axis_y = -input_axis_value(move_axis_y, player_index=idx) + axis_x = -input_axis_value(move_axis_x, player_index=idx) move_vec = Vec2(_clamp_unit(axis_x), _clamp_unit(axis_y)) elif move_mode_type is MovementControlType.MOUSE_POINT_CLICK: - move_to_cursor_pressed = input_code_is_down_for_player(reload_key, player_index=idx) + move_to_cursor_pressed = input_code_is_down(reload_key, player_index=idx) if move_to_cursor_pressed: state.move_target = mouse_world if float(state.move_target.x) >= 0.0 and float(state.move_target.y) >= 0.0: @@ -391,25 +351,25 @@ def build_player_input( move_vec = _dir elif move_mode_type is MovementControlType.STATIC: move_up_pressed = _key_down_with_single_player_alt( - up_key, + move_forward_key, alt_key=_ALT_MOVE_KEY_UP, config=config, player_index=idx, ) move_down_pressed = _key_down_with_single_player_alt( - down_key, + move_backward_key, alt_key=_ALT_MOVE_KEY_DOWN, config=config, player_index=idx, ) move_left_pressed = _key_down_with_single_player_alt( - left_key, + turn_left_key, alt_key=_ALT_MOVE_KEY_LEFT, config=config, player_index=idx, ) move_right_pressed = _key_down_with_single_player_alt( - right_key, + turn_right_key, alt_key=_ALT_MOVE_KEY_RIGHT, config=config, player_index=idx, @@ -426,10 +386,10 @@ def build_player_input( ) else: move_vec = Vec2( - float(input_code_is_down_for_player(right_key, player_index=idx)) - - float(input_code_is_down_for_player(left_key, player_index=idx)), - float(input_code_is_down_for_player(down_key, player_index=idx)) - - float(input_code_is_down_for_player(up_key, player_index=idx)), + float(input_code_is_down(turn_right_key, player_index=idx)) + - float(input_code_is_down(turn_left_key, player_index=idx)), + float(input_code_is_down(move_backward_key, player_index=idx)) + - float(input_code_is_down(move_forward_key, player_index=idx)), ) heading = float(state.aim_heading) @@ -444,9 +404,9 @@ def build_player_input( heading = delta.to_heading() elif aim_scheme is AimScheme.KEYBOARD: if move_mode_type in {MovementControlType.RELATIVE, MovementControlType.STATIC}: - if input_code_is_down_for_player(aim_right_key, player_index=idx): + if input_code_is_down(aim_right_key, player_index=idx): heading = float(heading + float(dt) * _AIM_KEYBOARD_TURN_RATE) - if input_code_is_down_for_player(aim_left_key, player_index=idx): + if input_code_is_down(aim_left_key, player_index=idx): heading = float(heading - float(dt) * _AIM_KEYBOARD_TURN_RATE) aim = _aim_point_from_heading(player.pos, heading) elif aim_scheme is AimScheme.MOUSE_RELATIVE: @@ -455,8 +415,8 @@ def build_player_input( heading = rel.to_heading() aim = _aim_point_from_heading(player.pos, heading) elif aim_scheme is AimScheme.DUAL_ACTION_PAD: - axis_y = input_axis_value_for_player(aim_axis_y, player_index=idx) - axis_x = input_axis_value_for_player(aim_axis_x, player_index=idx) + axis_y = input_axis_value(aim_axis_y, player_index=idx) + axis_x = input_axis_value(aim_axis_x, player_index=idx) axis_vec = Vec2(axis_x, axis_y) mag_sq = axis_vec.length_sq() if mag_sq > 1e-9: @@ -505,12 +465,12 @@ def build_player_input( heading = delta.to_heading() state.aim_heading = float(heading) - fire_down = input_code_is_down_for_player(fire_key, player_index=idx) - fire_pressed = input_code_is_pressed_for_player(fire_key, player_index=idx) + fire_down = input_code_is_down(fire_key, player_index=idx) + fire_pressed = input_code_is_pressed(fire_key, player_index=idx) if aim_scheme is AimScheme.COMPUTER and computer_auto_fire: fire_down = True - reload_pressed = input_code_is_pressed_for_player(reload_key, player_index=idx) - reload_down = input_code_is_down_for_player(reload_key, player_index=idx) + reload_pressed = input_code_is_pressed(reload_key, player_index=idx) + reload_down = input_code_is_down(reload_key, player_index=idx) return PlayerInput( move=move_vec, @@ -532,7 +492,7 @@ def build_frame_inputs( self, *, players: Sequence[PlayerState], - config: CrimsonConfig | None, + config: CrimsonConfig, mouse_screen: Vec2, screen_to_world: Callable[[Vec2], Vec2], dt: float, diff --git a/src/crimson/modes/components/perk_prompt_controller.py b/src/crimson/modes/components/perk_prompt_controller.py index f3de297ba..4f9687551 100644 --- a/src/crimson/modes/components/perk_prompt_controller.py +++ b/src/crimson/modes/components/perk_prompt_controller.py @@ -7,10 +7,9 @@ from grim.math import clamp from ...input_codes import ( - input_code_is_down_for_player, - input_code_is_pressed_for_player, + input_code_is_down, + input_code_is_pressed, input_primary_just_pressed, - player_fire_keybind, ) from .perk_menu_controller import PerkMenuUiContext from .perk_prompt_ui import PERK_PROMPT_MAX_TIMER_MS, PerkPromptUi @@ -106,10 +105,11 @@ def draw( ) def _prompt_open_requested(self, *, config: CrimsonConfig, player_count: int) -> bool: - fire_key = player_fire_keybind(config, player_index=0) - pick_key = config.controls.pick_perk_key - if input_code_is_pressed_for_player(pick_key, player_index=0) and ( - not input_code_is_down_for_player(fire_key, player_index=0) + fire_key = int(config.controls.player(0).fire_code) + pick_key = config.controls.pick_perk_code + if input_code_is_pressed(pick_key, player_index=0) and ( + not input_code_is_down(fire_key, player_index=0) ): return True - return self.hover and input_primary_just_pressed(config, player_count=player_count) + fire_codes = tuple(int(config.controls.player(idx).fire_code) for idx in range(4)) + return self.hover and input_primary_just_pressed(fire_codes=fire_codes, player_count=player_count) diff --git a/src/crimson/modes/tutorial_mode.py b/src/crimson/modes/tutorial_mode.py index f5b4638b0..da415c76a 100644 --- a/src/crimson/modes/tutorial_mode.py +++ b/src/crimson/modes/tutorial_mode.py @@ -11,7 +11,7 @@ from grim.view import ViewContext from ..game_modes import GameMode -from ..input_codes import input_code_is_down, input_code_is_pressed, player_move_fire_keybinds +from ..input_codes import input_code_is_down, input_code_is_pressed from ..perks.selection import perk_selection_prepared_choices from ..replay import ReplayHeader, ReplayRecorder, ReplayStatusSnapshot from ..replay.checkpoints import DEFAULT_CHECKPOINT_SAMPLE_RATE @@ -185,14 +185,13 @@ def _handle_input(self) -> None: return def _build_input(self) -> PlayerInput: - up_key, down_key, left_key, right_key, fire_key = player_move_fire_keybinds( - self.config, - player_index=0, - ) + controls = self.config.controls.player(0) + move_forward_key, move_backward_key, turn_left_key, turn_right_key = controls.move_codes + fire_key = controls.fire_code move = Vec2( - float(input_code_is_down(right_key)) - float(input_code_is_down(left_key)), - float(input_code_is_down(down_key)) - float(input_code_is_down(up_key)), + float(input_code_is_down(turn_right_key)) - float(input_code_is_down(turn_left_key)), + float(input_code_is_down(move_backward_key)) - float(input_code_is_down(move_forward_key)), ) mouse = self._ui_mouse_pos() @@ -200,7 +199,7 @@ def _build_input(self) -> PlayerInput: fire_down = input_code_is_down(fire_key) fire_pressed = input_code_is_pressed(fire_key) - reload_key = self.config.controls.reload_key + reload_key = self.config.controls.reload_code reload_pressed = input_code_is_pressed(reload_key) return PlayerInput( diff --git a/src/crimson/screens/panels/controls.py b/src/crimson/screens/panels/controls.py index edc78e047..59dc8c254 100644 --- a/src/crimson/screens/panels/controls.py +++ b/src/crimson/screens/panels/controls.py @@ -4,8 +4,7 @@ from grim.assets import RuntimeResources, TextureId from grim.config import ( - KEYBIND_UNBOUND_CODE, - default_player_keybind_block, + default_crimson_cfg, ) from grim.fonts.small import SmallFontData, draw_small_text, measure_small_text_width from grim.geom import Rect, Vec2 @@ -25,11 +24,10 @@ ) from .base import PANEL_TIMELINE_END_MS, PANEL_TIMELINE_START_MS, PanelMenuView from .controls_labels import ( - PICK_PERK_BIND_SLOT, - RELOAD_BIND_SLOT, + RebindRowSpec, + RebindTarget, controls_aim_method_dropdown_ids, - controls_method_values, - controls_rebind_slot_plan, + controls_rebind_plan, input_configure_for_label, input_scheme_label, ) @@ -49,7 +47,65 @@ CONTROLS_REBIND_HOVER_COLOR = rl.Color(200, 230, 250, 230) CONTROLS_REBIND_ACTIVE_COLOR = rl.Color(255, 228, 170, 255) -_AXIS_REBIND_SLOTS = frozenset((9, 10, 11, 12)) + +def _row_binding_code(row: RebindRowSpec, *, player_index: int, controls) -> int: + player_controls = controls.player(player_index) + match row.target: + case RebindTarget.PLAYER_MOVE_CODES: + assert row.target_index is not None + return int(player_controls.move_codes[row.target_index]) + case RebindTarget.PLAYER_FIRE_CODE: + return int(player_controls.fire_code) + case RebindTarget.PLAYER_KEYBOARD_AIM_CODES: + assert row.target_index is not None + return int(player_controls.keyboard_aim_codes[row.target_index]) + case RebindTarget.PLAYER_AIM_AXIS_CODES: + assert row.target_index is not None + return int(player_controls.aim_axis_codes[row.target_index]) + case RebindTarget.PLAYER_MOVE_AXIS_CODES: + assert row.target_index is not None + return int(player_controls.move_axis_codes[row.target_index]) + case RebindTarget.GLOBAL_PICK_PERK_CODE: + return int(controls.pick_perk_code) + case RebindTarget.GLOBAL_RELOAD_CODE: + return int(controls.reload_code) + + +def _set_row_binding_code(row: RebindRowSpec, value: int, *, player_index: int, controls) -> None: + player_controls = controls.player(player_index) + code = int(value) + match row.target: + case RebindTarget.PLAYER_MOVE_CODES: + assert row.target_index is not None + values = list(player_controls.move_codes) + values[row.target_index] = code + player_controls.move_codes = tuple(values) + case RebindTarget.PLAYER_FIRE_CODE: + player_controls.fire_code = code + case RebindTarget.PLAYER_KEYBOARD_AIM_CODES: + assert row.target_index is not None + values = list(player_controls.keyboard_aim_codes) + values[row.target_index] = code + player_controls.keyboard_aim_codes = tuple(values) + case RebindTarget.PLAYER_AIM_AXIS_CODES: + assert row.target_index is not None + values = list(player_controls.aim_axis_codes) + values[row.target_index] = code + player_controls.aim_axis_codes = tuple(values) + case RebindTarget.PLAYER_MOVE_AXIS_CODES: + assert row.target_index is not None + values = list(player_controls.move_axis_codes) + values[row.target_index] = code + player_controls.move_axis_codes = tuple(values) + case RebindTarget.GLOBAL_PICK_PERK_CODE: + controls.pick_perk_code = code + case RebindTarget.GLOBAL_RELOAD_CODE: + controls.reload_code = code + + +def _default_row_binding_code(player_index: int, row: RebindRowSpec) -> int: + controls = default_crimson_cfg().controls + return _row_binding_code(row, player_index=player_index, controls=controls) def _controls_left_panel_pos_x(screen_width: float) -> float: @@ -105,8 +161,7 @@ class _ControlsDropdownLayout(DropdownLayoutBase, frozen=True): class _RebindRowLayout(msgspec.Struct, frozen=True): - label: str - slot: int + row: RebindRowSpec row_y: float value_pos: Vec2 value_rect: Rect @@ -126,7 +181,7 @@ def __init__(self, state: GameState) -> None: self._aim_method_open = False self._player_profile_open = False self._dirty = False - self._rebind_slot: int | None = None + self._rebind_row: RebindRowSpec | None = None self._rebind_player_index: int | None = None self._rebind_skip_frames = 0 @@ -185,15 +240,15 @@ def _current_player_index(self) -> int: return max(0, min(3, int(self._config_player) - 1)) def _rebind_active(self) -> bool: - return self._rebind_slot is not None and self._rebind_player_index is not None + return self._rebind_row is not None and self._rebind_player_index is not None def _clear_rebind_capture(self) -> None: - self._rebind_slot = None + self._rebind_row = None self._rebind_player_index = None self._rebind_skip_frames = 0 - def _start_rebind_capture(self, *, slot: int, player_index: int) -> None: - self._rebind_slot = int(slot) + def _start_rebind_capture(self, *, row: RebindRowSpec, player_index: int) -> None: + self._rebind_row = row self._rebind_player_index = max(0, min(3, int(player_index))) self._move_method_open = False self._aim_method_open = False @@ -202,44 +257,19 @@ def _start_rebind_capture(self, *, slot: int, player_index: int) -> None: self._rebind_skip_frames = 1 @staticmethod - def _slot_is_axis(slot: int) -> bool: - return int(slot) in _AXIS_REBIND_SLOTS - - @staticmethod - def _capture_prompt_for_slot(slot: int) -> str: - if ControlsMenuView._slot_is_axis(int(slot)): + def _capture_prompt_for_binding(row: RebindRowSpec) -> str: + if row.axis: return "" return "" - def _slot_default_key(self, *, player_index: int, slot: int) -> int: - slot_idx = int(slot) - if slot_idx == PICK_PERK_BIND_SLOT: - return 0x101 - if slot_idx == RELOAD_BIND_SLOT: - return 0x102 - defaults = default_player_keybind_block(int(player_index)) - if 0 <= slot_idx < len(defaults): - return int(defaults[slot_idx]) - return int(KEYBIND_UNBOUND_CODE) - - def _slot_key(self, *, player_index: int, slot: int) -> int: - slot_idx = int(slot) - if slot_idx == PICK_PERK_BIND_SLOT: - return self.state.config.controls.pick_perk_key - if slot_idx == RELOAD_BIND_SLOT: - return self.state.config.controls.reload_key - return self.state.config.controls.player(player_index).keybind(slot_idx) - - def _set_slot_key(self, *, player_index: int, slot: int, code: int) -> None: - slot_idx = int(slot) - value = int(code) - if slot_idx == PICK_PERK_BIND_SLOT: - self.state.config.controls.pick_perk_key = value - return - if slot_idx == RELOAD_BIND_SLOT: - self.state.config.controls.reload_key = value - return - self.state.config.controls.player(player_index).set_keybind(slot_idx, value) + def _binding_default_code(self, *, player_index: int, row: RebindRowSpec) -> int: + return _default_row_binding_code(player_index, row) + + def _binding_code(self, *, player_index: int, row: RebindRowSpec) -> int: + return _row_binding_code(row, player_index=player_index, controls=self.state.config.controls) + + def _set_binding_code(self, *, player_index: int, row: RebindRowSpec, code: int) -> None: + _set_row_binding_code(row, int(code), player_index=player_index, controls=self.state.config.controls) def _left_panel_top_left(self, panel_scale: float) -> Vec2: panel_w = MENU_PANEL_WIDTH * panel_scale @@ -335,13 +365,13 @@ def _rebind_sections( player_index: int, aim_scheme: AimScheme, move_mode: MovementControlType, - ) -> tuple[tuple[str, tuple[tuple[str, int], ...]], ...]: - aim_rows, move_rows, misc_rows = controls_rebind_slot_plan( + ) -> tuple[tuple[str, tuple[RebindRowSpec, ...]], ...]: + aim_rows, move_rows, misc_rows = controls_rebind_plan( aim_scheme=aim_scheme, move_mode=move_mode, player_index=player_index, ) - sections: list[tuple[str, tuple[tuple[str, int], ...]]] = [("Aiming", aim_rows), ("Moving", move_rows)] + sections: list[tuple[str, tuple[RebindRowSpec, ...]]] = [("Aiming", aim_rows), ("Moving", move_rows)] if misc_rows: sections.append(("Misc", misc_rows)) return tuple(sections) @@ -352,15 +382,15 @@ def _collect_rebind_rows( right_top_left: Vec2, panel_scale: float, player_index: int, - sections: tuple[tuple[str, tuple[tuple[str, int], ...]], ...], + sections: tuple[tuple[str, tuple[RebindRowSpec, ...]], ...], font: SmallFontData, ) -> tuple[_RebindRowLayout, ...]: rows: list[_RebindRowLayout] = [] y = right_top_left.y + 64.0 * panel_scale for _section_title, section_rows in sections: row_y = y + 18.0 * panel_scale - for label, slot in section_rows: - key_code = int(self._slot_key(player_index=player_index, slot=slot)) + for row in section_rows: + key_code = int(self._binding_code(player_index=player_index, row=row)) value_text = input_code_name(key_code) value_pos = Vec2(right_top_left.x + 180.0 * panel_scale, row_y) value_w = max(60.0 * panel_scale, measure_small_text_width(font, value_text)) @@ -371,8 +401,7 @@ def _collect_rebind_rows( ) rows.append( _RebindRowLayout( - label=str(label), - slot=int(slot), + row=row, row_y=float(row_y), value_pos=value_pos, value_rect=value_rect, @@ -384,7 +413,9 @@ def _collect_rebind_rows( def _update_rebind_capture(self, *, right_top_left: Vec2, panel_scale: float, font: SmallFontData) -> bool: player_idx = self._current_player_index() - aim_scheme, move_mode = controls_method_values(self.state.config.controls, player_index=player_idx) + player_controls = self.state.config.controls.player(player_idx) + aim_scheme = player_controls.aim_scheme + move_mode = player_controls.movement sections = self._rebind_sections(player_index=player_idx, aim_scheme=aim_scheme, move_mode=move_mode) rows = self._collect_rebind_rows( right_top_left=right_top_left, @@ -395,7 +426,7 @@ def _update_rebind_capture(self, *, right_top_left: Vec2, panel_scale: float, fo ) if self._rebind_active(): - active_slot = int(self._rebind_slot or 0) + active_row = self._rebind_row or RebindRowSpec("Fire:", RebindTarget.PLAYER_FIRE_CODE) active_player = int(self._rebind_player_index or 0) if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE) or rl.is_mouse_button_pressed( rl.MouseButton.MOUSE_BUTTON_RIGHT, @@ -404,17 +435,17 @@ def _update_rebind_capture(self, *, right_top_left: Vec2, panel_scale: float, fo return True if rl.is_key_pressed(rl.KeyboardKey.KEY_BACKSPACE): - self._set_slot_key( + self._set_binding_code( player_index=active_player, - slot=active_slot, - code=self._slot_default_key(player_index=active_player, slot=active_slot), + row=active_row, + code=self._binding_default_code(player_index=active_player, row=active_row), ) self._dirty = True self._clear_rebind_capture() return True if rl.is_key_pressed(rl.KeyboardKey.KEY_DELETE): - self._set_slot_key(player_index=active_player, slot=active_slot, code=INPUT_CODE_UNBOUND) + self._set_binding_code(player_index=active_player, row=active_row, code=INPUT_CODE_UNBOUND) self._dirty = True self._clear_rebind_capture() return True @@ -423,7 +454,7 @@ def _update_rebind_capture(self, *, right_top_left: Vec2, panel_scale: float, fo self._rebind_skip_frames = max(0, int(self._rebind_skip_frames) - 1) return True - axis_only = self._slot_is_axis(active_slot) + axis_only = active_row.axis captured = capture_first_pressed_input_code( player_index=active_player, include_keyboard=not axis_only, @@ -433,7 +464,7 @@ def _update_rebind_capture(self, *, right_top_left: Vec2, panel_scale: float, fo axis_threshold=0.5, ) if captured is not None: - self._set_slot_key(player_index=active_player, slot=active_slot, code=int(captured)) + self._set_binding_code(player_index=active_player, row=active_row, code=int(captured)) self._dirty = True self._clear_rebind_capture() return True @@ -446,7 +477,7 @@ def _update_rebind_capture(self, *, right_top_left: Vec2, panel_scale: float, fo mouse = Vec2.from_xy(rl.get_mouse_position()) for row in rows: if row.value_rect.contains(mouse): - self._start_rebind_capture(slot=row.slot, player_index=player_idx) + self._start_rebind_capture(row=row.row, player_index=player_idx) return True return False @@ -539,7 +570,9 @@ def _update_dropdown( def _update_method_dropdowns(self, *, left_top_left: Vec2, panel_scale: float, font: SmallFontData) -> bool: config = self.state.config player_idx = self._current_player_index() - aim_scheme, move_mode = controls_method_values(config.controls, player_index=player_idx) + player_controls = config.controls.player(player_idx) + aim_scheme = player_controls.aim_scheme + move_mode = player_controls.movement move_mode_ids = self._move_method_ids(move_mode=move_mode) move_items = tuple(input_scheme_label(mode) for mode in move_mode_ids) aim_item_ids = controls_aim_method_dropdown_ids(aim_scheme) @@ -654,7 +687,9 @@ def _draw_contents(self) -> None: text_color_soft = rl.Color(255, 255, 255, 204) config = self.state.config player_idx = self._current_player_index() - aim_scheme, move_mode = controls_method_values(config.controls, player_index=player_idx) + player_controls = config.controls.player(player_idx) + aim_scheme = player_controls.aim_scheme + move_mode = player_controls.movement move_mode_ids = self._move_method_ids(move_mode=move_mode) move_items = tuple(input_scheme_label(mode) for mode in move_mode_ids) aim_item_ids = controls_aim_method_dropdown_ids(aim_scheme) @@ -830,20 +865,23 @@ def _draw_section_heading(title: str, *, y: float) -> None: row_y = y + 18.0 * panel_scale for _ in section_rows: row = next(row_iter) - label = row.label - slot = int(row.slot) - active_row = rebind_active and int(self._rebind_slot or -1) == slot and int( + active_row = rebind_active and self._rebind_row == row.row and int( self._rebind_player_index or -1, ) == player_idx hovered_row = (not rebind_active) and (not dropdown_blocked) and row.value_rect.contains(mouse) value_text = ( - self._capture_prompt_for_slot(slot) + self._capture_prompt_for_binding(row.row) if active_row - else input_code_name(self._slot_key(player_index=player_idx, slot=slot)) + else input_code_name(self._binding_code(player_index=player_idx, row=row.row)) ) value_pos = row.value_pos - draw_small_text(font, label, Vec2(right_top_left.x + 52.0 * panel_scale, row_y), rl.Color(255, 255, 255, 178)) + draw_small_text( + font, + row.row.label, + Vec2(right_top_left.x + 52.0 * panel_scale, row_y), + rl.Color(255, 255, 255, 178), + ) value_color = CONTROLS_REBIND_VALUE_COLOR if hovered_row: value_color = CONTROLS_REBIND_HOVER_COLOR diff --git a/src/crimson/screens/panels/controls_labels.py b/src/crimson/screens/panels/controls_labels.py index d68ecaa68..fef722fcd 100644 --- a/src/crimson/screens/panels/controls_labels.py +++ b/src/crimson/screens/panels/controls_labels.py @@ -1,12 +1,30 @@ from __future__ import annotations +from dataclasses import dataclass +from enum import Enum, auto + from grim.config import CrimsonControlsConfig from ...aim_schemes import AimScheme from ...movement_controls import MovementControlType -PICK_PERK_BIND_SLOT = -1 -RELOAD_BIND_SLOT = -2 + +class RebindTarget(Enum): + PLAYER_MOVE_CODES = auto() + PLAYER_FIRE_CODE = auto() + PLAYER_KEYBOARD_AIM_CODES = auto() + PLAYER_AIM_AXIS_CODES = auto() + PLAYER_MOVE_AXIS_CODES = auto() + GLOBAL_PICK_PERK_CODE = auto() + GLOBAL_RELOAD_CODE = auto() + + +@dataclass(frozen=True, slots=True) +class RebindRowSpec: + label: str + target: RebindTarget + target_index: int | None = None + axis: bool = False def input_configure_for_label(config_id: AimScheme) -> str: @@ -37,17 +55,10 @@ def input_scheme_label(scheme: MovementControlType) -> str: return labels.get(scheme, "Unknown") -def controls_method_values( - controls: CrimsonControlsConfig, - *, - player_index: int, -) -> tuple[AimScheme, MovementControlType]: - player = controls.player(player_index) - return player.aim_scheme, player.movement - - def controls_method_labels(controls: CrimsonControlsConfig, *, player_index: int) -> tuple[str, str]: - aim_scheme, move_mode = controls_method_values(controls, player_index=player_index) + player = controls.player(player_index) + aim_scheme = player.aim_scheme + move_mode = player.movement return input_configure_for_label(aim_scheme), input_scheme_label(move_mode) @@ -65,57 +76,61 @@ def controls_aim_method_dropdown_ids(current_aim_scheme: AimScheme) -> tuple[Aim return tuple(ids) -def controls_rebind_slot_plan( +def controls_rebind_plan( *, aim_scheme: AimScheme, move_mode: MovementControlType, player_index: int, -) -> tuple[tuple[tuple[str, int], ...], tuple[tuple[str, int], ...], tuple[tuple[str, int], ...]]: +) -> tuple[ + tuple[RebindRowSpec, ...], + tuple[RebindRowSpec, ...], + tuple[RebindRowSpec, ...], +]: """Return (aim_rows, move_rows, misc_rows) for `controls_menu_update`.""" - aim_rows: list[tuple[str, int]] = [] - move_rows: list[tuple[str, int]] = [] - misc_rows: list[tuple[str, int]] = [] + aim_rows: list[RebindRowSpec] = [] + move_rows: list[RebindRowSpec] = [] + misc_rows: list[RebindRowSpec] = [] if aim_scheme is AimScheme.KEYBOARD: - aim_rows.append(("Torso left:", 7)) - aim_rows.append(("Torso right:", 8)) + aim_rows.append(RebindRowSpec("Torso left:", RebindTarget.PLAYER_KEYBOARD_AIM_CODES, 0)) + aim_rows.append(RebindRowSpec("Torso right:", RebindTarget.PLAYER_KEYBOARD_AIM_CODES, 1)) elif aim_scheme is AimScheme.DUAL_ACTION_PAD: - aim_rows.append(("Aim Up/Down Axis:", 9)) - aim_rows.append(("Aim Left/Right Axis:", 10)) - aim_rows.append(("Fire:", 4)) + aim_rows.append(RebindRowSpec("Aim Up/Down Axis:", RebindTarget.PLAYER_AIM_AXIS_CODES, 0, axis=True)) + aim_rows.append(RebindRowSpec("Aim Left/Right Axis:", RebindTarget.PLAYER_AIM_AXIS_CODES, 1, axis=True)) + aim_rows.append(RebindRowSpec("Fire:", RebindTarget.PLAYER_FIRE_CODE)) if move_mode is MovementControlType.STATIC: move_rows.extend( ( - ("Move Up:", 0), - ("Move Down:", 1), - ("Move Left:", 2), - ("Move Right:", 3), + RebindRowSpec("Move Up:", RebindTarget.PLAYER_MOVE_CODES, 0), + RebindRowSpec("Move Down:", RebindTarget.PLAYER_MOVE_CODES, 1), + RebindRowSpec("Move Left:", RebindTarget.PLAYER_MOVE_CODES, 2), + RebindRowSpec("Move Right:", RebindTarget.PLAYER_MOVE_CODES, 3), ), ) elif move_mode is MovementControlType.RELATIVE: move_rows.extend( ( - ("Forward:", 0), - ("Backwards:", 1), - ("Turn left:", 2), - ("Turn right:", 3), + RebindRowSpec("Forward:", RebindTarget.PLAYER_MOVE_CODES, 0), + RebindRowSpec("Backwards:", RebindTarget.PLAYER_MOVE_CODES, 1), + RebindRowSpec("Turn left:", RebindTarget.PLAYER_MOVE_CODES, 2), + RebindRowSpec("Turn right:", RebindTarget.PLAYER_MOVE_CODES, 3), ), ) elif move_mode is MovementControlType.DUAL_ACTION_PAD: move_rows.extend( ( - ("Up/Down Axis:", 11), - ("Left/Right Axis:", 12), + RebindRowSpec("Up/Down Axis:", RebindTarget.PLAYER_MOVE_AXIS_CODES, 0, axis=True), + RebindRowSpec("Left/Right Axis:", RebindTarget.PLAYER_MOVE_AXIS_CODES, 1, axis=True), ), ) elif move_mode is MovementControlType.MOUSE_POINT_CLICK: - move_rows.append(("Move to cursor:", RELOAD_BIND_SLOT)) + move_rows.append(RebindRowSpec("Move to cursor:", RebindTarget.GLOBAL_RELOAD_CODE)) if int(player_index) == 0: - misc_rows.append(("Level Up:", PICK_PERK_BIND_SLOT)) + misc_rows.append(RebindRowSpec("Level Up:", RebindTarget.GLOBAL_PICK_PERK_CODE)) if move_mode is not MovementControlType.MOUSE_POINT_CLICK: - misc_rows.append(("Reload:", RELOAD_BIND_SLOT)) + misc_rows.append(RebindRowSpec("Reload:", RebindTarget.GLOBAL_RELOAD_CODE)) return tuple(aim_rows), tuple(move_rows), tuple(misc_rows) diff --git a/src/crimson/ui/text_input.py b/src/crimson/ui/text_input.py index 5109c87ef..4be9c5c66 100644 --- a/src/crimson/ui/text_input.py +++ b/src/crimson/ui/text_input.py @@ -7,7 +7,7 @@ from grim.raylib_api import rl from grim.sfx_map import SfxId -from ..input_codes import INPUT_CODE_UNBOUND, config_keybinds_for_player, input_code_is_down_for_player +from ..input_codes import INPUT_CODE_UNBOUND, input_code_is_down from ..rng_caller_static import RngCallerStatic _CONTROL_BIND_SLOTS = 5 @@ -79,15 +79,21 @@ def update_name_entry_text( def gameplay_controls_held(config: CrimsonConfig) -> bool: player_count = max(1, min(4, config.gameplay.player_count)) for player_index in range(player_count): - binds = config_keybinds_for_player(config, player_index=player_index) - for code in binds[:_CONTROL_BIND_SLOTS]: - key_code = int(code) - if key_code == INPUT_CODE_UNBOUND: + player_controls = config.controls.player(player_index) + move_forward_key, move_backward_key, turn_left_key, turn_right_key = player_controls.move_codes + for code in ( + move_forward_key, + move_backward_key, + turn_left_key, + turn_right_key, + player_controls.fire_code, + )[:_CONTROL_BIND_SLOTS]: + if code == INPUT_CODE_UNBOUND: continue - if input_code_is_down_for_player(key_code, player_index=player_index): + if input_code_is_down(code, player_index=player_index): return True for code in _SINGLE_PLAYER_ALT_MOVE_CODES: - if input_code_is_down_for_player(int(code), player_index=0): + if input_code_is_down(int(code), player_index=0): return True return False diff --git a/src/grim/config.py b/src/grim/config.py index e070e3d98..63f4b111b 100644 --- a/src/grim/config.py +++ b/src/grim/config.py @@ -3,7 +3,7 @@ from collections.abc import Sequence from enum import IntEnum from pathlib import Path -from typing import TypeAlias, cast +from typing import Any import msgspec from construct import Array, Byte, Bytes, Float32l, Int32sl, Struct @@ -20,38 +20,35 @@ SAVED_NAME_SLOT_COUNT = 8 SAVED_NAME_ENTRY_SIZE = 0x1B SAVED_NAMES_BLOB_SIZE = SAVED_NAME_SLOT_COUNT * SAVED_NAME_ENTRY_SIZE -KEYBINDS_BLOB_SIZE = 0x80 UNKNOWN_248_SIZE = 0x1F8 PLAYER_BIND_BLOCK_DWORDS = 0x10 PLAYER_BIND_BLOCK_SIZE = PLAYER_BIND_BLOCK_DWORDS * 4 -PLAYER_BIND_INPUT_DWORDS = 0x0D EXT_DIRECTION_ARROW_FLAG_COUNT = 2 EXTENDED_RESERVED_GAP_SIZE = UNKNOWN_248_SIZE - 2 * PLAYER_BIND_BLOCK_SIZE - EXT_DIRECTION_ARROW_FLAG_COUNT EXT_DIRECTION_ARROW_UNSET = 0 EXT_DIRECTION_ARROW_OFF = 1 EXT_DIRECTION_ARROW_ON = 2 KEYBIND_UNBOUND_CODE = 0x17E - -PlayerKeybindBlock: TypeAlias = tuple[ - int, - int, - int, - int, - int, - int, - int, - int, - int, - int, - int, - int, - int, - int, - int, - int, -] - -PLAYER_BIND_BLOCK_STRUCT = Array(PLAYER_BIND_BLOCK_DWORDS, Int32sl) +RESERVED_KEYBIND_SLOT_COUNT = 2 +PADDING_KEYBIND_SLOT_COUNT = 3 +_DEFAULT_WIRE_RESERVED_KEYS = (KEYBIND_UNBOUND_CODE, KEYBIND_UNBOUND_CODE) +_DEFAULT_WIRE_PADDING = (KEYBIND_UNBOUND_CODE, KEYBIND_UNBOUND_CODE, KEYBIND_UNBOUND_CODE) + +PLAYER_BIND_BLOCK_STRUCT = Struct( + "move_forward" / Int32sl, + "move_backward" / Int32sl, + "turn_left" / Int32sl, + "turn_right" / Int32sl, + "fire" / Int32sl, + "reserved_keys" / Array(RESERVED_KEYBIND_SLOT_COUNT, Int32sl), + "aim_left" / Int32sl, + "aim_right" / Int32sl, + "axis_aim_y" / Int32sl, + "axis_aim_x" / Int32sl, + "axis_move_y" / Int32sl, + "axis_move_x" / Int32sl, + "padding" / Array(PADDING_KEYBIND_SLOT_COUNT, Int32sl), +) CRIMSON_CFG_STRUCT = Struct( "sound_disable" / Byte, @@ -121,81 +118,6 @@ "keybind_reload" / Int32sl, ) -_DEFAULT_PLAYER_BIND_BLOCKS: tuple[tuple[int, ...], ...] = ( - ( - 0x11, - 0x1F, - 0x1E, - 0x20, - 0x100, - 0x17E, - 0x17E, - 0x10, - 0x12, - 0x13F, - 0x140, - 0x141, - 0x153, - 0x17E, - 0x17E, - 0x17E, - ), - ( - 0xC8, - 0xD0, - 0xCB, - 0xCD, - 0x9D, - 0x17E, - 0x17E, - 0xD3, - 0xD1, - 0x13F, - 0x140, - 0x141, - 0x153, - 0x17E, - 0x17E, - 0x17E, - ), - ( - 0x17, - 0x25, - 0x24, - 0x26, - 0x36, - 0x17E, - 0x17E, - 0x16, - 0x18, - 0x17E, - 0x17E, - 0x17E, - 0x17E, - 0x17E, - 0x17E, - 0x17E, - ), - ( - 0x131, - 0x132, - 0x133, - 0x134, - 0x11F, - 0x17E, - 0x17E, - 0x17E, - 0x17E, - 0x140, - 0x13F, - 0x153, - 0x154, - 0x17E, - 0x17E, - 0x17E, - ), -) - _DEFAULT_PROFILE_NAME = "10tons" _DEFAULT_SAVED_NAMES: tuple[str, str, str, str, str, str, str, str] = ( "default", @@ -293,23 +215,62 @@ def saved_name_labels(self) -> tuple[str, ...]: class CrimsonPlayerControls(msgspec.Struct): movement: MovementControlType aim_scheme: AimScheme - keybinds: PlayerKeybindBlock show_direction_arrow: bool + move_codes: tuple[int, int, int, int] + fire_code: int + keyboard_aim_codes: tuple[int, int] + aim_axis_codes: tuple[int, int] + move_axis_codes: tuple[int, int] - def keybind(self, slot_index: int) -> int: - return int(self.keybinds[_slot_index(slot_index)]) - def set_keybind(self, slot_index: int, value: int) -> None: - slot = _slot_index(slot_index) - block = list(self.keybinds) - block[slot] = int(value) - self.keybinds = _player_keybind_block(block) +_DEFAULT_PLAYER_CONTROL_TEMPLATES: tuple[CrimsonPlayerControls, ...] = ( + CrimsonPlayerControls( + movement=MovementControlType.STATIC, + aim_scheme=AimScheme.MOUSE, + show_direction_arrow=True, + move_codes=(0x11, 0x1F, 0x1E, 0x20), + fire_code=0x100, + keyboard_aim_codes=(0x10, 0x12), + aim_axis_codes=(0x13F, 0x140), + move_axis_codes=(0x141, 0x153), + ), + CrimsonPlayerControls( + movement=MovementControlType.STATIC, + aim_scheme=AimScheme.MOUSE, + show_direction_arrow=True, + move_codes=(0xC8, 0xD0, 0xCB, 0xCD), + fire_code=0x9D, + keyboard_aim_codes=(0xD3, 0xD1), + aim_axis_codes=(0x13F, 0x140), + move_axis_codes=(0x141, 0x153), + ), + CrimsonPlayerControls( + movement=MovementControlType.STATIC, + aim_scheme=AimScheme.MOUSE, + show_direction_arrow=True, + move_codes=(0x17, 0x25, 0x24, 0x26), + fire_code=0x36, + keyboard_aim_codes=(0x16, 0x18), + aim_axis_codes=(0x17E, 0x17E), + move_axis_codes=(0x17E, 0x17E), + ), + CrimsonPlayerControls( + movement=MovementControlType.STATIC, + aim_scheme=AimScheme.MOUSE, + show_direction_arrow=True, + move_codes=(0x131, 0x132, 0x133, 0x134), + fire_code=0x11F, + keyboard_aim_codes=(0x17E, 0x17E), + aim_axis_codes=(0x140, 0x13F), + move_axis_codes=(0x153, 0x154), + ), +) class CrimsonControlsConfig(msgspec.Struct): players: tuple[CrimsonPlayerControls, CrimsonPlayerControls, CrimsonPlayerControls, CrimsonPlayerControls] - pick_perk_key: int - reload_key: int + pick_perk_code: int + reload_code: int def player(self, player_index: int) -> CrimsonPlayerControls: return self.players[_player_index(player_index)] @@ -334,49 +295,100 @@ def _player_index(player_index: int) -> int: return idx -def _slot_index(slot_index: int) -> int: - idx = int(slot_index) - if idx < 0 or idx >= PLAYER_BIND_BLOCK_DWORDS: - raise IndexError(f"keybind slot must be in 0..{PLAYER_BIND_BLOCK_DWORDS - 1}, got {idx}") - return idx - - def _require_range(value: int, *, minimum: int, maximum: int, field: str) -> int: if value < minimum or value > maximum: raise ValueError(f"{field} must be in {minimum}..{maximum}, got {value}") return value -def _block_uninitialized(values: Sequence[int]) -> bool: - for idx in range(min(len(values), PLAYER_BIND_INPUT_DWORDS)): - if int(values[idx]) != 0: - return False - return True +def _parsed_player_bind_block(raw: dict[str, Any], *, player_index: int) -> dict[str, Any]: + idx = _player_index(player_index) + if idx < 2: + return raw["keybinds_p1_p2"][idx] + return raw["extended_keybinds_p3_p4"][idx - 2] + + +def _parsed_player_bind_block_is_uninitialized(raw_block: dict[str, Any]) -> bool: + return not any( + ( + raw_block["move_forward"], + raw_block["move_backward"], + raw_block["turn_left"], + raw_block["turn_right"], + raw_block["fire"], + *raw_block["reserved_keys"], + raw_block["aim_left"], + raw_block["aim_right"], + raw_block["axis_aim_y"], + raw_block["axis_aim_x"], + raw_block["axis_move_y"], + raw_block["axis_move_x"], + ), + ) -def _player_keybind_block(values: Sequence[int]) -> PlayerKeybindBlock: - if len(values) != PLAYER_BIND_BLOCK_DWORDS: - raise ValueError(f"keybind block must have {PLAYER_BIND_BLOCK_DWORDS} entries, got {len(values)}") - return cast(PlayerKeybindBlock, tuple(int(value) for value in values)) +def _player_controls_from_parsed_bind_block( + raw_block: dict[str, Any], + *, + player_index: int, + movement: MovementControlType, + aim_scheme: AimScheme, + show_direction_arrow: bool, +) -> CrimsonPlayerControls: + if _parsed_player_bind_block_is_uninitialized(raw_block): + defaults = _default_player_controls(player_index) + return CrimsonPlayerControls( + movement=movement, + aim_scheme=aim_scheme, + show_direction_arrow=show_direction_arrow, + move_codes=defaults.move_codes, + fire_code=defaults.fire_code, + keyboard_aim_codes=defaults.keyboard_aim_codes, + aim_axis_codes=defaults.aim_axis_codes, + move_axis_codes=defaults.move_axis_codes, + ) + return CrimsonPlayerControls( + movement=movement, + aim_scheme=aim_scheme, + show_direction_arrow=show_direction_arrow, + move_codes=( + raw_block["move_forward"], + raw_block["move_backward"], + raw_block["turn_left"], + raw_block["turn_right"], + ), + fire_code=raw_block["fire"], + keyboard_aim_codes=(raw_block["aim_left"], raw_block["aim_right"]), + aim_axis_codes=(raw_block["axis_aim_y"], raw_block["axis_aim_x"]), + move_axis_codes=(raw_block["axis_move_y"], raw_block["axis_move_x"]), + ) -def _decode_player_bind_block(raw: dict, *, player_index: int) -> PlayerKeybindBlock: - idx = _player_index(player_index) - if idx < 2: - block = tuple(int(value) for value in raw["keybinds_p1_p2"][idx]) - else: - block = tuple(int(value) for value in raw["extended_keybinds_p3_p4"][idx - 2]) - if _block_uninitialized(block): - return _default_player_bind_block(idx) - return _player_keybind_block(block) +def _encode_player_bind_block(player: CrimsonPlayerControls, *, player_index: int) -> dict[str, object]: + _ = player_index + return { + "move_forward": player.move_codes[0], + "move_backward": player.move_codes[1], + "turn_left": player.move_codes[2], + "turn_right": player.move_codes[3], + "fire": player.fire_code, + "reserved_keys": [int(_DEFAULT_WIRE_RESERVED_KEYS[0]), int(_DEFAULT_WIRE_RESERVED_KEYS[1])], + "aim_left": player.keyboard_aim_codes[0], + "aim_right": player.keyboard_aim_codes[1], + "axis_aim_y": player.aim_axis_codes[0], + "axis_aim_x": player.aim_axis_codes[1], + "axis_move_y": player.move_axis_codes[0], + "axis_move_x": player.move_axis_codes[1], + "padding": [int(_DEFAULT_WIRE_PADDING[0]), int(_DEFAULT_WIRE_PADDING[1]), int(_DEFAULT_WIRE_PADDING[2])], + } -def _encode_primary_keybinds(players: Sequence[CrimsonPlayerControls]) -> list[list[int]]: - return [[int(value) for value in players[idx].keybinds] for idx in range(2)] +def _encode_primary_keybinds(players: Sequence[CrimsonPlayerControls]) -> list[dict[str, object]]: + return [_encode_player_bind_block(players[idx], player_index=idx) for idx in range(2)] -def _encode_extended_keybinds(players: Sequence[CrimsonPlayerControls]) -> list[list[int]]: - return [[int(value) for value in players[idx].keybinds] for idx in range(2, 4)] +def _encode_extended_keybinds(players: Sequence[CrimsonPlayerControls]) -> list[dict[str, object]]: + return [_encode_player_bind_block(players[idx], player_index=idx) for idx in range(2, 4)] def _decode_direction_arrow(raw: dict, *, player_index: int) -> bool: @@ -458,21 +470,17 @@ def _saved_name_order_values() -> tuple[int, ...]: return tuple(range(SAVED_NAME_SLOT_COUNT)) -def _default_player_bind_block(player_index: int) -> PlayerKeybindBlock: - idx = _player_index(player_index) - return cast(PlayerKeybindBlock, _DEFAULT_PLAYER_BIND_BLOCKS[idx]) - - -def default_player_keybind_block(player_index: int) -> tuple[int, ...]: - return _default_player_bind_block(player_index) - - def _default_player_controls(player_index: int) -> CrimsonPlayerControls: + defaults = _DEFAULT_PLAYER_CONTROL_TEMPLATES[_player_index(player_index)] return CrimsonPlayerControls( - movement=MovementControlType.STATIC, - aim_scheme=AimScheme.MOUSE, - keybinds=_default_player_bind_block(player_index), - show_direction_arrow=True, + movement=defaults.movement, + aim_scheme=defaults.aim_scheme, + show_direction_arrow=defaults.show_direction_arrow, + move_codes=defaults.move_codes, + fire_code=defaults.fire_code, + keyboard_aim_codes=defaults.keyboard_aim_codes, + aim_axis_codes=defaults.aim_axis_codes, + move_axis_codes=defaults.move_axis_codes, ) @@ -522,8 +530,8 @@ def default_crimson_cfg(path: Path = Path("")) -> CrimsonConfig: _default_player_controls(2), _default_player_controls(3), ), - pick_perk_key=0x101, - reload_key=0x102, + pick_perk_code=0x101, + reload_code=0x102, ), ) @@ -546,10 +554,11 @@ def decode_crimson_cfg(path: Path, blob: bytes) -> CrimsonConfig: detail_preset = _require_range(detail_preset, minimum=1, maximum=5, field="detail_preset") players = tuple( - CrimsonPlayerControls( + _player_controls_from_parsed_bind_block( + _parsed_player_bind_block(raw, player_index=idx), + player_index=idx, movement=_decode_movement(raw["player_mode_flags"][idx]), aim_scheme=_decode_aim_scheme(raw["aim_schemes"][idx]), - keybinds=_decode_player_bind_block(raw, player_index=idx), show_direction_arrow=_decode_direction_arrow(raw, player_index=idx), ) for idx in range(4) @@ -607,8 +616,8 @@ def decode_crimson_cfg(path: Path, blob: bytes) -> CrimsonConfig: ), controls=CrimsonControlsConfig( players=players, # type: ignore[arg-type] - pick_perk_key=int(raw["keybind_pick_perk"]), - reload_key=int(raw["keybind_reload"]), + pick_perk_code=int(raw["keybind_pick_perk"]), + reload_code=int(raw["keybind_reload"]), ), ) @@ -685,8 +694,8 @@ def encode_crimson_cfg(config: CrimsonConfig) -> bytes: field="detail_preset", ) data["mouse_sensitivity"] = float(config.display.mouse_sensitivity) - data["keybind_pick_perk"] = int(config.controls.pick_perk_key) - data["keybind_reload"] = int(config.controls.reload_key) + data["keybind_pick_perk"] = config.controls.pick_perk_code + data["keybind_reload"] = config.controls.reload_code return CRIMSON_CFG_STRUCT.build(data) diff --git a/tests/conftest.py b/tests/conftest.py index 5ca392327..1a51208dc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -65,9 +65,9 @@ def _apply_config_updates(cfg: "CrimsonConfig", updates: Mapping[str, object]) - case "music_volume": cfg.audio.music_volume = _as_float(value) case "keybind_pick_perk": - cfg.controls.pick_perk_key = _as_int(value) + cfg.controls.pick_perk_code = _as_int(value) case "keybind_reload": - cfg.controls.reload_key = _as_int(value) + cfg.controls.reload_code = _as_int(value) case "player_name": cfg.profile.set_player_name_input(str(value)) case "selected_saved_name_slot": diff --git a/tests/grim/test_grim_config.py b/tests/grim/test_grim_config.py index 3dd0ef600..d110fc254 100644 --- a/tests/grim/test_grim_config.py +++ b/tests/grim/test_grim_config.py @@ -34,31 +34,62 @@ def test_crimson_cfg_backfills_zero_keybinds(tmp_path: Path) -> None: cfg = grim_config.default_crimson_cfg() data = grim_config.CRIMSON_CFG_STRUCT.parse(grim_config.encode_crimson_cfg(cfg)) data["keybinds_p1_p2"] = [ - [0] * grim_config.PLAYER_BIND_BLOCK_DWORDS, - [0] * grim_config.PLAYER_BIND_BLOCK_DWORDS, + { + "move_forward": 0, + "move_backward": 0, + "turn_left": 0, + "turn_right": 0, + "fire": 0, + "reserved_keys": [0, 0], + "aim_left": 0, + "aim_right": 0, + "axis_aim_y": 0, + "axis_aim_x": 0, + "axis_move_y": 0, + "axis_move_x": 0, + "padding": [0, 0, 0], + }, + { + "move_forward": 0, + "move_backward": 0, + "turn_left": 0, + "turn_right": 0, + "fire": 0, + "reserved_keys": [0, 0], + "aim_left": 0, + "aim_right": 0, + "axis_aim_y": 0, + "axis_aim_x": 0, + "axis_move_y": 0, + "axis_move_x": 0, + "padding": [0, 0, 0], + }, ] path = tmp_path / grim_config.CRIMSON_CFG_NAME path.write_bytes(grim_config.CRIMSON_CFG_STRUCT.build(data)) loaded = grim_config.ensure_crimson_cfg(tmp_path) - assert loaded.controls.player(0).keybinds == grim_config.default_player_keybind_block(0) - assert loaded.controls.player(1).keybinds == grim_config.default_player_keybind_block(1) + defaults = grim_config.default_crimson_cfg(Path("")).controls + assert loaded.controls.player(0) == defaults.player(0) + assert loaded.controls.player(1) == defaults.player(1) def test_player_keybind_roundtrip_for_extended_players_uses_reserved_gap_extension() -> None: cfg = grim_config.default_crimson_cfg(Path("")) - cfg.controls.player(2).set_keybind(4, 0x120) - cfg.controls.player(3).set_keybind(0, 0x11F) + cfg.controls.player(2).fire_code = 0x120 + cfg.controls.player(3).move_codes = (0x11F, 0x91, 0x8A, 0x97) blob = grim_config.encode_crimson_cfg(cfg) parsed = grim_config.CRIMSON_CFG_STRUCT.parse(blob) - assert list(parsed["extended_keybinds_p3_p4"][0])[4] == 0x120 - assert list(parsed["extended_keybinds_p3_p4"][1])[0] == 0x11F + assert int(parsed["extended_keybinds_p3_p4"][0]["fire"]) == 0x120 + assert int(parsed["extended_keybinds_p3_p4"][1]["move_forward"]) == 0x11F + assert list(parsed["extended_keybinds_p3_p4"][0]["reserved_keys"]) == [0x17E, 0x17E] + assert list(parsed["extended_keybinds_p3_p4"][0]["padding"]) == [0x17E, 0x17E, 0x17E] assert parsed["extended_reserved_gap"] == b"\x00" * len(parsed["extended_reserved_gap"]) loaded = grim_config.decode_crimson_cfg(Path(""), blob) - assert loaded.controls.player(2).keybind(4) == 0x120 - assert loaded.controls.player(3).keybind(0) == 0x11F + assert loaded.controls.player(2).fire_code == 0x120 + assert loaded.controls.player(3).move_codes[0] == 0x11F def test_direction_arrow_extension_roundtrip_for_players_three_and_four() -> None: diff --git a/tests/input/test_input_codes.py b/tests/input/test_input_codes.py index 8bda6826b..1516351dc 100644 --- a/tests/input/test_input_codes.py +++ b/tests/input/test_input_codes.py @@ -2,7 +2,6 @@ import crimson.input_codes as input_codes from crimson.input_codes import INPUT_CODE_UNBOUND, input_code_name -from grim.config import default_player_keybind_block def test_input_code_name_extended_axes_match_original_labels() -> None: @@ -47,22 +46,23 @@ def test_pressed_edge_does_not_retrigger_after_unpolled_held_frame(mocker) -> No input_codes.input_begin_frame() key_down["value"] = True - assert input_codes.input_code_is_pressed_for_player(0x11, player_index=0) + assert input_codes.input_code_is_pressed(0x11, player_index=0) input_codes.input_begin_frame() # Simulate a frame where this binding is not queried at all. input_codes.input_begin_frame() - assert not input_codes.input_code_is_pressed_for_player(0x11, player_index=0) + assert not input_codes.input_code_is_pressed(0x11, player_index=0) def test_input_primary_just_pressed_latches_across_multiplayer_fire_keys(mocker) -> None: down: dict[tuple[int, int], bool] = {} + fire_codes = (0x100, 0x9D, 0x36, 0x11F) - def _fake_input_code_is_down_for_player(key_code: int, *, player_index: int) -> bool: + def _fake_input_code_is_down(key_code: int, *, player_index: int = 0) -> bool: return bool(down.get((int(player_index), int(key_code)), False)) - mocker.patch.object(input_codes, "input_code_is_down_for_player", side_effect=_fake_input_code_is_down_for_player) + mocker.patch.object(input_codes, "input_code_is_down", side_effect=_fake_input_code_is_down) mocker.patch.object(input_codes.rl, "get_mouse_wheel_move", return_value=0.0) input_codes._PRESSED_STATE.prev_down.clear() @@ -74,30 +74,24 @@ def _fake_input_code_is_down_for_player(key_code: int, *, player_index: int) -> # Player 2 fire key press opens the latch in two-player mode. input_codes.input_begin_frame() down[(1, 0x9D)] = True - assert input_codes.input_primary_just_pressed(None, player_count=2) + assert input_codes.input_primary_just_pressed(fire_codes=fire_codes, player_count=2) # Holding any primary source should not retrigger next frame. input_codes.input_begin_frame() - assert not input_codes.input_primary_just_pressed(None, player_count=2) + assert not input_codes.input_primary_just_pressed(fire_codes=fire_codes, player_count=2) # Pressing another primary source while already held still does not retrigger. input_codes.input_begin_frame() down[(0, 0x100)] = True - assert not input_codes.input_primary_just_pressed(None, player_count=2) + assert not input_codes.input_primary_just_pressed(fire_codes=fire_codes, player_count=2) # Releasing all sources clears the latch. input_codes.input_begin_frame() down[(1, 0x9D)] = False down[(0, 0x100)] = False - assert not input_codes.input_primary_just_pressed(None, player_count=2) + assert not input_codes.input_primary_just_pressed(fire_codes=fire_codes, player_count=2) # Fresh primary press edges again after full release. input_codes.input_begin_frame() down[(0, 0x100)] = True - assert input_codes.input_primary_just_pressed(None, player_count=2) - - -def test_player_move_fire_keybinds_falls_back_to_default_block() -> None: - expected = tuple(int(value) for value in default_player_keybind_block(0)[:5]) - - assert input_codes.player_move_fire_keybinds(None, player_index=0) == expected + assert input_codes.input_primary_just_pressed(fire_codes=fire_codes, player_count=2) diff --git a/tests/input/test_local_input.py b/tests/input/test_local_input.py index c447dad72..23aa4b62a 100644 --- a/tests/input/test_local_input.py +++ b/tests/input/test_local_input.py @@ -41,35 +41,82 @@ def _test_config(**updates: object) -> CrimsonConfig: def _patch_keys_down(mocker: MockerFixture, *, down_codes: set[int]) -> None: mocker.patch.object( local_input, - "input_code_is_down_for_player", + "input_code_is_down", lambda key, **_kwargs: int(key) in down_codes, ) - mocker.patch.object(local_input, "input_code_is_pressed_for_player", lambda *_args, **_kwargs: False) - mocker.patch.object(local_input, "input_axis_value_for_player", lambda *_args, **_kwargs: 0.0) + mocker.patch.object(local_input, "input_code_is_pressed", lambda *_args, **_kwargs: False) + mocker.patch.object(local_input, "input_axis_value", lambda *_args, **_kwargs: 0.0) def _patch_no_user_input(mocker: MockerFixture) -> None: - mocker.patch.object(local_input, "input_code_is_down_for_player", lambda *_args, **_kwargs: False) - mocker.patch.object(local_input, "input_code_is_pressed_for_player", lambda *_args, **_kwargs: False) - mocker.patch.object(local_input, "input_axis_value_for_player", lambda *_args, **_kwargs: 0.0) + mocker.patch.object(local_input, "input_code_is_down", lambda *_args, **_kwargs: False) + mocker.patch.object(local_input, "input_code_is_pressed", lambda *_args, **_kwargs: False) + mocker.patch.object(local_input, "input_axis_value", lambda *_args, **_kwargs: 0.0) + + +def _bind_values(values: list[int] | tuple[int, ...] | range) -> tuple[int, ...]: + block = tuple(int(v) for v in values) + if len(block) != 16: + raise ValueError(f"expected 16 keybind values, got {len(block)}") + return block + + +def _set_player_bind_values( + cfg: CrimsonConfig, + values: list[int] | tuple[int, ...] | range, + *, + player_index: int = 0, +) -> CrimsonConfig: + block = _bind_values(values) + player = cfg.controls.player(player_index) + player.move_codes = (block[0], block[1], block[2], block[3]) + player.fire_code = block[4] + player.keyboard_aim_codes = (block[7], block[8]) + player.aim_axis_codes = (block[9], block[10]) + player.move_axis_codes = (block[11], block[12]) + return cfg + + +def _set_player_modes( + cfg: CrimsonConfig, + *, + aim_scheme: AimScheme | None = None, + move_mode: MovementControlType | None = None, + player_index: int = 0, +) -> CrimsonConfig: + player = cfg.controls.player(player_index) + if aim_scheme is not None: + player.aim_scheme = aim_scheme + if move_mode is not None: + player.movement = move_mode + return cfg + + +def _config_with_player_bind_values( + values: list[int] | tuple[int, ...] | range, + *, + player_index: int = 0, + player_count: int = 1, + aim_scheme: AimScheme | None = None, + move_mode: MovementControlType | None = None, +) -> CrimsonConfig: + cfg = _test_config(player_count=player_count) + _set_player_modes(cfg, aim_scheme=aim_scheme, move_mode=move_mode, player_index=player_index) + return _set_player_bind_values(cfg, values, player_index=player_index) def test_local_input_computer_aim_auto_fires_without_fire_pressed(mocker: MockerFixture) -> None: _patch_no_user_input(mocker) - mocker.patch.object( - local_input.LocalInputInterpreter, - "_safe_controls_modes", - staticmethod(lambda _config, *, player_index: (AimScheme.COMPUTER, MovementControlType.STATIC)), - ) interpreter = local_input.LocalInputInterpreter() player = PlayerState(index=0, pos=Vec2(512.0, 512.0), aim=Vec2(560.0, 512.0)) creatures = [_DummyCreature(pos=Vec2(612.0, 512.0), active=True, hp=20.0)] + config = _set_player_modes(_test_config(), aim_scheme=AimScheme.COMPUTER) out = interpreter.build_player_input( player_index=0, player=player, - config=None, + config=config, mouse_screen=Vec2(), mouse_world=Vec2(), screen_center=Vec2(), @@ -85,19 +132,15 @@ def test_local_input_computer_aim_auto_fires_without_fire_pressed(mocker: Mocker def test_local_input_computer_aim_without_target_points_away_from_center(mocker: MockerFixture) -> None: _patch_no_user_input(mocker) - mocker.patch.object( - local_input.LocalInputInterpreter, - "_safe_controls_modes", - staticmethod(lambda _config, *, player_index: (AimScheme.COMPUTER, MovementControlType.STATIC)), - ) interpreter = local_input.LocalInputInterpreter() player = PlayerState(index=0, pos=Vec2(512.0, 512.0), aim=Vec2(512.0, 512.0)) + config = _set_player_modes(_test_config(), aim_scheme=AimScheme.COMPUTER) out = interpreter.build_player_input( player_index=0, player=player, - config=None, + config=config, mouse_screen=Vec2(), mouse_world=Vec2(), screen_center=Vec2(), @@ -115,15 +158,11 @@ def test_local_input_computer_target_state_tracks_player_identity_not_call_slot( mocker: MockerFixture, ) -> None: _patch_no_user_input(mocker) - mocker.patch.object( - local_input.LocalInputInterpreter, - "_safe_controls_modes", - staticmethod(lambda _config, *, player_index: (AimScheme.COMPUTER, MovementControlType.STATIC)), - ) interpreter = local_input.LocalInputInterpreter() player0 = PlayerState(index=0, pos=Vec2(0.0, 0.0), aim=Vec2(0.0, 0.0)) player1 = PlayerState(index=1, pos=Vec2(128.0, 0.0), aim=Vec2(128.0, 0.0)) + config = _set_player_modes(_test_config(), aim_scheme=AimScheme.COMPUTER) creatures = [ _DummyCreature(pos=Vec2(100.0, 0.0), active=True, hp=20.0), # nearest to player0 _DummyCreature(pos=Vec2(130.0, 0.0), active=True, hp=20.0), # nearest to player1 @@ -133,7 +172,7 @@ def test_local_input_computer_target_state_tracks_player_identity_not_call_slot( interpreter.build_player_input( player_index=0, player=player1, - config=None, + config=config, mouse_screen=Vec2(), mouse_world=Vec2(), screen_center=Vec2(), @@ -144,7 +183,7 @@ def test_local_input_computer_target_state_tracks_player_identity_not_call_slot( out = interpreter.build_player_input( player_index=0, player=player0, - config=None, + config=config, mouse_screen=Vec2(), mouse_world=Vec2(), screen_center=Vec2(), @@ -171,24 +210,15 @@ def test_local_input_static_mode_conflict_precedence_matches_native( expected_move: Vec2, ) -> None: _patch_keys_down(mocker, down_codes=down_codes) - mocker.patch.object( - local_input, - "_load_player_bind_block", - lambda _config, *, player_index: tuple(range(16)), - ) - mocker.patch.object( - local_input.LocalInputInterpreter, - "_safe_controls_modes", - staticmethod(lambda _config, *, player_index: (AimScheme.MOUSE, MovementControlType.STATIC)), - ) interpreter = local_input.LocalInputInterpreter() player = PlayerState(index=0, pos=Vec2(100.0, 100.0), aim=Vec2(160.0, 100.0)) + config = _config_with_player_bind_values(range(16)) out = interpreter.build_player_input( player_index=0, player=player, - config=None, + config=config, mouse_screen=Vec2(), mouse_world=Vec2(), screen_center=Vec2(), @@ -203,24 +233,15 @@ def test_local_input_relative_mode_single_player_uses_alt_arrow_fallback( mocker: MockerFixture, ) -> None: _patch_keys_down(mocker, down_codes={0xC8, 0xCB}) - mocker.patch.object( - local_input, - "_load_player_bind_block", - lambda _config, *, player_index: (0x17E,) * 16, - ) - mocker.patch.object( - local_input.LocalInputInterpreter, - "_safe_controls_modes", - staticmethod(lambda _config, *, player_index: (AimScheme.MOUSE, MovementControlType.RELATIVE)), - ) interpreter = local_input.LocalInputInterpreter() player = PlayerState(index=0, pos=Vec2(100.0, 100.0), aim=Vec2(160.0, 100.0)) + config = _config_with_player_bind_values((0x17E,) * 16, move_mode=MovementControlType.RELATIVE) out = interpreter.build_player_input( player_index=0, player=player, - config=None, + config=config, mouse_screen=Vec2(), mouse_world=Vec2(), screen_center=Vec2(), @@ -237,17 +258,7 @@ def test_local_input_relative_mode_multiplayer_does_not_use_alt_arrow_fallback( mocker: MockerFixture, ) -> None: _patch_keys_down(mocker, down_codes={0xC8, 0xCB}) - mocker.patch.object( - local_input, - "_load_player_bind_block", - lambda _config, *, player_index: (0x17E,) * 16, - ) - mocker.patch.object( - local_input.LocalInputInterpreter, - "_safe_controls_modes", - staticmethod(lambda _config, *, player_index: (AimScheme.MOUSE, MovementControlType.RELATIVE)), - ) - config = _test_config(player_count=2) + config = _config_with_player_bind_values((0x17E,) * 16, player_count=2, move_mode=MovementControlType.RELATIVE) interpreter = local_input.LocalInputInterpreter() player = PlayerState(index=0, pos=Vec2(100.0, 100.0), aim=Vec2(160.0, 100.0)) @@ -274,14 +285,9 @@ def test_local_input_reload_pressed_is_available_in_multiplayer( _patch_no_user_input(mocker) mocker.patch.object( local_input, - "input_code_is_pressed_for_player", + "input_code_is_pressed", lambda key, **_kwargs: int(key) == 0x102, ) - mocker.patch.object( - local_input.LocalInputInterpreter, - "_safe_controls_modes", - staticmethod(lambda _config, *, player_index: (AimScheme.MOUSE, MovementControlType.STATIC)), - ) interpreter = local_input.LocalInputInterpreter() player = PlayerState(index=0, pos=Vec2(100.0, 100.0), aim=Vec2(160.0, 100.0)) @@ -316,14 +322,9 @@ def test_local_input_reload_pressed_reads_per_player_input_slot( _patch_no_user_input(mocker) mocker.patch.object( local_input, - "input_code_is_pressed_for_player", + "input_code_is_pressed", lambda key, **kwargs: int(key) == 0x102 and int(kwargs.get("player_index", -1)) == 1, ) - mocker.patch.object( - local_input.LocalInputInterpreter, - "_safe_controls_modes", - staticmethod(lambda _config, *, player_index: (AimScheme.MOUSE, MovementControlType.STATIC)), - ) interpreter = local_input.LocalInputInterpreter() player = PlayerState(index=1, pos=Vec2(100.0, 100.0), aim=Vec2(160.0, 100.0)) @@ -347,33 +348,24 @@ def test_local_input_mouse_point_click_marks_move_to_cursor_press( mouse_world = Vec2(160.0, 140.0) mocker.patch.object( local_input, - "input_code_is_down_for_player", + "input_code_is_down", lambda key, **_kwargs: int(key) == 0x102, ) mocker.patch.object( local_input, - "input_code_is_pressed_for_player", + "input_code_is_pressed", lambda key, **_kwargs: int(key) == 0x102, ) - mocker.patch.object(local_input, "input_axis_value_for_player", lambda *_args, **_kwargs: 0.0) - mocker.patch.object( - local_input, - "_load_player_bind_block", - lambda _config, *, player_index: tuple(range(16)), - ) - mocker.patch.object( - local_input.LocalInputInterpreter, - "_safe_controls_modes", - staticmethod(lambda _config, *, player_index: (AimScheme.MOUSE, MovementControlType.MOUSE_POINT_CLICK)), - ) + mocker.patch.object(local_input, "input_axis_value", lambda *_args, **_kwargs: 0.0) interpreter = local_input.LocalInputInterpreter() player = PlayerState(index=0, pos=Vec2(100.0, 100.0), aim=Vec2(160.0, 100.0)) + config = _config_with_player_bind_values(range(16), move_mode=MovementControlType.MOUSE_POINT_CLICK) out = interpreter.build_player_input( player_index=0, player=player, - config=None, + config=config, mouse_screen=Vec2(), mouse_world=mouse_world, screen_center=Vec2(), @@ -393,20 +385,16 @@ def test_local_input_computer_move_mode_near_center_heads_toward_target( mocker: MockerFixture, ) -> None: _patch_no_user_input(mocker) - mocker.patch.object( - local_input.LocalInputInterpreter, - "_safe_controls_modes", - staticmethod(lambda _config, *, player_index: (AimScheme.MOUSE, MovementControlType.COMPUTER)), - ) interpreter = local_input.LocalInputInterpreter() player = PlayerState(index=0, pos=Vec2(500.0, 500.0), aim=Vec2(560.0, 500.0)) creatures = [_DummyCreature(pos=Vec2(560.0, 500.0), active=True, hp=20.0)] + config = _set_player_modes(_test_config(), move_mode=MovementControlType.COMPUTER) out = interpreter.build_player_input( player_index=0, player=player, - config=None, + config=config, mouse_screen=Vec2(), mouse_world=Vec2(), screen_center=Vec2(), @@ -422,20 +410,16 @@ def test_local_input_computer_move_mode_far_from_center_heads_toward_center( mocker: MockerFixture, ) -> None: _patch_no_user_input(mocker) - mocker.patch.object( - local_input.LocalInputInterpreter, - "_safe_controls_modes", - staticmethod(lambda _config, *, player_index: (AimScheme.MOUSE, MovementControlType.COMPUTER)), - ) interpreter = local_input.LocalInputInterpreter() player = PlayerState(index=0, pos=Vec2(900.0, 900.0), aim=Vec2(960.0, 900.0)) creatures = [_DummyCreature(pos=Vec2(960.0, 900.0), active=True, hp=20.0)] + config = _set_player_modes(_test_config(), move_mode=MovementControlType.COMPUTER) out = interpreter.build_player_input( player_index=0, player=player, - config=None, + config=config, mouse_screen=Vec2(), mouse_world=Vec2(), screen_center=Vec2(), @@ -452,20 +436,16 @@ def test_local_input_computer_aim_scheme_forces_computer_movement( mocker: MockerFixture, ) -> None: _patch_no_user_input(mocker) - mocker.patch.object( - local_input.LocalInputInterpreter, - "_safe_controls_modes", - staticmethod(lambda _config, *, player_index: (AimScheme.COMPUTER, MovementControlType.STATIC)), - ) interpreter = local_input.LocalInputInterpreter() player = PlayerState(index=0, pos=Vec2(500.0, 500.0), aim=Vec2(560.0, 500.0)) creatures = [_DummyCreature(pos=Vec2(560.0, 500.0), active=True, hp=20.0)] + config = _set_player_modes(_test_config(), aim_scheme=AimScheme.COMPUTER) out = interpreter.build_player_input( player_index=0, player=player, - config=None, + config=config, mouse_screen=Vec2(), mouse_world=Vec2(), screen_center=Vec2(), @@ -481,24 +461,15 @@ def test_local_input_joystick_aim_uses_pov_not_aim_keybinds( mocker: MockerFixture, ) -> None: _patch_keys_down(mocker, down_codes={8}) - mocker.patch.object( - local_input, - "_load_player_bind_block", - lambda _config, *, player_index: tuple(range(16)), - ) - mocker.patch.object( - local_input.LocalInputInterpreter, - "_safe_controls_modes", - staticmethod(lambda _config, *, player_index: (AimScheme.JOYSTICK, MovementControlType.STATIC)), - ) interpreter = local_input.LocalInputInterpreter() player = PlayerState(index=0, pos=Vec2(100.0, 100.0), aim=Vec2(160.0, 100.0)) + config = _config_with_player_bind_values(range(16), aim_scheme=AimScheme.JOYSTICK) out = interpreter.build_player_input( player_index=0, player=player, - config=None, + config=config, mouse_screen=Vec2(), mouse_world=Vec2(), screen_center=Vec2(), @@ -515,24 +486,15 @@ def test_local_input_joystick_aim_turns_with_pov_input( mocker: MockerFixture, ) -> None: _patch_keys_down(mocker, down_codes={0x134}) - mocker.patch.object( - local_input, - "_load_player_bind_block", - lambda _config, *, player_index: tuple(range(16)), - ) - mocker.patch.object( - local_input.LocalInputInterpreter, - "_safe_controls_modes", - staticmethod(lambda _config, *, player_index: (AimScheme.JOYSTICK, MovementControlType.STATIC)), - ) interpreter = local_input.LocalInputInterpreter() player = PlayerState(index=0, pos=Vec2(100.0, 100.0), aim=Vec2(160.0, 100.0)) + config = _config_with_player_bind_values(range(16), aim_scheme=AimScheme.JOYSTICK) out = interpreter.build_player_input( player_index=0, player=player, - config=None, + config=config, mouse_screen=Vec2(), mouse_world=Vec2(), screen_center=Vec2(), @@ -550,29 +512,25 @@ def test_local_input_joystick_aim_reads_player_pov_by_default( ) -> None: mocker.patch.object( local_input, - "input_code_is_down_for_player", + "input_code_is_down", lambda key, **kwargs: int(key) == 0x134 and int(kwargs.get("player_index", -1)) == 1, ) - mocker.patch.object(local_input, "input_code_is_pressed_for_player", lambda *_args, **_kwargs: False) - mocker.patch.object(local_input, "input_axis_value_for_player", lambda *_args, **_kwargs: 0.0) - mocker.patch.object( - local_input, - "_load_player_bind_block", - lambda _config, *, player_index: tuple(range(16)), - ) - mocker.patch.object( - local_input.LocalInputInterpreter, - "_safe_controls_modes", - staticmethod(lambda _config, *, player_index: (AimScheme.JOYSTICK, MovementControlType.STATIC)), - ) + mocker.patch.object(local_input, "input_code_is_pressed", lambda *_args, **_kwargs: False) + mocker.patch.object(local_input, "input_axis_value", lambda *_args, **_kwargs: 0.0) interpreter = local_input.LocalInputInterpreter() player = PlayerState(index=1, pos=Vec2(100.0, 100.0), aim=Vec2(160.0, 100.0)) + config = _config_with_player_bind_values( + range(16), + player_index=1, + player_count=2, + aim_scheme=AimScheme.JOYSTICK, + ) out = interpreter.build_player_input( player_index=1, player=player, - config=_test_config(player_count=2), + config=config, mouse_screen=Vec2(), mouse_world=Vec2(), screen_center=Vec2(), @@ -590,30 +548,26 @@ def test_local_input_joystick_aim_preserve_bugs_uses_player1_pov_slot( ) -> None: mocker.patch.object( local_input, - "input_code_is_down_for_player", + "input_code_is_down", lambda key, **kwargs: int(key) == 0x134 and int(kwargs.get("player_index", -1)) == 0, ) - mocker.patch.object(local_input, "input_code_is_pressed_for_player", lambda *_args, **_kwargs: False) - mocker.patch.object(local_input, "input_axis_value_for_player", lambda *_args, **_kwargs: 0.0) - mocker.patch.object( - local_input, - "_load_player_bind_block", - lambda _config, *, player_index: tuple(range(16)), - ) - mocker.patch.object( - local_input.LocalInputInterpreter, - "_safe_controls_modes", - staticmethod(lambda _config, *, player_index: (AimScheme.JOYSTICK, MovementControlType.STATIC)), - ) + mocker.patch.object(local_input, "input_code_is_pressed", lambda *_args, **_kwargs: False) + mocker.patch.object(local_input, "input_axis_value", lambda *_args, **_kwargs: 0.0) interpreter = local_input.LocalInputInterpreter() interpreter.set_preserve_bugs(True) player = PlayerState(index=1, pos=Vec2(100.0, 100.0), aim=Vec2(160.0, 100.0)) + config = _config_with_player_bind_values( + range(16), + player_index=1, + player_count=2, + aim_scheme=AimScheme.JOYSTICK, + ) out = interpreter.build_player_input( player_index=1, player=player, - config=_test_config(player_count=2), + config=config, mouse_screen=Vec2(), mouse_world=Vec2(), screen_center=Vec2(), @@ -629,31 +583,22 @@ def test_local_input_joystick_aim_preserve_bugs_uses_player1_pov_slot( def test_local_input_dual_action_pad_aim_uses_native_radius_scale( mocker: MockerFixture, ) -> None: - mocker.patch.object(local_input, "input_code_is_down_for_player", lambda *_args, **_kwargs: False) - mocker.patch.object(local_input, "input_code_is_pressed_for_player", lambda *_args, **_kwargs: False) + mocker.patch.object(local_input, "input_code_is_down", lambda *_args, **_kwargs: False) + mocker.patch.object(local_input, "input_code_is_pressed", lambda *_args, **_kwargs: False) mocker.patch.object( local_input, - "input_axis_value_for_player", + "input_axis_value", lambda key, **_kwargs: 1.0 if int(key) == 10 else 0.0, ) - mocker.patch.object( - local_input, - "_load_player_bind_block", - lambda _config, *, player_index: tuple(range(16)), - ) - mocker.patch.object( - local_input.LocalInputInterpreter, - "_safe_controls_modes", - staticmethod(lambda _config, *, player_index: (AimScheme.DUAL_ACTION_PAD, MovementControlType.STATIC)), - ) interpreter = local_input.LocalInputInterpreter() player = PlayerState(index=0, pos=Vec2(100.0, 100.0), aim=Vec2(160.0, 100.0)) + config = _config_with_player_bind_values(range(16), aim_scheme=AimScheme.DUAL_ACTION_PAD) out = interpreter.build_player_input( player_index=0, player=player, - config=None, + config=config, mouse_screen=Vec2(), mouse_world=Vec2(), screen_center=Vec2(), @@ -670,24 +615,15 @@ def test_local_input_keyboard_aim_in_static_mode_reanchors_to_heading( mocker: MockerFixture, ) -> None: _patch_no_user_input(mocker) - mocker.patch.object( - local_input, - "_load_player_bind_block", - lambda _config, *, player_index: tuple(range(16)), - ) - mocker.patch.object( - local_input.LocalInputInterpreter, - "_safe_controls_modes", - staticmethod(lambda _config, *, player_index: (AimScheme.KEYBOARD, MovementControlType.STATIC)), - ) interpreter = local_input.LocalInputInterpreter() player = PlayerState(index=0, pos=Vec2(100.0, 100.0), aim=Vec2(180.0, 130.0), aim_heading=0.0) + config = _config_with_player_bind_values(range(16), aim_scheme=AimScheme.KEYBOARD) out = interpreter.build_player_input( player_index=0, player=player, - config=None, + config=config, mouse_screen=Vec2(), mouse_world=Vec2(), screen_center=Vec2(), @@ -703,24 +639,19 @@ def test_local_input_keyboard_aim_with_non_relative_move_mode_keeps_world_aim( mocker: MockerFixture, ) -> None: _patch_no_user_input(mocker) - mocker.patch.object( - local_input, - "_load_player_bind_block", - lambda _config, *, player_index: tuple(range(16)), - ) - mocker.patch.object( - local_input.LocalInputInterpreter, - "_safe_controls_modes", - staticmethod(lambda _config, *, player_index: (AimScheme.KEYBOARD, MovementControlType.DUAL_ACTION_PAD)), - ) interpreter = local_input.LocalInputInterpreter() player = PlayerState(index=0, pos=Vec2(100.0, 100.0), aim=Vec2(180.0, 130.0), aim_heading=0.0) + config = _config_with_player_bind_values( + range(16), + aim_scheme=AimScheme.KEYBOARD, + move_mode=MovementControlType.DUAL_ACTION_PAD, + ) out = interpreter.build_player_input( player_index=0, player=player, - config=None, + config=config, mouse_screen=Vec2(), mouse_world=Vec2(), screen_center=Vec2(), @@ -738,25 +669,16 @@ def test_local_input_relative_mouse_aim_centered_keeps_world_aim( mocker: MockerFixture, ) -> None: _patch_no_user_input(mocker) - mocker.patch.object( - local_input, - "_load_player_bind_block", - lambda _config, *, player_index: tuple(range(16)), - ) - mocker.patch.object( - local_input.LocalInputInterpreter, - "_safe_controls_modes", - staticmethod(lambda _config, *, player_index: (AimScheme.MOUSE_RELATIVE, MovementControlType.STATIC)), - ) interpreter = local_input.LocalInputInterpreter() player = PlayerState(index=0, pos=Vec2(100.0, 100.0), aim=Vec2(180.0, 130.0), aim_heading=0.0) center = Vec2(320.0, 200.0) + config = _config_with_player_bind_values(range(16), aim_scheme=AimScheme.MOUSE_RELATIVE) out = interpreter.build_player_input( player_index=0, player=player, - config=None, + config=config, mouse_screen=center, mouse_world=Vec2(), screen_center=center, diff --git a/tests/modes/test_perk_prompt_controller.py b/tests/modes/test_perk_prompt_controller.py index 151499ea0..61329fbb5 100644 --- a/tests/modes/test_perk_prompt_controller.py +++ b/tests/modes/test_perk_prompt_controller.py @@ -16,7 +16,7 @@ def _config(): config = default_crimson_cfg() - config.controls.pick_perk_key = 0x101 + config.controls.pick_perk_code = 0x101 config.gameplay.show_info_texts = True return config @@ -62,17 +62,12 @@ def test_prompt_open_request_from_pick_key(mocker) -> None: mocker.patch.object( perk_prompt_controller_module, - "input_code_is_pressed_for_player", + "input_code_is_pressed", return_value=True, ) mocker.patch.object( perk_prompt_controller_module, - "player_fire_keybind", - return_value=0x100, - ) - mocker.patch.object( - perk_prompt_controller_module, - "input_code_is_down_for_player", + "input_code_is_down", return_value=False, ) mocker.patch.object( @@ -98,14 +93,9 @@ def test_prompt_open_request_from_hover_click(mocker) -> None: mocker.patch.object( perk_prompt_controller_module, - "input_code_is_pressed_for_player", + "input_code_is_pressed", return_value=False, ) - mocker.patch.object( - perk_prompt_controller_module, - "player_fire_keybind", - return_value=0x100, - ) mocker.patch.object( perk_prompt_controller_module, "input_primary_just_pressed", @@ -146,7 +136,7 @@ def test_prompt_open_request_returns_false_while_menu_active(mocker) -> None: mocker.patch.object( perk_prompt_controller_module, - "input_code_is_pressed_for_player", + "input_code_is_pressed", return_value=True, ) diff --git a/tests/ui/test_controls_labels.py b/tests/ui/test_controls_labels.py index 53f21463d..92b084cb3 100644 --- a/tests/ui/test_controls_labels.py +++ b/tests/ui/test_controls_labels.py @@ -5,12 +5,11 @@ from crimson.aim_schemes import AimScheme from crimson.movement_controls import MovementControlType from crimson.screens.panels.controls_labels import ( - PICK_PERK_BIND_SLOT, - RELOAD_BIND_SLOT, + RebindRowSpec, + RebindTarget, controls_aim_method_dropdown_ids, controls_method_labels, - controls_method_values, - controls_rebind_slot_plan, + controls_rebind_plan, input_configure_for_label, input_scheme_label, ) @@ -55,17 +54,16 @@ def test_controls_method_labels_reads_player_arrays() -> None: assert controls_method_labels(controls, player_index=1) == ("Joystick", "Mouse point click") assert controls_method_labels(controls, player_index=2) == ("Dual Action Pad", "Computer") assert controls_method_labels(controls, player_index=3) == ("Computer", "Relative") - assert controls_method_values(controls, player_index=1) == (AimScheme.JOYSTICK, MovementControlType.MOUSE_POINT_CLICK) def test_controls_method_labels_defaults_missing_blob() -> None: assert controls_method_labels(_controls(), player_index=0) == ("Mouse", "Static") -def test_controls_method_values_unknown_move_mode_maps_to_unknown_enum() -> None: +def test_controls_method_labels_unknown_move_mode_maps_to_unknown_enum() -> None: controls = _controls() controls.player(0).movement = MovementControlType.UNKNOWN - assert controls_method_values(controls, player_index=0) == (AimScheme.MOUSE, MovementControlType.UNKNOWN) + assert controls_method_labels(controls, player_index=0) == ("Mouse", "Unknown") def test_controls_aim_method_dropdown_ids_hides_computer_unless_loaded() -> None: @@ -86,23 +84,39 @@ def test_controls_aim_method_dropdown_ids_hides_computer_unless_loaded() -> None ) -def test_controls_rebind_slot_plan_keyboard_static_player1() -> None: - aim_rows, move_rows, misc_rows = controls_rebind_slot_plan( +def test_controls_rebind_plan_keyboard_static_player1() -> None: + aim_rows, move_rows, misc_rows = controls_rebind_plan( aim_scheme=AimScheme.KEYBOARD, move_mode=MovementControlType.STATIC, player_index=0, ) - assert aim_rows == (("Torso left:", 7), ("Torso right:", 8), ("Fire:", 4)) - assert move_rows == (("Move Up:", 0), ("Move Down:", 1), ("Move Left:", 2), ("Move Right:", 3)) - assert misc_rows == (("Level Up:", PICK_PERK_BIND_SLOT), ("Reload:", RELOAD_BIND_SLOT)) + assert aim_rows == ( + RebindRowSpec("Torso left:", RebindTarget.PLAYER_KEYBOARD_AIM_CODES, 0), + RebindRowSpec("Torso right:", RebindTarget.PLAYER_KEYBOARD_AIM_CODES, 1), + RebindRowSpec("Fire:", RebindTarget.PLAYER_FIRE_CODE), + ) + assert move_rows == ( + RebindRowSpec("Move Up:", RebindTarget.PLAYER_MOVE_CODES, 0), + RebindRowSpec("Move Down:", RebindTarget.PLAYER_MOVE_CODES, 1), + RebindRowSpec("Move Left:", RebindTarget.PLAYER_MOVE_CODES, 2), + RebindRowSpec("Move Right:", RebindTarget.PLAYER_MOVE_CODES, 3), + ) + assert misc_rows == ( + RebindRowSpec("Level Up:", RebindTarget.GLOBAL_PICK_PERK_CODE), + RebindRowSpec("Reload:", RebindTarget.GLOBAL_RELOAD_CODE), + ) -def test_controls_rebind_slot_plan_dualpad_mouse_cursor_player2() -> None: - aim_rows, move_rows, misc_rows = controls_rebind_slot_plan( +def test_controls_rebind_plan_dualpad_mouse_cursor_player2() -> None: + aim_rows, move_rows, misc_rows = controls_rebind_plan( aim_scheme=AimScheme.DUAL_ACTION_PAD, move_mode=MovementControlType.MOUSE_POINT_CLICK, player_index=1, ) - assert aim_rows == (("Aim Up/Down Axis:", 9), ("Aim Left/Right Axis:", 10), ("Fire:", 4)) - assert move_rows == (("Move to cursor:", RELOAD_BIND_SLOT),) + assert aim_rows == ( + RebindRowSpec("Aim Up/Down Axis:", RebindTarget.PLAYER_AIM_AXIS_CODES, 0, axis=True), + RebindRowSpec("Aim Left/Right Axis:", RebindTarget.PLAYER_AIM_AXIS_CODES, 1, axis=True), + RebindRowSpec("Fire:", RebindTarget.PLAYER_FIRE_CODE), + ) + assert move_rows == (RebindRowSpec("Move to cursor:", RebindTarget.GLOBAL_RELOAD_CODE),) assert misc_rows == ()