Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ The plugin has a settings page accessible via **Settings → Plugins → HELLDIV
|---------|-------------|---------|
| **Key Delay** | Delay between key presses (0.01-0.20 seconds). Increase if stratagems fail. | 0.03s |
| **Modifier Key** | Key to open stratagem menu when not in Hero mode. Options: Left/Right Ctrl, Alt, Shift | Left Ctrl |
| **Direction Keys** | Keys used for directional inputs. Options: Arrow keys or WASD. | Arrow keys |
| **Hold Modifier Key** | If ON, holds modifier during entire sequence. If OFF, presses/releases modifier then types sequence. | ON |
| **Show Labels** | If ON, displays text labels on stratagem buttons. If OFF, shows icons only. | ON |

Expand All @@ -152,6 +153,7 @@ Settings are managed through StreamController's plugin settings system:
```python
DEFAULT_KEY_DELAY = 0.03 # Seconds between key presses
DEFAULT_MODIFIER_KEY = "KEY_LEFTCTRL" # evdev key code
DEFAULT_DIRECTION_KEY_LAYOUT = "arrow_keys" # Arrow keys or WASD
DEFAULT_HOLD_MODIFIER = True # Hold modifier during sequence
DEFAULT_SHOW_LABELS = True # Show text labels on buttons

Expand Down Expand Up @@ -216,7 +218,7 @@ python test_stratagems.py
### How It Works

1. Focus the window
2. Press your Stream Deck button (or type arrow keys manually)
2. Press your Stream Deck button (or type arrow keys/WASD manually)
3. The tool displays the input sequence (↑ ↓ ← →)
4. Shows which stratagem matches the sequence
5. Press Escape to clear and try another
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Assumes the following:
* Left Control will open Stratagem menu (Hold/Press)
* Stratagem Directions have been remapped to arrow keys (Up, Down, Left, Right)
* Stratagem directions use either the arrow keys or W/A/S/D, selectable in the plugin settings

## For Users

Expand All @@ -18,6 +18,7 @@ Access settings via **Settings → Plugins → HELLDIVERS 2 → ⚙️**
|---------|-------------|---------|
| **Key Delay** | Delay between key presses. Increase if stratagems fail to register. | 0.03s |
| **Modifier Key** | Key to open stratagem menu (Left/Right Ctrl, Alt, or Shift) | Left Ctrl |
| **Direction Keys** | Keys used for Up, Down, Left, and Right stratagem inputs (Arrow keys or WASD) | Arrow keys |
| **Hold Modifier Key** | Hold modifier during sequence vs press-and-release | ON |
| **Show Labels** | Display text labels on buttons (OFF for icon-only look) | ON |

Expand Down
34 changes: 34 additions & 0 deletions key_mapping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Direction-to-key mappings used when executing stratagem sequences."""

DEFAULT_DIRECTION_KEY_LAYOUT = "arrow_keys"

DIRECTION_KEY_LAYOUTS = {
"arrow_keys": {
"UP": "KEY_UP",
"DOWN": "KEY_DOWN",
"LEFT": "KEY_LEFT",
"RIGHT": "KEY_RIGHT",
},
"wasd": {
"UP": "KEY_W",
"DOWN": "KEY_S",
"LEFT": "KEY_A",
"RIGHT": "KEY_D",
},
}


def normalize_direction_key_layout(layout: str) -> str:
"""Return a supported layout, falling back for missing or stale settings."""
if layout in DIRECTION_KEY_LAYOUTS:
return layout
return DEFAULT_DIRECTION_KEY_LAYOUT


def get_direction_key(direction: str, layout: str) -> str:
"""Return the evdev key name for a direction in the selected layout."""
normalized_layout = normalize_direction_key_layout(layout)
try:
return DIRECTION_KEY_LAYOUTS[normalized_layout][direction]
except KeyError as error:
raise ValueError(f"Unsupported stratagem direction: {direction!r}") from error
70 changes: 66 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
from gi.repository import Gtk, Adw
from loguru import logger as log

from key_mapping import (
DEFAULT_DIRECTION_KEY_LAYOUT,
get_direction_key as resolve_direction_key,
normalize_direction_key_layout,
)


log.debug("Init HELLDIVERS 2")

Expand All @@ -23,6 +29,11 @@
DEFAULT_HOLD_MODIFIER = True # False = press/release, True = hold during sequence
DEFAULT_SHOW_LABELS = True # Show text labels on buttons

DIRECTION_KEY_LAYOUT_OPTIONS = {
"Arrow keys": "arrow_keys",
"WASD keys": "wasd",
}

# Available modifier keys
MODIFIER_KEYS = {
"Left Ctrl": "KEY_LEFTCTRL",
Expand Down Expand Up @@ -220,10 +231,11 @@ def on_key_down(self, data=None):
sleep(key_delay)

for key in sequence:
self.plugin_base.ui.write(ecodes.EV_KEY, ecodes.ecodes[f"KEY_{key}"], 1)
key_code = ecodes.ecodes[self.plugin_base.get_direction_key(key)]
self.plugin_base.ui.write(ecodes.EV_KEY, key_code, 1)
self.plugin_base.ui.syn()
sleep(key_delay)
self.plugin_base.ui.write(ecodes.EV_KEY, ecodes.ecodes[f"KEY_{key}"], 0)
self.plugin_base.ui.write(ecodes.EV_KEY, key_code, 0)
self.plugin_base.ui.syn()
sleep(key_delay)
except Exception as e:
Expand Down Expand Up @@ -410,10 +422,11 @@ def on_key_down(self, data=None):
sleep(key_delay)

for key in self.stratagem:
self.plugin_base.ui.write(ecodes.EV_KEY, ecodes.ecodes[f"KEY_{key}"], 1)
key_code = ecodes.ecodes[self.plugin_base.get_direction_key(key)]
self.plugin_base.ui.write(ecodes.EV_KEY, key_code, 1)
self.plugin_base.ui.syn()
sleep(key_delay)
self.plugin_base.ui.write(ecodes.EV_KEY, ecodes.ecodes[f"KEY_{key}"], 0)
self.plugin_base.ui.write(ecodes.EV_KEY, key_code, 0)
self.plugin_base.ui.syn()
sleep(key_delay)
except Exception as e:
Expand Down Expand Up @@ -520,6 +533,17 @@ def get_modifier_key(self) -> str:
"""Get the modifier key to use for opening stratagem menu."""
settings = self.get_settings()
return settings.get("modifier_key", DEFAULT_MODIFIER_KEY)

def get_direction_key_layout(self) -> str:
"""Get the key layout used for stratagem directions."""
settings = self.get_settings()
return normalize_direction_key_layout(
settings.get("direction_key_layout", DEFAULT_DIRECTION_KEY_LAYOUT)
)

def get_direction_key(self, direction: str) -> str:
"""Get the evdev key name mapped to a stratagem direction."""
return resolve_direction_key(direction, self.get_direction_key_layout())

def get_hold_modifier(self) -> bool:
"""Get whether to hold the modifier key during the sequence."""
Expand Down Expand Up @@ -549,6 +573,7 @@ def get_settings_area(self):
# Execution settings
group.add(self._create_key_delay_row())
group.add(self._create_modifier_key_row())
group.add(self._create_direction_key_layout_row())
group.add(self._create_hold_modifier_row())

# Display settings
Expand Down Expand Up @@ -621,6 +646,43 @@ def _on_modifier_key_changed(self, row, _, modifier_names):
name = modifier_names[selected]
key = MODIFIER_KEYS[name]
self._save_setting("modifier_key", key)

def _create_direction_key_layout_row(self) -> Adw.ComboRow:
"""Create the direction key layout combo row."""
row = Adw.ComboRow(
title="Direction Keys",
subtitle="Keys mapped to Up, Down, Left, and Right stratagem inputs"
)

string_list = Gtk.StringList()
layout_names = list(DIRECTION_KEY_LAYOUT_OPTIONS.keys())
for name in layout_names:
string_list.append(name)

row.set_model(string_list)

current_layout = self.get_direction_key_layout()
for i, name in enumerate(layout_names):
if DIRECTION_KEY_LAYOUT_OPTIONS[name] == current_layout:
row.set_selected(i)
break

row.connect(
"notify::selected",
self._on_direction_key_layout_changed,
layout_names,
)
return row

def _on_direction_key_layout_changed(self, row, _, layout_names):
"""Handle direction key layout changes."""
selected = row.get_selected()
if selected < len(layout_names):
name = layout_names[selected]
self._save_setting(
"direction_key_layout",
DIRECTION_KEY_LAYOUT_OPTIONS[name],
)

def _create_hold_modifier_row(self) -> Adw.SwitchRow:
"""Create the hold modifier switch row."""
Expand Down
36 changes: 36 additions & 0 deletions test_key_mapping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import unittest

from key_mapping import (
DEFAULT_DIRECTION_KEY_LAYOUT,
get_direction_key,
normalize_direction_key_layout,
)


class DirectionKeyMappingTests(unittest.TestCase):
def test_arrow_key_layout(self):
self.assertEqual(get_direction_key("UP", "arrow_keys"), "KEY_UP")
self.assertEqual(get_direction_key("DOWN", "arrow_keys"), "KEY_DOWN")
self.assertEqual(get_direction_key("LEFT", "arrow_keys"), "KEY_LEFT")
self.assertEqual(get_direction_key("RIGHT", "arrow_keys"), "KEY_RIGHT")

def test_wasd_key_layout(self):
self.assertEqual(get_direction_key("UP", "wasd"), "KEY_W")
self.assertEqual(get_direction_key("DOWN", "wasd"), "KEY_S")
self.assertEqual(get_direction_key("LEFT", "wasd"), "KEY_A")
self.assertEqual(get_direction_key("RIGHT", "wasd"), "KEY_D")

def test_unknown_layout_uses_backward_compatible_default(self):
self.assertEqual(
normalize_direction_key_layout("removed-layout"),
DEFAULT_DIRECTION_KEY_LAYOUT,
)
self.assertEqual(get_direction_key("UP", "removed-layout"), "KEY_UP")

def test_unknown_direction_is_rejected(self):
with self.assertRaisesRegex(ValueError, "Unsupported stratagem direction"):
get_direction_key("FORWARD", "wasd")


if __name__ == "__main__":
unittest.main()
18 changes: 13 additions & 5 deletions test_stratagems.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

Controls:
- Focus the window
- Press arrow keys (with or without modifier)
- Press arrow keys or W/A/S/D (with or without modifier)
- The tool will match sequences to stratagems
- Press Escape to clear the current sequence
"""
Expand All @@ -28,12 +28,20 @@
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
STRATAGEMS_PATH = os.path.join(SCRIPT_DIR, "assets", "data", "stratagems.json")

# Arrow key mappings
# Supported direction key mappings
KEY_TO_DIRECTION = {
Gdk.KEY_Up: "UP",
Gdk.KEY_Down: "DOWN",
Gdk.KEY_Left: "LEFT",
Gdk.KEY_Right: "RIGHT",
Gdk.KEY_w: "UP",
Gdk.KEY_W: "UP",
Gdk.KEY_s: "DOWN",
Gdk.KEY_S: "DOWN",
Gdk.KEY_a: "LEFT",
Gdk.KEY_A: "LEFT",
Gdk.KEY_d: "RIGHT",
Gdk.KEY_D: "RIGHT",
}

DIRECTION_DISPLAY = {
Expand Down Expand Up @@ -100,7 +108,7 @@ def build_ui(self):

# Instructions
instructions = Gtk.Label(
label="Press arrow keys to input a stratagem sequence.\n"
label="Press arrow keys or W/A/S/D to input a stratagem sequence.\n"
"Press Escape to clear."
)
instructions.set_wrap(True)
Expand Down Expand Up @@ -193,7 +201,7 @@ def on_key_pressed(self, controller, keyval, keycode, state):
self.update_modifier_display()
return True

# Check if it's an arrow key
# Check if it is a configured direction key
direction = KEY_TO_DIRECTION.get(keyval)
if direction:
# Check if modifier is currently held
Expand Down Expand Up @@ -351,7 +359,7 @@ def main():
sys.exit(1)

print(f"Loaded {len(stratagems)} stratagems")
print("Focus the window and press arrow keys to test.")
print("Focus the window and press arrow keys or W/A/S/D to test.")

app = ValidatorApp(stratagems)
app.run(sys.argv)
Expand Down