CASE STUDIESProjekt własny

Tibia Key Presser: klawisze do gry wciskane w tle

Małe narzędzie w Pythonie, które wciska hotkeye w Tibii, gdy okno gry jest zminimalizowane. Dwa lata później, przy kolejnym podejściu do projektu, wyszły pętla bez sleep, wyścig przy Stop → Start i współbieżne wysyłanie klawiszy na jednym oknie.

Kiedy
2024–2026
Rola
Autor: aplikacja, build, rozwój
Stack
PythonTkinterpywinautoPyInstallerpytest
  • 8

    klawiszy, każdy z własnym opóźnieniem

  • 100 ms

    maksymalny czas reakcji na Stop

  • 780

    linii Pythona w wersji wyjściowej

Problem

Tibia ma powtarzalne akcje pod hotkeyami: leczenie, mana, runy. Chciałem, żeby program wciskał je sam w stałym rytmie, także gdy okno gry jest zminimalizowane, a ja robię coś innego. Symulowana klawiatura idzie do aktywnego okna, więc trzeba trafić w konkretne okno gry, a nie w to, które akurat ma fokus.

Pierwsza wersja z września 2024 działała, ale miała błędy, które zobaczyłem dopiero po powrocie do projektu w październiku 2026. Tkinter zgłasza nazwy klawiszy (Return, space, comma), których pywinauto nie zna; nieznany klawisz rzucał wyjątek, kod traktował go jak utratę okna, łączył się ponownie i próbował znowu. Pętla bez żadnego sleep, z tracebackiem co iterację i bez sygnału w interfejsie. Stop i Start w odstępie poniżej 100 ms wskrzeszały stare wątki, bo zdarzenie stop było jedno na cały czas życia aplikacji. Kolory, limity i ścieżki żyły w trzech miejscach naraz.

Podejście

Wysyłka idzie przez pywinauto do uchwytu okna znalezionego po tytule „<klient> - <postać>” (wyrażenie regularne, opcjonalny clients.txt dla innych serwerów), a nie do aktywnego okna. Każdy klawisz ma własny wątek z absolutnym terminem następnego wciśnięcia, więc narzut wyszukiwania okna i wysyłki nie kumuluje się w dryf. Sen jest cięty na odcinki po 100 ms, żeby Stop reagował natychmiast, a uchwyt okna jest sprawdzany co 5 sekund.

Zanim ruszyłem z kodem, spisałem analizę: błędy z wagą, duplikacje, martwy kod, plan zmian etapami i pytania do decyzji. Zachowanie pywinauto sprawdziłem w źródłach wersji 0.6.9 zamiast zakładać, co robi.

Tibia Key Presser w stanie spoczynku: wiersze klawisz i opóźnienie, przycisk Start
Stan spoczynku: białe pole czeka na kliknięcie i klawisz do powiązania.

Rozwiązanie

  • Moduł keys.py: czysta funkcja tłumacząca nazwy klawiszy tkintera na kody pywinauto (Return na ENTER, Prior na PGUP, interpunkcja na znaki, cały numpad), a same modyfikatory nie wiążą się, więc Shift+F1 wiąże F1. Walidacja w chwili bindowania: nieobsługiwany klawisz to czerwone pole i komunikat, bez możliwości startu. Sześć testów w pytest.
  • Pętla wysyłki odczekuje interwał po nieudanej wysyłce, więc trwały błąd nie zamienia się w busy loop.
  • Jedno źródło prawdy: theme.py dla kolorów, fontów i stylów ttk, paths.py dla ścieżek w trybie źródłowym i w pliku .exe, więc ikona przestała zależeć od bieżącego katalogu.
  • Narzędzia: pyproject z uv, ruff i pytest, build do pojedynczego .exe przez PyInstaller, clients.txt dla serwerów OTS.
  • Zaplanowane, jeszcze nie wdrożone: jeden wątek-scheduler zamiast wątku na klawisz. W źródłach pywinauto wysyłka to SetKeyboardState i AttachThreadInput na współdzielonej kolejce wejścia, bez żadnej blokady, więc dwa wątki z pokrywającymi się terminami mogą sobie nadpisać stan klawiatury. Zserializowane wątki mają tę samą przepustowość co jeden scheduler, a Stop staje się set i join.

Efekt

Do ośmiu klawiszy z niezależnymi opóźnieniami, działa przy zminimalizowanej grze, zatrzymuje się w około 100 ms i pokazuje nazwę postaci w tytule okna. Jeden plik .exe bez instalacji.

Tibia Key Presser w trakcie pracy: zielone pola klawiszy i przycisk Stop
W trakcie pracy: zielony błysk pola oznacza klawisz właśnie wysłany do gry.

Czego się nauczyłem

  • Wyjątek łapany jako coś innego (nieznany klawisz jako utrata okna) tworzy pętle, których interfejs nie widzi. Każda gałąź błędu potrzebuje własnej reakcji i własnego odczekania.
  • Powrót do własnego kodu po dwóch latach zaczyna się od spisanej analizy z wagami i planem. Zmiany robione przy okazji kosztują więcej.

© 2026 SKOFTWARE Maciej Skorus

Polityka Cookies