Задать вопрос
mamonblch
@mamonblch
Python, Clang

[Python + pywin32] Как эмулировать движения курсора, клики кнопками мыши и нажатия клавиш клавиатуры в игре WoT?

Всем привет! Возможно ответ на вопрос окажется до неприличного простым, а сам вопрос глупым, однако я с этим вожусь уже 4-ю неделю, и на данный момент прямых ответов не нашел.
Есть игра: WorldOfTanks (в простонародье и СНГ "Мир танков" с завязкой на Lesta GameCenter), нужно 3-мя методами: sendMove, sendClick, sendKey - реализовать движения курсора, клики кнопок мыши и нажатия клавиш клавиатуры, соответственно, внутри окна игры, используя Python + pywin32 (т.е. не затрагивая основной курсор машины).
Я пытался реализовать такой функционал через win32gui и win32api с методами SendMessage и PostMessage, заранее активируя окно и задавая ему общий фокус, передавая фокус клавиатурой и мышью (SetForegroundWindow, SetFocus, SetCursor) - ничего не работает. Хотя с другими программами такой подход работает "как по маслу".
Сейчас методы выглядят так:
# Подключение библиотек
import win32api
import win32gui
import win32con




# Эмуляция движения курсора
def sendMove(hwnd: int, pos: tuple) -> None:

    # Преобразование к LONG
    lParam: int = win32api.MAKELONG(pos[0], pos[1])

    # Реализация движения курсора
    win32gui.PostMessage(hwnd, win32con.WM_MOUSEMOVE, 0, lParam)



# Эмуляция клика внутри указанного окна
def sendClick(hwnd: int, pos: tuple, button: int) -> None:

    # Преобразование к LONG
    lParam: int = win32api.MAKELONG(pos[0], pos[1])

    # Реализация клика
    win32gui.PostMessage(hwnd, win32con.WM_LBUTTONDOWN, button, lParam)
    win32gui.PostMessage(hwnd, win32con.WM_LBUTTONUP, button, lParam)



# Эмуляция нажатия кнопки клавиатуры
def sendKey(hwnd: int, key: int) -> None:

    # Реализация нажатия кнопки
    win32gui.PostMessage(hwnd, win32con.WM_KEYDOWN, key, 0)
    win32gui.PostMessage(hwnd, win32con.WM_KEYUP, key, 0)




# "Начало начал"
if __name__ == "__main__":

    # Дескриптор окна
    hwnd: int = 3870292

    # Пресет "В бой"
    sendMove(hwnd, (500, 20))
    sendClick(hwnd, (500, 20), win32con.VK_LBUTTON)

    # Пресет "Закрыть игру"
    sendKey(hwnd, win32con.VK_ESCAPE)
    sendMove(hwnd, (300, 400))
    sendClick(hwnd, (300, 400), win32con.VK_LBUTTON)


Вывод Spy++ при "естественных" действиях:
679a4330916a1090360244.png

Вывод Spy++ при эмуляции действий:
679a429e36481462648310.jpeg

Тут либо я чего-то не понимаю, либо я чего-то не понимаю. В любом случае, с другими играми и программами все эти методы работают, но вот именно с этой - нет.
И такие же артефакты "невыполнения" случаются, когда используешь эти методы на программах, запущенных в "Sandboxie". Тот же AHK прекрасно выполняет данную задачу, но он использует основной курсор машины, и по определению не подходит, т.к. необходимо одновременно работать с множеством окон.
Не нужно говорить: "Это невозможно! Реализация только через виртуалки!" - нет, у нас есть рабочий софт, реализующий клик по координатам внутри окна игры, обернутого в Sandboxie. Но он собран в .exe и написан человеком давно ушедшим из проекта.
Буду рад любой помощи! Заранее спасибо!
  • Вопрос задан
  • 283 просмотра
Подписаться 1 Простой Комментировать
Решения вопроса 1
VoidVolker
@VoidVolker
Dark side eye. А у нас печеньки! А у вас?
Игры для работы с пользовательским вводом очень часто работают через графические API DirectX/OpenGL/Vulkan и прочее, при этом данные от системных вызовов типа оконного цикла, SendInput и прочее не используются. Сюрприз! В этом случае остаётся только три варианта:
  • Разбираться с этими API и выяснять как ему подсунуть нужные данные (не копал в эту сторону - так что без понятия возможно ли это или только ломать) (DirectInput, устаревшее)
  • Установить свой драйвер пользовательского ввода в ring0 (ядро ОС)
  • Использовать внешнее устройство, которое эмулирует физически клавиатуру и мышь и управляется через USB драйвером или прошивкой

А вот эмуляция пользовательского ввода в нескольких окнах возможна только с помощью виртуализации. Ибо все современные ОС поддерживают только одну клавиатуру и мышку для одного пользователя (всякие специфические утилиты/случаи не рассматриваем). SendMessage/PostMessage работает только с приложениями, которые получают пользовательский ввод через оконный цикл.
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
TrueBers
@TrueBers
Гуглю за еду
вожусь уже 4-ю неделю, и на данный момент прямых ответов не нашел.
Что можно было делать 4 недели, если ответ гуглится за 4 секунды? За 4 недели можно было игру дизассемблировать и посмотреть, как там всё устроено.

Способов обработки ввода как минимум несколько. На одном Window Message Loop Legacy Input, который ты насилуешь, свет клином не сошёлся. Он никогда не считался подходящим для игр и точного ввода, почему им пользуется огромное количество игр — загадка. Либо банальная человеческая лень.

Есть DirectInput, есть XInput, есть window message loop input events, есть RawInput, и GameInput. Первые два давно уже устаревшими помечены, не развиваются с 2000-х и не рекомендуются к использованию в современных играх. Устаревшие ивенты из window message loop, которые ты пытаешься посылать в окно, никогда не рекомендовались для реалтаймовых игр, где важна скорость реакции и точность наведения курсора. Остаётся RawInput. Ну а GameInput — слишком новая, высокоуровневая и специфическая штука, чтобы её часто использовали. Более того, все эти *Input базируются на низком уровне как раз на RawInput.

Обрати внимание на ивенты, которые приходят перед нужными тебе. WM_INPUT, вот он как раз относится к RawInput. Его то и обрабатывает приложение. Сначала приходит WM_INPUT, а потом за ним старое сообщение просто для совместимости с древними хендлерами. Приходят старые сообщения потому, что разработчики не отключили их генерацию флагом RIDEV_NOLEGACY, либо есть какая-то логика отката к старому обработчику.
И самое главное, WM_INPUT генерируется HID-железкой внутри самого системного хендлера событий. То есть самому послать это сообщение от лица пользователя невозможно.

Эмулировать RawInput можно только аппаратно/драйвером/гипервизором, либо внедрившись в код игры. Придётся реверсить игру и разбираться в логике обработки сообщений. Возможно, можно как-то заабьюзить логику, которая проверяет, будет ли обрабатывать сырой ввод, и вдруг есть какое-то условие или флаг из конфига, при котором произойдёт fallback, который сбросит обработку WM_INPUT на старые ивенты.

PS. С ответа про обработку ввода через графические API поржал :D. Нормальная шутка, смешная.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы