Это вторая часть поста на тему создания несложного бота для игры World of Warcraft. С первой частью можно ознакомиться здесь. Сегодня мы поговорим о
![](https://habrastorage.org/r/w780q1/storage2/885/6b7/ecc/8856b7ecc37610d90d47f1e872772437.jpg)
Наша задача: чтение цветов пикселей, определение нажатий клавиш, фоновая работа с возможностью приостановки, посылка кликов в приложение. Здесь как нельзя лучше подойдет язык AutoIt. Чтобы написать то же самое на Си, пришлось бы заморачиваться с хендлами, девайсами, хуками, событиями Windows… Причем, для каждого из этих действий надо почитать справку, выяснить способ, определиться со структурами, типами, апишками.
Для редактирования скрипта и быстрого запуска я использовал SciTE-Lite, который включает Highlighting, CodeFolding, Autocomplete, Intellisense, ну и SyntaxCheck. Встроенная справка по языку в подарок. Стандартные фишечки.
В каждом скрипте, работающем с вашим рабочим столоми могущим захватить над ним контроль, советую использовать пару обработчиков горячих клавиш
В противном случае вы рискуете впасть в состояние наблюдателя без возможности управлять своей системой. Судный день.
Отмечу, что на F12 и некоторые другие кнопки обработчик повесить нельзя. Я какое-то время не мог понять, почему он не вызывался.
Напомню, что в аддоне мы клали в цветовые компоненты пикселей числа с плавающей запятой, а приезжают они к нам уже в виде целых байтов:
Считав данные, мы производим с ними обратные преобразования. Функции возвращают массивы и массивы записываются в переменные абсолютно прозрачно для программиста.
Читал я где-то, что автор языка AutoIt не хотел, чтобы такой простой и мощный язык использовался злоумышленниками для написания вредоносных программ. Поэтому он убрал возможность создать обработчик нажатия для всех клавиш сразу, чтобы хотя бы Keylogger'ы не клепали.
Что я могу сказать. AutoIt весьма активно используется для написания вредоносов, а перехват нажатия нужных клавиш мы будем делать так:
Таким образом, нам не нужно изобретать свою систему наименования клавиш для сохранения их в файл, достаточно использовать принятую в AutoIT
Если нам понадобится перехватить что-то еще (а при игре мы заранее знаем, что мы будем давить и какими заклинаниями пользоваться), мы просто добавим их в длинный список в начале.
Перехватив нажатие клавиши, нам надо послать его дальше в приложение, для этого мы временно убираем свой обработчик
Такой подход рекомендуется в справке, но на практике при нажатии на комбинации с Alt и другие ощутимо глючит. Я не стал разбираться, а просто избегал использования таких комбинаций.
В AutoIt нет штатного способа перехвата нажатий кнопок мыши. Есть сторонний модуль с примером использования. Или вы можете повесить свой SetWindowsHookEx (WH_MOUSE_LL). Познавательный пример использования WinAPI Callback-функций на AutoIt тут. Но я не использовал такой подход по двум причинам:
Поэтому, как вы уже заметили, я всего лишь использовал кнопки "[" и "]" и давил их по мере необходимости. Главное, не забывать нажимать их.
Ну и, конечно же, наш Recorder должен в фоне записывать передвижение персонажа
Вы можете спросить, зачем так часто (10 раз в секунду) записывать координаты? Дело в том, что на нашем маршруте навалено препятствий: ящиков, углов, дверных проемов, фонарных столбов,граблей. А персонаж просто «магнитится» к ним. Если где-то, пробегая мимо, он может застрять, он обязательно сделает это. Даже если вы длительное время бежите по прямой, вспомните, азимут движения был задан не идеально, поэтому, возможно, вы давно бежите, упираясь лбом в стенку.
Вместе с SciTE4AutoIt3 поставляется AutoIt3\Extras\Au3Record\Au3Record.exe, который позволяет записывать ваши действия с рабочим столом и оформляет это сразу в виде AutoIt-скрипта. Если вам предстоит много раз совершать последовательность машинальных действий без необходимости думать (например, при установке ПО или патчей на большой парк компьютеров), присмотритесь к этому инструменту.
Напомню, что результатом работы Recorder'а, описанного в предыдущей главе, является набор команд вида
На первый взгляд кажется, что «проиграть» их не составит никакого труда, но давайте детально рассмотрим этот процесс.
Поворот и наклон при помощи нажатия на кнопки дает точность порядка 30 градусов, что неприемлемо для нашей задачи. Поэтому мы будем использовать для этого вторую возможность: движение мыши с зажатой правой кнопкой. Алгоритм следующий:
Как нам попроще определить, в какую сторону выгоднее поворачиваться, если у нас есть два угла: текущий и требуемый?
![](https://habrastorage.org/r/w1560/storage2/cbc/ae5/d42/cbcae5d42f62af52e64ee84cfddc6c3e.png)
Можно посмотреть на разницу углов, если она положительна — направо. Но рассмотрим случай: текущий угол — 30 градусов, требуемый — 330 градусов. Разница отрицательна, но нам все равно направо. А еще углы могут быть отрицательными. Чтобы не приходилось выписывать все эти условия, просто воспользуемся синусом разницы углов и будем смотреть только на его знак.
Переменные $want и $current содержат (x, y, azimuth)
Синус же позволяет нам регулировать скорость поворота. Чем меньше угол, тем аккуратнее будет двигаться наш курсор и тем точнее мы повернемся. И наоборот, на большие углы нам хочется поворачиваться побыстрее.
Функция изменения наклона к горизонту полностью аналогична, только сдвигать курсор надо по вертикали.
В приведенном примере кода, есть логическая ошибка, которая может привести к неправильному поведению бота. Предлагаю любопытному читателю самому ее найти.
На движение накладываются следующие требования
Если вы не любите математику или закончили школу очень давно, вы можете пропустить этот раздел без ущерба для понимания. Но ничего особо сложного тут нет. Пусть персонаж стоит на (ax, ay) и смотрит под углом
и ему надо попасть в (bx, by), ему бежать вперед или назад? Для начала слегка перефразируем задачу: пусть персонаж стоит в (0, 0), смотрит в
, а надо ему в (dx, dy).
![](http://habrastorage.org/r/w1560/storage2/3ae/1ec/e0f/3ae1ece0f14436a608e435652af60432.png)
Если решать задачу в лоб, то надо бы вычислить косинус угла между векторами. Если он положительный, то угол острый и бежать вперед. Если отрицательный — тупой, надо пятиться. Косинус можно вычислять по теореме косинусов
![a^2=b^2+c^2-2bc\cdot cos\theta](http://habrastorage.org/getpro/habr/post_images/550/9af/e98/5509afe98797a4769ee0eae7e5dc1bc8.gif)
Но для этого нам придется вычислять длины каждого из векторов, извлекать корни, очень громоздко.
Есть так же формула скалярного произведения
![\langle a,b\rangle=|a||b|cos\theta](http://habrastorage.org/getpro/habr/post_images/450/2a2/5bd/4502a25bd2bb5ef17463b3895de1b2c8.gif)
Которое, как известно, в декартовой системе координат может быть вычислено по формуле
![\langle a,b\rangle=a_{x}b_{x}+a_{y}b_{y}](http://habrastorage.org/getpro/habr/post_images/3e8/66a/9c5/3e866a9c5eca1fdc853e46c9bb6536d7.gif)
Таким образом, вычислив скалярное произведение и посмотрев на его знак, мы сможем определить, бежать нам вперед или назад. А в нашем случае вычислять мы его будем по формуле
. Более того, это же самое скалярное произведение показывает нам, сколько именно надо бежать вперед или назад по своему геометрическому определению (проекция одного вектора на другой, помноженная на длину второго).
![image](http://habrastorage.org/r/w1560/getpro/habr/post_images/d42/fb3/e99/d42fb3e9914b179707d290ae79e67c33.png)
Аналогичным образом, синус угла между векторами взгляда
и направления до цели (dx, dy) определяет, в какой бок идти. Если положительный — цель по правую руку, и наоборот. Тут нам поможет векторное произведение, а если быть более точным, то псевдоскалярное произведение, вычисляемое в декартовых координатах по формуле
![a\Lambda b=|a||b|sin\theta=a_x b_y-a_y b_x](http://habrastorage.org/getpro/habr/post_images/5ab/88d/eaa/5ab88deaa42ff6fd92ab52e0e5f39a9e.gif)
Опять-таки величина этого числа определяет, насколько много надо сделать шагов вбок.
Запрограммируем наши рассуждения
Пусть вас не смущает, что в программе синус и косинус поменяны местами и взяты с противоположным знаком. Просто в WoW азимут отсчитывается от севера против часовой стрелки. Эти детали можно просто «попробовать» в реальной программе. И, если вышло наоборот, поиграться со знаками.
Функция GetDirection() возвращает массив из двух значений: сколько идти вперед/назад, сколько идти вбок.
Некоторые читатели могут воскликнуть: «Да ведь эти умножения на синусы-косинусы — ничто иное, как обыкновенный поворот системы координат,
![](http://habrastorage.org/storage2/c27/741/0c2/c277410c20a18c528f8c512c7df61788.gif)
описываемый
![image](http://habrastorage.org/r/w1560/getpro/habr/post_images/ea6/600/9d1/ea66009d156eb28245603fdd9ab32fd5.png)
Зачем морочить нам головы.»
Верно, но тогда мне было бы сложнее пояснить, почему матрица именно такая.
Теперь мы готовы давить кнопки бега
Обратите внимание, мы идем в том направлении, в котором идти дальше всего, и заканчиваем путь, когда мы почти у цели. Это позволяет персонажу «не мельтешить». Кнопка движения вперед не отпускается по достижении точки назначения. Поэтому поворачиваться на требуемый угол нам надо сравнительно быстро.
Код, отвечающий за проигрывание файлов команд, не представляет из себя ничего интересного, и при желании вы можете ознакомиться с ним самостоятельно.
![ED-209 from Robocop falls down thе stairs](http://habrastorage.org/r/w780q1/storage2/64a/4b3/cc6/64a4b3cc69f274f5f8c91eaad5861852.jpg)
Вот, казалось бы, и все. Пробежим по маршруту, запишем координаты, нажатия клавиш и клики, и включим воспроизведение. Но не тут-то было. Здесь начинается самое сложное.
В подземелье живут мобы, которые хоть и не представляют угрозы для жизни нашего персонажа, но норовят оглушить, кинуть «молчанку», отбежать в сторонку как раз тогда, когда вы бы хотели их убить, и бросаются на амбразуру в тот момент, когда вы думали взять в цель совсем другого моба. Иногда ваша цель забегает за спину, иногда — умирает за один удар, хотя должна жить три. В общем, их хаотичное поведение вносит заметный элемент случайности в наш маршрут, и в итоге персонаж может оказаться у запертой двери в самом конце маршрута просто потому, что не смог убить кого-то в самом начале.
Да, можно было написать внутриигровые макросы, которые бы брали в цель нужного моба, и тем самым избавиться от некоторых проблем, но мне хотелось оставить бота как можно более универсальным.
Двери-решетки. На них нужно кликнуть, чтобы открыть. А так как они решетки, то кликнуть вы можете случайно аккуратно в дырку. И персонаж будет биться лбом об дверь в попытке пробежать дальше.
Таким образом, записав маршрут, вы открываете его в блокноте и отлаживаете. Вы наблюдаете за персонажем и при проблемах или возможных проблемах «вбиваете костыли» в маршрут: вставляете дополнительное заклинание для верности (вдруг моб выживет), добегаете до той точки, в которой моб будет точно виден и досягаем независимо от его желания прогуляться. Вы изменяете маршрут, чтобы уж точно не споткнуться об этот злосчастный ящик. После 10 забегов я пришел к выводу, что лучше бы я просто переубивал там всех пауков, чем они потом смогли бы отомстить мне в одном случае из 10 своим внезапным заклинанием. Но, когда персонаж уже бегает сам, вам уже не хочется бежать самому ножками, вам хочется просто смотреть на это.
Я понял, каково это, запускать марсоход, программировать робот-пылесос, идти в полной темноте по квартире или учить роботов играть в футбол. Надо все делать надежно, с запасом. На марсе ямы будут везде, а еще будут нависающие скалы и пещеры. В квартире роботу-пылесосу будут противостоять провода и тапочки повсюду, а еще будет кот, который захочет поиграть. В полной темноте будут углы, ножки стульев, и даже упавшие на пол ножи. Только на ощупь!
Признаюсь, выполнил я заявленную в прошлой части лишь программу минимум. Бот все еще может не выполнить задачу с вероятностью 1%. Так что оставлять его без контроля на ночь нельзя, застрянет.
![](http://habrastorage.org/r/w780q1/storage2/4d9/78f/4c9/4d978f4c9aa489062a3e5aa1f10af06c.jpg)
Люди, далекие от MMORPG игр, спросят: «А зачем вообще противодействовать ботам? Ведь автоматизация везде приветствуется.» А вот и нет. Если все платят одинаково, то и возможности должны быть равными. Иначе ущемленная часть обижается и перестает играть. Если в игре доступны, например, макросы, то они должны быть понятны и не программистам. Поэтому разработчик игры запрещает использование средств автоматизации на уровне лицензионного соглашения и следит.
Я нередко встречал возгласы, что Blizzard (это разработчик WoW) плохо следит, и боты повсюду. Во-первых, сам я так не считаю, думаю, их доля преувеличена. А во-вторых, давайте обсудим, что же есть в арсенале разработчика. Как не только распознать бота, но еще и иметь подтверждающие факты. Ведь на основе подозрений невежливо банить игрока.
Например, гейм-мастер (внутриигровая поддержка) способен наблюдать за игроком и видеть его глазами. Но даже, увидев явно «нечеловеческое» поведение, как то: длительное вращение на месте, попытки пройти сквозь стену, беспрерывная игра в течение суток (обычно сбор ресурсов), игнорирование попыток общения других игроков, — все это не дает гарантий использования ботов. Быть может, игрок просто заснул на клавиатуре, очень целеустремлен, или просто не хочет общаться, когда «работает».
Поэтому у Blizzard есть Warden. ПО по сути напоминает облачный антивирус:
Есть данные, что игроков банили за запуск игры из-под Wine. Blizzard не исключает возможность бана за использование программируемых клавиатур и мышек.
Давайте подумаем, что же надо делать, чтобы как можно быстрее привлечь к себе внимание и попасть под подозрение:
И вот тогда рано или поздно придет ban-wave для всех пользователей этого ПО.
Что же касается посылки нажатия клавиш и языка AutoIt, то по всей видимости, его запущенный интерпретатор слишком распространен среди игроков и используется также для «мирных» целей. В итоге отличить ботоводов от офисных автоматизаторов труда Blizzard не могут, ну, или не хотят.
Сегодня мы описали:
Больше всего времени при написании бота заняло, конечно же, написание самого поста. Бот сделал забегов 20 (игра запрещает делать больше 5 в час). Пока он бегает, компьютер занят, а мне надо пост писать. Конь пока не выпал. Но если выпадет, я обрадуюсь не меньше, ведь я получу его хоть и не фармом, но все-таки трудом.
Голосуйте, пишите комментарии, не ругайте за спорную тему. Удачи.
- написании Recorder'а клавиш и координат на языке AutoIt
- написании Player'a инструкций для бота
- математике 2D, как ориентироваться в декартовой системе координат без теоремы косинусов
- управлении роботом при недостаточном количестве датчиков
- мерах противодействия ботам
![](https://habrastorage.org/storage2/885/6b7/ecc/8856b7ecc37610d90d47f1e872772437.jpg)
Recorder
Наша задача: чтение цветов пикселей, определение нажатий клавиш, фоновая работа с возможностью приостановки, посылка кликов в приложение. Здесь как нельзя лучше подойдет язык AutoIt. Чтобы написать то же самое на Си, пришлось бы заморачиваться с хендлами, девайсами, хуками, событиями Windows… Причем, для каждого из этих действий надо почитать справку, выяснить способ, определиться со структурами, типами, апишками.
Для редактирования скрипта и быстрого запуска я использовал SciTE-Lite, который включает Highlighting, CodeFolding, Autocomplete, Intellisense, ну и SyntaxCheck. Встроенная справка по языку в подарок. Стандартные фишечки.
Горячие клавиши
В каждом скрипте, работающем с вашим рабочим столом
$paused = false
HotKeySet("{F11}", "Pause")
HotKeySet("{F10}", "Kill")
Func Pause()
$Paused = NOT $Paused
While $Paused
Sleep(100)
WEnd
EndFunc
Func Kill()
FileClose($hfile)
Exit
EndFunc
В противном случае вы рискуете впасть в состояние наблюдателя без возможности управлять своей системой. Судный день.
Отмечу, что на F12 и некоторые другие кнопки обработчик повесить нельзя. Я какое-то время не мог понять, почему он не вызывался.
Получение координат
Напомню, что в аддоне мы клали в цветовые компоненты пикселей числа с плавающей запятой, а приезжают они к нам уже в виде целых байтов:
#include <Color.au3>
Opt("PixelCoordMode", 2) ;Отсчет координат пикселей от левого верхнего угла клиентской части окна
Opt("MouseCoordMode", 2) ;Отсчет координат мыши от левого верхнего угла клиентской части окна
$WinName = "World of Warcraft"
$hwnd = WinGetHandle($WinName)
Func GetPitch()
$pixel2 = _ColorGetRGB(PixelGetColor(10, 0, $hwnd));
Return ($pixel2[2]/255.0-0.5)*4
EndFunc
Func GetPos()
$pixel1 = _ColorGetRGB(PixelGetColor(0, 0, $hwnd));
$pixel2 = _ColorGetRGB(PixelGetColor(10, 0, $hwnd));
$result[3] = [ ($pixel1[0]+$pixel1[1]/255.0)/255*100, ($pixel2[0]+$pixel2[1]/255.0)/255*100, $pixel1[2]*7.0/255 ]
return $result
EndFunc
Считав данные, мы производим с ними обратные преобразования. Функции возвращают массивы и массивы записываются в переменные абсолютно прозрачно для программиста.
Логирование клавиш
Читал я где-то, что автор языка AutoIt не хотел, чтобы такой простой и мощный язык использовался злоумышленниками для написания вредоносных программ. Поэтому он убрал возможность создать обработчик нажатия для всех клавиш сразу, чтобы хотя бы Keylogger'ы не клепали.
Что я могу сказать. AutoIt весьма активно используется для написания вредоносов, а перехват нажатия нужных клавиш мы будем делать так:
local $keys = StringSplit("` [ ] 1 2 3 4 5 6 7 8 9 0 !1 !2 !3 !4 !5 !6 !7 !8 !9 !0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +f {SPACE} {TAB} {ESC} {PAUSE} {DELETE} {BACKSPACE}", " ", 2)
for $i = 0 to UBound($keys)-1
HotKeySet($keys[$i], "OnHotKey")
Next
Func OnHotKey()
;ToolTip(@HotKeyPressed)
HotKeySet(@HotKeyPressed)
Send(@HotKeyPressed)
HotKeySet(@HotKeyPressed, "OnHotKey")
Switch @HotKeyPressed
Case "["
FileWriteLine($hfile, "mouse left " & MouseGetPos(0) & " " & MouseGetPos(1))
Case "]"
FileWriteLine($hfile, "mouse right " & MouseGetPos(0) & " " & MouseGetPos(1))
Case "{PAUSE}"
FileWriteLine($hfile, "pause 1000")
Case "{BACKSPACE}"
FileWriteLine($hfile, StringFormat("pitch %.2f", GetPitch()))
Case Else
FileWriteLine($hfile, "key " & @HotKeyPressed)
EndSwitch
EndFunc
Таким образом, нам не нужно изобретать свою систему наименования клавиш для сохранения их в файл, достаточно использовать принятую в AutoIT
Если нам понадобится перехватить что-то еще (а при игре мы заранее знаем, что мы будем давить и какими заклинаниями пользоваться), мы просто добавим их в длинный список в начале.
Перехватив нажатие клавиши, нам надо послать его дальше в приложение, для этого мы временно убираем свой обработчик
HotKeySet(@HotKeyPressed)
Send(@HotKeyPressed)
HotKeySet(@HotKeyPressed, "OnHotKey")
Такой подход рекомендуется в справке, но на практике при нажатии на комбинации с Alt и другие ощутимо глючит. Я не стал разбираться, а просто избегал использования таких комбинаций.
Логирование мыши
В AutoIt нет штатного способа перехвата нажатий кнопок мыши. Есть сторонний модуль с примером использования. Или вы можете повесить свой SetWindowsHookEx (WH_MOUSE_LL). Познавательный пример использования WinAPI Callback-функций на AutoIt тут. Но я не использовал такой подход по двум причинам:
- Передвижение персонажа сопряжено с большим количеством нажатий мыши, которые логировать не надо. Плюс, возможны случайные нажатия. Пришлось бы писать логику по отделению мух от котлет.
- Использование Hook'ов повышает шансы привлечения к вам внимания «спецслужб». Об этом подробнее в разделе «Противодействие ботам».
Поэтому, как вы уже заметили, я всего лишь использовал кнопки "[" и "]" и давил их по мере необходимости. Главное, не забывать нажимать их.
Запись координат
Ну и, конечно же, наш Recorder должен в фоне записывать передвижение персонажа
$hfile = FileOpen("output.txt", 1)
$prev = ""
While true
WinWaitActive($hwnd)
local $pos = GetPos()
$command = StringFormat("move %.3f %.3f %.3f", $pos[0], $pos[1], $pos[2]);
if $pos[0] + $pos[1] > 0 And $command <> $prev Then
FileWriteLine($hfile, $command)
EndIf
$prev = $command
Sleep(100)
WEnd
Вы можете спросить, зачем так часто (10 раз в секунду) записывать координаты? Дело в том, что на нашем маршруте навалено препятствий: ящиков, углов, дверных проемов, фонарных столбов,
Полный исходный код Recorder'а
#include <Color.au3>
Global $WinName = "World of Warcraft"
Opt("PixelCoordMode", 2) ;Отсчет координат пикселей от левого верхнего угла клиентской части окна
Opt("MouseCoordMode", 2) ;Отсчет координат мыши от левого верхнего угла клиентской части окна
$paused = false
HotKeySet("{F11}", "Pause")
HotKeySet("{F10}", "Kill")
local $keys = StringSplit("` [ ] 1 2 3 4 5 6 7 8 9 0 !1 !2 !3 !4 !5 !6 !7 !8 !9 !0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +0 +f {SPACE} {TAB} {ESC} {PAUSE} {DELETE} {BACKSPACE}", " ", 2)
for $i = 0 to UBound($keys)-1
HotKeySet($keys[$i], "OnHotKey")
Next
$hwnd = WinGetHandle($WinName)
$hfile = FileOpen("output.txt", 1)
$prev = ""
While true
WinWaitActive($hwnd)
local $pos = GetPos()
$command = StringFormat("move %.3f %.3f %.3f", $pos[0], $pos[1], $pos[2]);
if $pos[0] + $pos[1] > 0 And $command <> $prev Then
FileWriteLine($hfile, $command)
EndIf
$prev = $command
Sleep(100)
WEnd
Func Pause()
$Paused = NOT $Paused
While $Paused
Sleep(100)
WEnd
EndFunc
Func Kill()
FileClose($hfile)
Exit
EndFunc
Func GetPitch()
$pixel2 = _ColorGetRGB(PixelGetColor(10, 0, $hwnd));
Return ($pixel2[2]/255.0-0.5)*4
EndFunc
Func GetPos()
$pixel1 = _ColorGetRGB(PixelGetColor(0, 0, $hwnd));
$pixel2 = _ColorGetRGB(PixelGetColor(10, 0, $hwnd));
local $result[3] = [ ($pixel1[0]+$pixel1[1]/255.0)/255*100, ($pixel2[0]+$pixel2[1]/255.0)/255*100, $pixel1[2]*7.0/255 ]
return $result
EndFunc
Func OnHotKey()
;ToolTip(@HotKeyPressed)
HotKeySet(@HotKeyPressed)
Send(@HotKeyPressed)
HotKeySet(@HotKeyPressed, "OnHotKey")
Switch @HotKeyPressed
Case "["
FileWriteLine($hfile, "mouse left " & MouseGetPos(0) & " " & MouseGetPos(1))
Case "]"
FileWriteLine($hfile, "mouse right " & MouseGetPos(0) & " " & MouseGetPos(1))
Case "{PAUSE}"
FileWriteLine($hfile, "pause 1000")
Case "{BACKSPACE}"
FileWriteLine($hfile, StringFormat("pitch %.2f", GetPitch()))
Case Else
FileWriteLine($hfile, "key " & @HotKeyPressed)
EndSwitch
EndFunc
Au3Record
Вместе с SciTE4AutoIt3 поставляется AutoIt3\Extras\Au3Record\Au3Record.exe, который позволяет записывать ваши действия с рабочим столом и оформляет это сразу в виде AutoIt-скрипта. Если вам предстоит много раз совершать последовательность машинальных действий без необходимости думать (например, при установке ПО или патчей на большой парк компьютеров), присмотритесь к этому инструменту.
Player
Напомню, что результатом работы Recorder'а, описанного в предыдущей главе, является набор команд вида
mouse left 1892 1021 pause 10000 pitch -0.89 mouse right 942 498 pause 10000 move 83.72 50.03 0.604 key ` pause 1000 key {SPACE} pitch -0.1 move 83.777 50.207 1.235 move 83.777 50.207 2.114 move 83.777 50.207 2.827 move 83.777 50.207 2.855 move 83.754 50.327 2.855
На первый взгляд кажется, что «проиграть» их не составит никакого труда, но давайте детально рассмотрим этот процесс.
Поворот и наклон
Поворот и наклон при помощи нажатия на кнопки дает точность порядка 30 градусов, что неприемлемо для нашей задачи. Поэтому мы будем использовать для этого вторую возможность: движение мыши с зажатой правой кнопкой. Алгоритм следующий:
- Зажмем правую кнопку мыши
- Игра сама поместит курсор на центр экрана и будет удерживать его там. Это позволит пользователю не упереться в итоге курсором в край экрана при повороте
- Сдвигаем курсор налево или направо на определенное число пикселей
- Направление персонажа смещается налево или направо. Причем, чем быстрее вы двигаете курсор, тем быстрее вертится персонаж
Как нам попроще определить, в какую сторону выгоднее поворачиваться, если у нас есть два угла: текущий и требуемый?
![](https://habrastorage.org/storage2/cbc/ae5/d42/cbcae5d42f62af52e64ee84cfddc6c3e.png)
Можно посмотреть на разницу углов, если она положительна — направо. Но рассмотрим случай: текущий угол — 30 градусов, требуемый — 330 градусов. Разница отрицательна, но нам все равно направо. А еще углы могут быть отрицательными. Чтобы не приходилось выписывать все эти условия, просто воспользуемся синусом разницы углов и будем смотреть только на его знак.
Переменные $want и $current содержат (x, y, azimuth)
Func Turn($want)
while true
$current = GetPos()
$sin = sin($current[2] - $want[2])
;ToolTip(StringFormat("(%.2f,%.2f,%.2f) to (%.2f,%.2f,%.2f)", $current[0], $current[1], $sin, $want[0], $want[1], $want[2]))
if abs($sin) < 0.05 then return
MouseMove(MouseGetPos(0)+50*$sin, MouseGetPos(1), 1)
wend
EndFunc
Синус же позволяет нам регулировать скорость поворота. Чем меньше угол, тем аккуратнее будет двигаться наш курсор и тем точнее мы повернемся. И наоборот, на большие углы нам хочется поворачиваться побыстрее.
Функция изменения наклона к горизонту полностью аналогична, только сдвигать курсор надо по вертикали.
В приведенном примере кода, есть логическая ошибка, которая может привести к неправильному поведению бота. Предлагаю любопытному читателю самому ее найти.
Движение
На движение накладываются следующие требования
- Точность позиционирования невысока. Нельзя встать ровно туда, куда вы желаете
- Двигаться надо, не поворачиваясь. Из доступных движений есть только бег вперед, шаги назад и strafe (движение вбок без поворота)
- Движение должно быть непрерывным, без остановок и рывков
- Как можно меньше возвратов и смен направлений движения. Неприятно смотреть на монитор, если персонаж пробежал чуть-чуть, потом попятился, потом снова побежал и так далее
Вперед или назад?
Если вы не любите математику или закончили школу очень давно, вы можете пропустить этот раздел без ущерба для понимания. Но ничего особо сложного тут нет. Пусть персонаж стоит на (ax, ay) и смотрит под углом
![\alpha](http://habrastorage.org/getpro/habr/post_images/afa/be9/d80/afabe9d808e93fbe09b29977a6800dc7.gif)
![](http://habrastorage.org/storage2/3ae/1ec/e0f/3ae1ece0f14436a608e435652af60432.png)
Если решать задачу в лоб, то надо бы вычислить косинус угла между векторами. Если он положительный, то угол острый и бежать вперед. Если отрицательный — тупой, надо пятиться. Косинус можно вычислять по теореме косинусов
![a^2=b^2+c^2-2bc\cdot cos\theta](http://habrastorage.org/getpro/habr/post_images/550/9af/e98/5509afe98797a4769ee0eae7e5dc1bc8.gif)
Но для этого нам придется вычислять длины каждого из векторов, извлекать корни, очень громоздко.
Есть так же формула скалярного произведения
![\langle a,b\rangle=|a||b|cos\theta](http://habrastorage.org/getpro/habr/post_images/450/2a2/5bd/4502a25bd2bb5ef17463b3895de1b2c8.gif)
Которое, как известно, в декартовой системе координат может быть вычислено по формуле
![\langle a,b\rangle=a_{x}b_{x}+a_{y}b_{y}](http://habrastorage.org/getpro/habr/post_images/3e8/66a/9c5/3e866a9c5eca1fdc853e46c9bb6536d7.gif)
Таким образом, вычислив скалярное произведение и посмотрев на его знак, мы сможем определить, бежать нам вперед или назад. А в нашем случае вычислять мы его будем по формуле
![cos\alpha*d_x +sin\alpha*d_y](http://habrastorage.org/getpro/habr/post_images/a2e/8b7/272/a2e8b727299867dbf8b23aad09439fa8.gif)
![image](http://habrastorage.org/getpro/habr/post_images/d42/fb3/e99/d42fb3e9914b179707d290ae79e67c33.png)
Влево или вправо?
Аналогичным образом, синус угла между векторами взгляда
![a\Lambda b=|a||b|sin\theta=a_x b_y-a_y b_x](http://habrastorage.org/getpro/habr/post_images/5ab/88d/eaa/5ab88deaa42ff6fd92ab52e0e5f39a9e.gif)
Опять-таки величина этого числа определяет, насколько много надо сделать шагов вбок.
Реализация
Запрограммируем наши рассуждения
Func ScalarMult($a, $b, $x, $y)
return $a*$x + $b*$y
EndFunc
Func VectorMult($a, $b, $x, $y)
return $a*$y - $b*$x
EndFunc
Func GetDirection($x, $y, $wx, $wy, $angle)
$dx = $x - $wx
$dy = $y - $wy
local $result[2] = [ ScalarMult(-sin($angle), -cos($angle), $dx, $dy), VectorMult(-sin($angle), -cos($angle), $dx, $dy) ]
Return $result
EndFunc
Пусть вас не смущает, что в программе синус и косинус поменяны местами и взяты с противоположным знаком. Просто в WoW азимут отсчитывается от севера против часовой стрелки. Эти детали можно просто «попробовать» в реальной программе. И, если вышло наоборот, поиграться со знаками.
Функция GetDirection() возвращает массив из двух значений: сколько идти вперед/назад, сколько идти вбок.
Это поворот!
Некоторые читатели могут воскликнуть: «Да ведь эти умножения на синусы-косинусы — ничто иное, как обыкновенный поворот системы координат,
![](http://habrastorage.org/storage2/c27/741/0c2/c277410c20a18c528f8c512c7df61788.gif)
описываемый
![image](http://habrastorage.org/getpro/habr/post_images/ea6/600/9d1/ea66009d156eb28245603fdd9ab32fd5.png)
Зачем морочить нам головы.»
Верно, но тогда мне было бы сложнее пояснить, почему матрица именно такая.
Остановка
Теперь мы готовы давить кнопки бега
Func Move($want)
while true
StartMoving()
local $pos = GetPos()
local $dir = GetDirection($pos[0], $pos[1], $want[0], $want[1], $pos[2])
;ToolTip(StringFormat("(%.2f,%.2f,%.2f) to (%.2f,%.2f, %.2f): (%.2f,%.2f)", $pos[0], $pos[1], $pos[2], $want[0], $want[1], $want[2], $dir[0], $dir[1]))
if abs($dir[0]) < 0.1 And abs($dir[1]) < 0.1 then ExitLoop
if abs($dir[0]) >= abs($dir[1]) Then
Send("{a up}{d up}")
if $dir[0] <=0 Then
Send("{w down}{s up}") ; forward
Else
Send("{s down}{w up}") ; backward
EndIf
Else
Send("{s up}")
if $dir[1] < 0 Then
Send("{d down}{a up}") ; right
Else
Send("{a down}{d up}") ; left
EndIf
EndIf
wend
Send("{s up}{a up}{d up}")
EndFunc
Обратите внимание, мы идем в том направлении, в котором идти дальше всего, и заканчиваем путь, когда мы почти у цели. Это позволяет персонажу «не мельтешить». Кнопка движения вперед не отпускается по достижении точки назначения. Поэтому поворачиваться на требуемый угол нам надо сравнительно быстро.
Код, отвечающий за проигрывание файлов команд, не представляет из себя ничего интересного, и при желании вы можете ознакомиться с ним самостоятельно.
Полный исходный код Player'а
#include <Color.au3>
Global $WinName = "World of Warcraft"
Opt("PixelCoordMode", 2) ;Отсчет координат пикселей от левого верхнего угла клиентской части окна
Opt("MouseCoordMode", 2) ;Отсчет координат мыши от левого верхнего угла клиентской части окна
$paused = false
$moving = false
HotKeySet("{F11}", "Pause")
HotKeySet("{F10}", "Kill")
;WinActivate($WinName)
$hwnd = WinGetHandle($WinName)
WinWaitActive($hwnd)
PlayFile("outdoor.txt", 0)
PlayFile("indoor.txt", 0)
PlayFile("outdoor.txt", 0)
PlayFile("indoor.txt", 0)
PlayFile("outdoor.txt", 0)
PlayFile("indoor.txt", 0)
PlayFile("outdoor.txt", 0)
PlayFile("indoor.txt", 0)
PlayFile("outdoor.txt", 0)
PlayFile("indoor.txt", 0)
Func PlayLine($line)
local $command = StringSplit($line, " ", 2)
;ToolTip(StringFormat("%s %s %s", $command[0], $command[1], $command[2]))
Switch $command[0]
Case "move"
local $want[3] = [ number($command[1]), number($command[2]), number($command[3]) ]
Move($want)
Turn($want)
Case "pause"
StopMoving()
Sleep($command[1])
Case "key"
If $command[1] = "{SPACE}" Then
Send("{SPACE down}")
Sleep(300)
Send("{SPACE up}")
Else
StopMoving()
if UBound($command) > 2 Then
Send($command[1] & " " & $command[2])
Else
Send($command[1])
EndIf
Sleep(1500)
EndIf
Case "mouse"
StopMoving()
MouseClick($command[1], $command[2], $command[3])
Sleep(500)
Case "pitch"
SetPitch($command[1])
EndSwitch
EndFunc
Func PlayFile($filename, $skip = 0)
$hfile = FileOpen($filename, 0)
For $i = 1 to $skip
$line = FileReadLine($hfile)
Next
while True
$line = FileReadLine($hfile)
if @error = -1 Then ExitLoop
PlayLine($line)
wend
FileClose($hfile)
StopMoving()
EndFunc
Func Sign($x)
if ($x < 0) then
return -1
else
return 1
EndIf
EndFunc
Func ScalarMult($a, $b, $x, $y)
return $a*$x + $b*$y
EndFunc
Func VectorMult($a, $b, $x, $y)
return $a*$y - $b*$x
EndFunc
Func StartMoving()
if $moving then return
$moving = true;
WinWaitActive($hwnd)
MouseMove(@DesktopWidth/2, @DesktopHeight/2, 0)
MouseDown("right")
Sleep(300)
EndFunc
Func StopMoving()
$moving = false
Send("{w up}{s up}{a up}{d up}")
MouseUp("right")
Sleep(300)
EndFunc
Func GetPitch()
StartMoving()
$pixel2 = _ColorGetRGB(PixelGetColor(10, 0, $hwnd));
Return ($pixel2[2]/255.0-0.5)*4
EndFunc
Func GetPos()
StartMoving()
$pixel1 = _ColorGetRGB(PixelGetColor(0, 0, $hwnd));
$pixel2 = _ColorGetRGB(PixelGetColor(10, 0, $hwnd));
local $result[3] = [ ($pixel1[0]+$pixel1[1]/255.0)/255*100, ($pixel2[0]+$pixel2[1]/255.0)/255*100, $pixel1[2]*7.0/255 ]
return $result
EndFunc
Func GetDirection($x, $y, $wx, $wy, $angle)
$dx = $x - $wx
$dy = $y - $wy
local $result[2] = [ ScalarMult(-sin($angle), -cos($angle), $dx, $dy), VectorMult(-sin($angle), -cos($angle), $dx, $dy) ]
Return $result
EndFunc
Func Move($want)
while true
local $pos = GetPos()
local $dir = GetDirection($pos[0], $pos[1], $want[0], $want[1], $pos[2])
ToolTip(StringFormat("(%.2f,%.2f,%.2f) to (%.2f,%.2f, %.2f): (%.2f,%.2f)", $pos[0], $pos[1], $pos[2], $want[0], $want[1], $want[2], $dir[0], $dir[1]))
if abs($dir[0]) < 0.1 And abs($dir[1]) < 0.1 then ExitLoop
if abs($dir[0]) >= abs($dir[1]) Then
Send("{a up}{d up}")
if $dir[0] <=0 Then
Send("{w down}{s up}") ; forward
Else
Send("{s down}{w up}") ; backward
EndIf
Else
Send("{s up}")
if $dir[1] < 0 Then
Send("{d down}{a up}") ; right
Else
Send("{a down}{d up}") ; left
EndIf
EndIf
wend
Send("{s up}{a up}{d up}")
EndFunc
Func Turn($want)
while true
local $current = GetPos()
$sin = sin($current[2] - $want[2])
;ToolTip(StringFormat("(%.2f,%.2f,%.2f) to (%.2f,%.2f,%.2f)", $current[0], $current[1], $sin, $want[0], $want[1], $want[2]))
if abs($sin) < 0.05 then return
MouseMove(MouseGetPos(0)+50*$sin, MouseGetPos(1), 1)
wend
EndFunc
Func SetPitch($want)
while true
$current = GetPitch()
$sin = sin($current - $want)
;ToolTip(StringFormat("pitch %.2f to %.2f: %.2f", $current, $want, $sin))
if abs($sin) < 0.05 then return
MouseMove(MouseGetPos(0), MouseGetPos(1)+50*$sin, 1)
wend
EndFunc
Func Pause()
$paused = not $paused
if $paused then StopMoving()
While $Paused
Sleep(1000)
WEnd
EndFunc
Func Kill()
StopMoving()
Exit
EndFunc
Управление (ро)ботом
![ED-209 from Robocop falls down thе stairs](http://habrastorage.org/storage2/64a/4b3/cc6/64a4b3cc69f274f5f8c91eaad5861852.jpg)
Вот, казалось бы, и все. Пробежим по маршруту, запишем координаты, нажатия клавиш и клики, и включим воспроизведение. Но не тут-то было. Здесь начинается самое сложное.
В подземелье живут мобы, которые хоть и не представляют угрозы для жизни нашего персонажа, но норовят оглушить, кинуть «молчанку», отбежать в сторонку как раз тогда, когда вы бы хотели их убить, и бросаются на амбразуру в тот момент, когда вы думали взять в цель совсем другого моба. Иногда ваша цель забегает за спину, иногда — умирает за один удар, хотя должна жить три. В общем, их хаотичное поведение вносит заметный элемент случайности в наш маршрут, и в итоге персонаж может оказаться у запертой двери в самом конце маршрута просто потому, что не смог убить кого-то в самом начале.
Да, можно было написать внутриигровые макросы, которые бы брали в цель нужного моба, и тем самым избавиться от некоторых проблем, но мне хотелось оставить бота как можно более универсальным.
Двери-решетки. На них нужно кликнуть, чтобы открыть. А так как они решетки, то кликнуть вы можете случайно аккуратно в дырку. И персонаж будет биться лбом об дверь в попытке пробежать дальше.
Таким образом, записав маршрут, вы открываете его в блокноте и отлаживаете. Вы наблюдаете за персонажем и при проблемах или возможных проблемах «вбиваете костыли» в маршрут: вставляете дополнительное заклинание для верности (вдруг моб выживет), добегаете до той точки, в которой моб будет точно виден и досягаем независимо от его желания прогуляться. Вы изменяете маршрут, чтобы уж точно не споткнуться об этот злосчастный ящик. После 10 забегов я пришел к выводу, что лучше бы я просто переубивал там всех пауков, чем они потом смогли бы отомстить мне в одном случае из 10 своим внезапным заклинанием. Но, когда персонаж уже бегает сам, вам уже не хочется бежать самому ножками, вам хочется просто смотреть на это.
Я понял, каково это, запускать марсоход, программировать робот-пылесос, идти в полной темноте по квартире или учить роботов играть в футбол. Надо все делать надежно, с запасом. На марсе ямы будут везде, а еще будут нависающие скалы и пещеры. В квартире роботу-пылесосу будут противостоять провода и тапочки повсюду, а еще будет кот, который захочет поиграть. В полной темноте будут углы, ножки стульев, и даже упавшие на пол ножи. Только на ощупь!
Признаюсь, выполнил я заявленную в прошлой части лишь программу минимум. Бот все еще может не выполнить задачу с вероятностью 1%. Так что оставлять его без контроля на ночь нельзя, застрянет.
Противодействие ботам
![](http://habrastorage.org/storage2/4d9/78f/4c9/4d978f4c9aa489062a3e5aa1f10af06c.jpg)
Люди, далекие от MMORPG игр, спросят: «А зачем вообще противодействовать ботам? Ведь автоматизация везде приветствуется.» А вот и нет. Если все платят одинаково, то и возможности должны быть равными. Иначе ущемленная часть обижается и перестает играть. Если в игре доступны, например, макросы, то они должны быть понятны и не программистам. Поэтому разработчик игры запрещает использование средств автоматизации на уровне лицензионного соглашения и следит.
Я нередко встречал возгласы, что Blizzard (это разработчик WoW) плохо следит, и боты повсюду. Во-первых, сам я так не считаю, думаю, их доля преувеличена. А во-вторых, давайте обсудим, что же есть в арсенале разработчика. Как не только распознать бота, но еще и иметь подтверждающие факты. Ведь на основе подозрений невежливо банить игрока.
Например, гейм-мастер (внутриигровая поддержка) способен наблюдать за игроком и видеть его глазами. Но даже, увидев явно «нечеловеческое» поведение, как то: длительное вращение на месте, попытки пройти сквозь стену, беспрерывная игра в течение суток (обычно сбор ресурсов), игнорирование попыток общения других игроков, — все это не дает гарантий использования ботов. Быть может, игрок просто заснул на клавиатуре, очень целеустремлен, или просто не хочет общаться, когда «работает».
Поэтому у Blizzard есть Warden. ПО по сути напоминает облачный антивирус:
- фоновые программы анализируются
- их метаданные отправляются в облако
- чит-аналитики принимают решение, что такой-то процесс нечестно взаимодействует с игрой
- к пользователям, использовавшим эти программы, применяются санкции
Есть данные, что игроков банили за запуск игры из-под Wine. Blizzard не исключает возможность бана за использование программируемых клавиатур и мышек.
Давайте подумаем, что же надо делать, чтобы как можно быстрее привлечь к себе внимание и попасть под подозрение:
- Нужно читать, а главное, писать в память процесса игры
- Нужно инжектить туда свои модули
- Ваше ПО для получения преимуществ в игре должно быть как можно более популярным
- Вам надо использовать руткит-технологии и полиморфизм для обхода систем защиты
И вот тогда рано или поздно придет ban-wave для всех пользователей этого ПО.
Что же касается посылки нажатия клавиш и языка AutoIt, то по всей видимости, его запущенный интерпретатор слишком распространен среди игроков и используется также для «мирных» целей. В итоге отличить ботоводов от офисных автоматизаторов труда Blizzard не могут, ну, или не хотят.
Заключение
Сегодня мы описали:
- Программу для перехвата клавиш и записи в файл
- Программу для воспроизведения команд из файла и перемещения в мире
- Процесс управления персонажем в условиях малой информации о мире
- Математику для определения направления движения
- Порассуждали на тему противодействия ботам, так ли это легко
Больше всего времени при написании бота заняло, конечно же, написание самого поста. Бот сделал забегов 20 (игра запрещает делать больше 5 в час). Пока он бегает, компьютер занят, а мне надо пост писать. Конь пока не выпал. Но если выпадет, я обрадуюсь не меньше, ведь я получу его хоть и не фармом, но все-таки трудом.
Голосуйте, пишите комментарии, не ругайте за спорную тему. Удачи.
Only registered users can participate in poll. Log in, please.
Чего бы вам больше хотелось видеть в моем следующем посте
35.42% Лучше больше практических советов и пользы, опиши процесс создания простой 3D-игры своими силами198
26.83% Лучше больше математики и теории, закодируй и опиши процесс цветовой сегментации150
72.45% Лучше больше про игры и ботов, опиши бота для ловли рыбы с распознаванием удочки на экране и поиском поплавка405
8.05% Лучше больше не пиши посты, ничего нового не узнал45
559 users voted. 194 users abstained.