Введение
Прочитал недавно пост, в котором автор рассказал о своём боте для торговли на аукционе в игре World of Warcraft. Его персонаж бегал челноком на небольшие расстояния и выполнял конкретные действия, строго заданные в управляющей программе. Воспользовавшись его идеями, я решил пойти немного дальше: пусть персонаж будет способен бегать по маршруту длительностью хотя бы пять минут, при этом должна быть возможность записывать маршрут и выполняемые действия извне, не прибегая к изменению самой программы управления. Описанное мной является развитием поста-оригинала, тем не менее я постарался, чтобы его чтение не было обязательным.
В посте я опишу
- Процесс создания тривиального аддона для пользовательского интерфейса WoW на языке Lua
- Сериализация данных для передачи между частями системы
- Передача с помощью цвета пикселей
- Процесс управления персонажем на языке AutoIt
- Программа для записи выполняемых действий
- Программа для воспроизведения
- Мат.расчеты направления перемещения
- Почему не стоит прибегать к чтению/записи памяти процесса
- Программа управления (ро)ботом с командами, понятными непрограммисту
Предупреждение
Я отлично понимаю, что использование ботов запрещено лицензионным соглашением Blizzard. Это может привести к перманентному бану аккаунта. Я бы расстроился, узнав, что кто-то, воспользовавшись материалами данного поста, стал наживаться, ухудшая экономику игры и вызывая неодобрение других игроков. И да, я понимаю, то, что делаю я, нечестно.
Цель поста — рассказать о моем опыте и описать сложности, с которыми я столкнулся, узнать у вас, что можно было сделать лучше.
Предыстория
Коллекционирование
Люди, далекие от индустрии игр, могут без ущерба пропустить этот раздел. В игре WoW есть такой аспект — коллекционирование. Как и в нумизматике/филателии, чем больше у вас, например, верховых животных, тем вам приятнее. Точно так же важен процесс получения. Какие-то драконы требуют для получения выполнения десятка задач («ачивок»), какие-то — войти в лучшие 2% игроков в мире по навыку управления персонажем (сражения на арене), что-то можно просто купить за игровую валюту, что-то — за реальные деньги в магазине. Подробности можно почитать в другом источнике, речь не об этом. Так вот, некоторые из коллекционируемых предметов падают в подземелье с шансом около 0,5%. Если шанс выпадения дается игроку лишь раз в день (иногда в неделю), вам потребуется время, соизмеримое с годом, для забегов. И все ради того мгновения получения заветного выстраданного предмета. Чем больше вы потратили времени и сил на "фарм" предмета, тем приятнее вам его в итоге получить. По моему опыту радость очень мимолетна.
Зависимость шанса выпадения от количества попыток
Небольшое отступление. Какова вероятность того, что за шесть бросков игральной кости выпадет «6»? Очевидно, что . То есть, наше событие является противоположным событием к невыпадению «6» за все шесть попыток.
Используя второй замечательный предел, нетрудно показать, что .
А вероятность получения коня с шансом «дропа» в 1% за 100 попыток примерно равна 63%.
Некоторые игроки, сделав триста попыток считают, что вот-вот повезет — ведь вероятность облома за такое большое число попыток мала. Их ждет разочарование, ведь за следующие 100 им повезет снова лишь на 63%. А старые труды не дают ничего.
Задача
Есть подземелье. Нужно пробежать по нему с виражами, выполнить пару несложных задач по пути. Убить финального босса и поднять добычу. Чтобы не бежать к выходу пешком, телепортироваться в ближайшую деревню и долететь ко входу. Повторить 5 раз.
Карта подземелья и маршрут:
Задача максимум. Запустил и ушел на пол часа. Процесс полностью автоматический.
Задача минимум. Запустить и сидеть рядом, читать или играть на гитаре, поглядывая на монитор. При необходимости изредка помогать персонажу выбраться из непредвиденной ситуации.
Важно иметь возможность при желании описать маршрут другого подземелья, не залезая в код.
Подход
Движение и позиционирование
Просто записать нажатие и отпускание клавиш, позицию и щелчки мыши не получится. Точнее записать-то получится, только при проигрывании будет происходить совсем не то, что вы ожидали, по следующим причинам:
- Вам требуется воспроизвести первоначальное положение персонажа и направление его взгляда. Если вы перед автоматическим забегом повернетесь слегка в другую сторону, вы побежите кривовато и рано или поздно упретесь в стенку
- Если вы записали точные координаты и угол, то поставить персонажа туда же вам также не удастся. Реагирует на кнопки он как бульдозер и поворачивается так же. А даже ошибка в 1 градус через 100 метров выльется в метра (ширина дверного проема)
- Время нажатия и отпускания кнопок тоже далеко от идеала. Это похоже на передвижение по квартире с закрытыми глазами. Вроде бы шесть шагов вперед и строго направо, но на деле вместо туалета мы оказываемся в ванной. Попробуйте также открыть дверь с закрытыми глазами, не щупая ручку
Вот по этим причинам нам необходимо позиционироваться в мире при помощи координат. При этом сверять позицию и вносить коррективы необходимо регулярно, каждые 100 миллисекунд.
move 40.644 20.417 1.537 move 40.526 20.411 1.537 move 40.246 20.408 1.592 move 39.974 20.414 1.592 move 39.691 20.411 1.537 move 39.417 20.388 1.510
Это отрывок из списка команд робота по перемещению. Указаны координаты x, y и угол поворота в пространстве в радианах.
Клавиши и команды
По пути надо нажимать кнопки и кликать мышкой, открывать двери, где-то нужно ждать. Не хотелось бы выписывать это все вручную. Да и искать среди команд с координатами (в примере выше), куда же что вписать, не очень приятно. Поэтому единовременные нажатия на кнопки и клики мы будем писать параллельно с записью координат.
pitch -0.89 mouse right 942 498 pause 10000 move 39.417 20.388 1.510 key ` pause 1000 key {SPACE} move 39.142 20.364 1.510 key {TAB} key 3 key 3 mouse right 963 622 move 38.864 20.340 1.510 move 38.653 20.321 1.510
Отдельно упомяну про «pitch» (наклон). Если персонаж смотрит в горизонт, он равен нулю. Если под ноги, наклон отрицателен. А в небо — положителен. Измеряется в радианах. Его пришлось добавить, как только персонажу захотелось летать.
Модули
Таким образом, у нас вырисовываются части программно-аппаратного комплекса бота:
- Мы напишем addon для интерфейса WoW, который будет определять позицию персонажа, азимут, наклон. Правилами игры аддоны ничуть не запрещены, указанная информация доступна через игровое API внутри аддона. Он будет рисовать это все на экране. Сам аддон пишется на языке Lua, потому что так решили разработчики игры.
- В аддоне можно определять кое-что, можно рисовать, но вот заставить персонажа двигаться нельзя. Еще в аддоне нельзя писать в файл, спрашивать у игры, где стены, где враги, да и много чего еще нельзя. Поэтому нам нужно написать внешнее управляющее приложение. Я не стал оригинальничать и использовал AutoIt. Это действительно быстро и удобно, хоть и немного дико для меня, привыкшего программировать на строго типизированных языках.
- Приложение должно уметь записывать мои действия. Назовем этот функционал Recorder
- Приложение должно уметь воспроизводить запись. Назовем Player
- Записанные действия и координаты мы будем хранить в текстовом файле построчно. Их можно и нужно редактировать вручную. Добавлять костыли, удалять мусор. Скажу сразу, это самое сложное.
Addon для WoW на Lua
Аддон для WoW это скрипт на языке Lua. Язык полноценный, гибкий, эффективный. Делать с системой ничего нельзя. Делать с игрой можно все, но только в рамках того API, которое предоставляет вам игра. Так как Blizzard не хочет, а точнее, запрещает игрокам и предпринимателям писать ботов, то и API кроме упомянутых выше координат почти ничего полезного и не предоставляет. Разумеется, полезного для наших нужд. Неплохой гайд по написанию аддонов сразу с описанием Lua можно найти здесь. Ну а я опишу свой вариант.
Манифест и GUI
Подробности и источник. В папке World of WarcraftInterfaceAddOns создаете папку HelloWorld. В нее кладете HelloWorld.toc с содержимым
## Interface: 50001 ## Title: Hello, World! ## Notes: My first AddOn HelloWorld.lua
Если бы я хотел понарисовать формочек и кнопочек (а для этого обычно и нужны аддоны), последней строчкой я бы вписал еще HelloWorld.xml с описанием этих кнопочек. В дизайне и написании такого GUI-аддона хорошо помогает AddOn Studio. Мощный инструмент на базе MS Visual Studio.
Но так как я минималист, рисовать мы в этот раз не будем. Кроме того, это даст мне возможность показать, что создавать формы можно динамически из самого скрипта без использования дизайнера. Рассмотрим же HelloWorld.lua. Редактировал я его в Notepad++. Чтобы игра подключила изменения в HelloWorld.toc, надо перезапустить ее целиком. А вот изменения в HelloWorld.lua можно подхватить, написав в консоли команду /reload. Поэтому процесс программирования и отладки не такой болезненный.
Слеш-команды
Кстати, об отладке. Советую сразу добавить обработчик слеш-команд:
SLASH_HELLO_WORLD1 = '/helloworld';
function SlashCmdList.HELLO_WORLD(msg, editbox)
local facing = GetPlayerFacing();
local pitch = GetUnitPitch("player");
local x, y = GetPlayerMapPosition("player");
print(format("HelloWorld %.2f %.2f %.2f %.2f", x*100, y*100, facing, pitch));
end
Для этого определим переменную вида SLASH_ИМЯn. Где ИМЯ уникально для всех аддонов, а n либо пусто, либо порядковое число с 1. И добавим функцию с именем ИМЯ в некий объект. Может показаться странным для программиста на C++, что мы эту функцию-обработчик нигде явно не регистрируем. Да и со строковой переменной связываем чисто по имени переменной. Но вот такая она, мощь и магия Lua.
Теперь команда в WoW-консоли /helloworld выполнит требуемые вам действия: позволит вывести отладочную информацию, а для простейших аддонов, собственно, произведет все то, ради чего вы их писали.
Ну и тут же я показал WoW API, которое достает требуемую информацию.
Обработчик событий
Система работы с GUI в аддоне похожа на работу Windows со своими сообщениями и их обработкой.
local EventFrame = CreateFrame("Frame")
function EventFrame:OnEvent(event, ...)
print("HelloWorld:", event)
self[event](self, ...)
end
EventFrame:SetScript("OnEvent", EventFrame.OnEvent)
EventFrame:RegisterEvent("PLAYER_LOGIN")
function EventFrame:PLAYER_LOGIN()
-- Инициализация тут
end
function EventFrame:OnUpdate()
-- Полезные действия тут
end
EventFrame:SetScript("OnUpdate", EventFrame.OnUpdate)
Мы создаем фрейм типа "Frame" и подключаемся на действия "OnEvent" и "OnUpdate" командами
EventFrame:SetScript("OnEvent", EventFrame.OnEvent)
EventFrame:SetScript("OnUpdate", EventFrame.OnUpdate)
Обработчик OnUpdate будет вызываться после каждого кадра — то, что нам нужно для обновления координат. А OnEvent будет вызываться при других желаемых событиях. Из него будем дергать соответствующие функции:
self[event](self, ...)
Что еще удобнее в Lua, это то, что вот такой конструкцией можно вызвать уникальные обработчики вида
function EventFrame:PLAYER_LOGIN()
Здесь PLAYER_LOGIN — это событие, которое рассылается во все фреймы при входе в мир и перезапуске пользовательского интерфейса. Другие события: PLAYER_LEAVE_COMBAT, QUEST_FINISHED, PLAYER_EQUIPMENT_CHANGED, PLAYER_DEAD, — и еще очень много. С полным списком можно ознакомиться тут. Зарегистрируем то событие, которое мы хотим обрабатывать, командой
EventFrame:RegisterEvent("PLAYER_LOGIN")
Передача информации
Так как в аддоне нельзя ничего делать с системой, информацию из WoW другой части бота мы будем передавать при помощи изменения цвета пикселей. Как это делал rednaxi в своем посте. Но только мы будем передавать цветом не один бит информации, а будем сериализовать данные и передавать их больше.
Рисование
Так как для рисования тоже нужны фреймы, создадим их
local HelloWorld1 = CreateFrame("Frame", nil, UIParent)
local HelloWorld2 = CreateFrame("Frame", nil, UIParent)
function EventFrame:PLAYER_LOGIN()
HelloWorld1:SetFrameStrata("BACKGROUND")
HelloWorld1:SetWidth(10)
HelloWorld1:SetHeight(10)
HelloWorld1.texture = HelloWorld1:CreateTexture(nil,"BACKGROUND")
HelloWorld1.texture:SetAllPoints(HelloWorld1)
HelloWorld1:SetPoint("TOPLEFT",0,0)
HelloWorld1:Show()
HelloWorld2:SetFrameStrata("BACKGROUND")
HelloWorld2:SetWidth(10)
HelloWorld2:SetHeight(10)
HelloWorld2.texture = HelloWorld2:CreateTexture(nil,"BACKGROUND")
HelloWorld2.texture:SetAllPoints(HelloWorld2)
HelloWorld2:SetPoint("TOPLEFT",10,0)
HelloWorld2:Show()
end
Думаю, тут все понятно и без комментариев. Подробности по каждому методу ищите сами.
Сериализация
У нас есть две координаты и два угла. Все — числа с плавающей запятой. Компоненты цветов пикселей в аддоне тоже числа с плавающей запятой, но от 0.0 до 1.0. Кроме того, известно, что сохранится компонента цвета в одном байте. Поначалу я каждую координату сохранял в одну компоненту цвета. В итоге точность позиционирования в локации составляла 1/255 от размера карты. Получалось как в GPS: вроде координаты есть, но вести машину на автомате (управление компьютером без человека) по дороге по навигатору не выйдет. Так что пришлось давать два байта. Как повыгоднее сохранить одно дробное число в два байта? Я сделал так
local x1, x2 = math.modf(x*255)
Функция modf возвращает целую и дробную часть числа через запятую. Здесь используется параллельное присваивание — еще одна полезная фишечка Lua.
При таком подходе я использую полную мощность каждого из двух байтов, в которые сохраняется координата. Ну а азимут и наклон не так требовательны к точности, лишь бы укладывались в отрезок 0.0-1.0. Получается вот так:
local math = getfenv(0).math
function EventFrame:OnUpdate()
local facing = GetPlayerFacing();
local pitch = GetUnitPitch("player");
local x, y = GetPlayerMapPosition("player");
local x1, x2 = math.modf(x*255)
local y1, y2 = math.modf(y*255)
HelloWorld1.texture:SetTexture(x1/255, x2, facing/7)
HelloWorld2.texture:SetTexture(y1/255, y2, pitch/4+0.5)
end
Теперь при перемещении по миру в левом верхнем углу два квадратика 10 на 10 будут хаотично менять свой цвет.
local math = getfenv(0).math
SLASH_HELLO_WORLD1 = '/helloworld';
local EventFrame = CreateFrame("Frame")
local HelloWorld1 = CreateFrame("Frame", nil, UIParent)
local HelloWorld2 = CreateFrame("Frame", nil, UIParent)
function EventFrame:OnEvent(event, ...)
print("HelloWorld:", event)
self[event](self, ...)
end
EventFrame:SetScript("OnEvent", EventFrame.OnEvent)
EventFrame:RegisterEvent("PLAYER_LOGIN")
function EventFrame:PLAYER_LOGIN()
HelloWorld1:SetFrameStrata("BACKGROUND")
HelloWorld1:SetWidth(10)
HelloWorld1:SetHeight(10)
HelloWorld1.texture = HelloWorld1:CreateTexture(nil,"BACKGROUND")
HelloWorld1.texture:SetAllPoints(HelloWorld1)
HelloWorld1:SetPoint("TOPLEFT",0,0)
HelloWorld1:Show()
HelloWorld2:SetFrameStrata("BACKGROUND")
HelloWorld2:SetWidth(10)
HelloWorld2:SetHeight(10)
HelloWorld2.texture = HelloWorld2:CreateTexture(nil,"BACKGROUND")
HelloWorld2.texture:SetAllPoints(HelloWorld2)
HelloWorld2:SetPoint("TOPLEFT",10,0)
HelloWorld2:Show()
end
function EventFrame:OnUpdate()
local facing = GetPlayerFacing();
local pitch = GetUnitPitch("player");
local x, y = GetPlayerMapPosition("player");
local x1, x2 = math.modf(x*255)
local y1, y2 = math.modf(y*255)
HelloWorld1.texture:SetTexture(x1/255, x2, facing/7)
HelloWorld2.texture:SetTexture(y1/255, y2, pitch/4+0.5)
end
EventFrame:SetScript("OnUpdate", EventFrame.OnUpdate)
function SlashCmdList.HELLO_WORLD(msg, editbox)
local facing = GetPlayerFacing();
local pitch = GetUnitPitch("player");
local x, y = GetPlayerMapPosition("player");
print(format("HelloWorld %.2f %.2f %.2f %.2f", x*100, y*100, facing, pitch));
end
Заключение первой части
Повторим, о чем шла речь
- Мы посчитали вероятность получения предмета за большое число попыток
- Обозначили задачу, выработали подход к решению, разбили на модули
- Мы рассмотрели код простейшего аддона для WoW на языке Lua
- научились передавать координаты при помощи цвета
- научились рисовать, обрабатывать события
- научились обрабатывать слеш-команды
Теперь вы готовы к написанию своего аддона.
А если вы разрабатываете нечто и хотите дать возможность себе или пользователям принимать участие, гибко подстраивать ПО под себя с использованием вашего API (например, писать искусственный интеллект противников, дать возможность брокерам размещать позиции, основываясь на котировках, дать возможность администраторам выполнять свои действия по результатам инвентаризации систем пользователей и т.п.), то знайте, Lua очень гибок и очень легко встраивается. Обдумайте такую возможность.
Ну, а в следующей части мы поговорим о
- написании Recorder'а клавиш и координат на языке AutoIt
- написании Player'a инструкций для бота
- математике 2D, как ориентироваться в декартовой системе координат без теоремы косинусов
- управлении роботом при недостаточном количестве датчиков
- мерах противодействия ботам
Автор: AlexeyVanilov