Files
my-botty/test/test_key_detector.py
alex 0af8a54116 feat: auto-detect D2R key bindings from .key/.keyo file
- Read character .key file from Saved Games / D2R install dir / APPDATA
- Auto-fill empty hotkeys in [char] section (inventory, belt, potions, etc.)
- Match skill slots to configured skills for validation
- Normalize key variants (left alt ~ alt, left shift ~ shift)
- params.ini values always take priority over detected bindings
- Warn on genuine mismatches between config and .key file
2026-05-19 16:46:24 +02:00

180 lines
6.2 KiB
Python

#!/usr/bin/env python
"""
End-to-end test for key_detector without needing a real D2R install.
Creates a temporary .key file, loads config with it, and verifies auto-fill.
"""
import os
import sys
import tempfile
import shutil
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
# ─── Mock .key file ───────────────────────────────────────────────────────────
# 17 bindings total. Each line: VK action_type param
MOCK_KEY_CONTENT = """\
17
73 2 0
256 3 0
16 4 0
75 5 0
49 6 1
50 6 2
51 6 3
52 6 4
53 1 1
54 1 2
115 1 3
116 1 4
117 1 5
118 1 6
69 13 0
87 1 7
114 1 8
"""
def test():
print("=" * 50)
print(" KEY DETECTOR E2E TEST")
print("=" * 50)
tmpdir = tempfile.mkdtemp()
key_path = os.path.join(tmpdir, "testchar.key")
with open(key_path, "w") as f:
f.write(MOCK_KEY_CONTENT)
print(f"\n1. Created mock .key file: {key_path}")
# ─── 2. Parse ──────────────────────────────────────────────────────
from utils.key_detector import (
parse_key_file, vk_to_name, _find_key_file,
_try_fill, _auto_fill_char,
)
bindings = parse_key_file(key_path)
print(f"\n2. Parsed {len(bindings)} bindings:")
for k, v in sorted(bindings.items()):
print(f" {k:20s} = {v}")
# ─── 3. Verify expected values ─────────────────────────────────────
expected = {
"inventory": "i", # VK 73
"show_items": "alt", # VK 256
"stand_still": "capslock", # VK 16
"show_belt": "k", # VK 75
"potion1": "1", # VK 49
"potion2": "2", # VK 50
"potion3": "3", # VK 51
"potion4": "4", # VK 52
"skill1": "5", # VK 53
"skill2": "6", # VK 54
"skill3": "f4", # VK 115
"skill4": "f5", # VK 116
"skill5": "f6", # VK 117
"skill6": "f7", # VK 118
"force_move": "e", # VK 69
"skill7": "w", # VK 87
"skill8": "f3", # VK 114 (unused slot)
}
print("\n3. Checking expected values:")
fail = 0
for key, exp_val in sorted(expected.items()):
actual = bindings.get(key, "<missing>")
if actual != exp_val:
fail += 1
print(f" {key:20s}: {actual:>10s} (expected {exp_val:>10s}) [FAIL]")
else:
print(f" {key:20s}: {actual:>10s} [OK]")
# ─── 4. File discovery ─────────────────────────────────────────────
print("\n4. File discovery (d2r_path = tmpdir):")
found = _find_key_file(tmpdir, "testchar")
if found == key_path:
print(f" .key found: [OK]")
else:
print(f" .key found: {found} [FAIL]")
fail += 1
# .keyo
keyo_path = os.path.join(tmpdir, "testchar.keyo")
shutil.copy2(key_path, keyo_path)
found_keyo = _find_key_file(tmpdir, "testchar")
# .key takes priority over .keyo in the search order
if found_keyo and ("testchar.key" in found_keyo or "testchar.keyo" in found_keyo):
print(f" .keyo found: [OK]")
else:
print(f" .keyo found: {found_keyo} [FAIL]")
fail += 1
# ─── 5. _try_fill ──────────────────────────────────────────────────
print("\n5. _try_fill logic:")
cfg = {"show_belt": "", "inventory_screen": "", "teleport": "5"}
_try_fill(cfg, "show_belt", "k")
assert cfg["show_belt"] == "k", f"Got {cfg['show_belt']!r}"
print(" Fill empty: [OK]")
_try_fill(cfg, "teleport", "6")
assert cfg["teleport"] == "5", f"Got {cfg['teleport']!r}"
print(" Preserve existing: [OK]")
# ─── 5b. Normalization: left alt ~ alt ─────────────────────────────
print("\n5b. Key normalization:")
cfg2 = {"show_items": "alt"}
_try_fill(cfg2, "show_items", "left alt")
assert cfg2["show_items"] == "alt", f"Got {cfg2['show_items']!r}"
print(" left alt ~ alt: [OK]")
cfg3 = {"stand_still": "shift"}
_try_fill(cfg3, "stand_still", "left shift")
assert cfg3["stand_still"] == "shift", f"Got {cfg3['stand_still']!r}"
print(" left shift ~ shift: [OK]")
# ─── 6. _auto_fill_char ────────────────────────────────────────────
print("\n6. _auto_fill_char:")
char_cfg = {
"inventory_screen": "",
"show_items": "",
"stand_still": "capslock",
"show_belt": "k",
"force_move": "",
"potion1": "", "potion2": "", "potion3": "", "potion4": "",
}
_auto_fill_char(char_cfg, bindings)
checks = [
("inventory_screen", "i"),
("show_items", "alt"),
("force_move", "e"),
("potion1", "1"),
("potion2", "2"),
("potion3", "3"),
("potion4", "4"),
]
for key, exp in checks:
actual = char_cfg[key]
if actual != exp:
fail += 1
print(f" {key:20s}: {actual:>10s} (expected {exp:>10s}) [FAIL]")
else:
print(f" {key:20s}: {actual:>10s} [OK]")
shutil.rmtree(tmpdir)
# ─── Summary ───────────────────────────────────────────────────────
print(f"\n{'=' * 50}")
if fail:
print(f" {fail} FAIL(S)")
print(f"{'=' * 50}")
return False
print(" ALL TESTS PASSED")
print(f"{'=' * 50}")
return True
if __name__ == "__main__":
success = test()
sys.exit(0 if success else 1)