Пишем бота-кликера на Kotlin для Lineage 2

8bec82af454b772e16d0f8e7ebe1be4f.png

Еще не все новогодние салаты были съедены, «Ирония судьбы» уже просмотрена, а до начала рабочей недели еще целая вечность и нужно было придумать себе развлечение на оставшиеся праздники. Предвкушая ностальгию я открыл Lineage 2, одну из самых популярных MMORPG «нулевых» на СНГ пространстве. Однако, самому играть уже не хотелось и пришла идея автоматизировать это дело. За подробностями под кат!

Введение

В школьные годы мы с друзьями играли в разные MMORPG игрушки, но самой залипательной для нас была Lineage 2. Суть игры состоит в том, чтобы 80% времени повторять одни и те же действия по убийству монстров и, время от времени, сражаться с другими игроками за этих самых монстров. К сожалению, времени на такие занятия у меня уже нет, поэтому было принято решение заняться автоматизацией! Как раз недавно попалась статья по OpenCV, которая вдохновила меня немного разобраться в этой теме —  там автор определял наличие помидора на картинке:)

Сказано — сделано! И вот уже открыт гугл в поисках готовых реализаций и каково же было мое удивление, что я сразу же нашел релазицию хабре. Мои идеи совпали с идеями автора, вот только код написан на Python. (Пост)
Небольшое отступление: я мобильный разработчик, который никогда не трогал этого вашего Питона. К сожалению, за два вечера у меня не получилось запустить код из репозитория автора. Сначала были конфликты в версии самого питона, потом какие-то непонятные ошибки с библиотеками из либы, в конце тулза AutoHotPy для работы с мышью и клавиатурой вообще отказалась работать. В итоге было принято решение написать свою реализацию на Kotlin с блекджеком и гномками! (Гномы — одна из рас в игре Lienage 2)

Поехали!

Для работы с окном игры будем использовать опенсорсную либу Java Native Access (JNA). Создаем новый проект в нашей любимой IDE, качаем два джарника с гитхаба (https://github.com/java-native-access/jna) JNA и JNA Platform, кладем их в наш проект и не забываем подключить их с помощью gradle:

implementation(files("lib/jna-5.12.1.jar"))
implementation(files("lib/jna-platform-5.12.1.jar"))

Определяем окно игры

Здесь ничего сложного, в списке окон находим окно с названием Lineage, получаем его координаты и далаем активным:

private fun detektWindow(windowName: String): Rectangle {
    val user32 = MyUser32.instance
    val rect = Rectangle(0, 0, 0, 0)
    var windowTitle = ""

    val windows = WindowUtils.getAllWindows(true)
    windows.forEach {
        if (it.title.contains(windowName)) {
            rect.setRect(it.locAndSize)
            windowTitle = it.title
        }
    }

    val tst: WinDef.HWND = user32.FindWindow(null, windowTitle)
    user32.ShowWindow(tst, User32.SW_SHOW)
    user32.SetForegroundWindow(tst)

    return rect
}

Поиск цели

Моя идея была такая же как и у автора из упомянутой статьи, однако некоторые детали у меня не сработали и пришлось подбирать реализацию под себя. Наш алгоритм действий будет следующими: делаем скриншот игры, с помощью фильтрации OpenCV находим имена монстров, наводимся на них мышкой, атакуем пока у монстра не закончится здоровье, переключаемся на следующего монстра и так до бесконечности! Весело, не правда ли? Погнали!

Устанавливаем OpenCV по гайду с офф сайта, скачиваем и закидываем в проект openCV-…jar и opencv_java…dll. Не забываем их подключить к проекту через gradle

implementation(files("lib/opencv-460.jar"))  

Делаем скришот окна игры

Основное окно Lineage 2Основное окно Lineage 2

На скриншоте видно, что помимо имен монстров у нас есть еще другие белые объекты, которые могут помешать: радар, чат, имя нашего персонажа и т.д. Для этого модифицируем наш скриншот и закрашиваем ненужные области в черный цвет:

Закрашены Закрашены «ненужные» помехи

Здесь уже вступает в игру OpenCV. Чтобы начать гриндить, нам необходимо найти цели. Как работаем — нам нужно провести фильтрацию так, чтобы на экране остались только белые прямоугольные объекты (так выглядят имена монстров). Идея следующая, мы помним что картинка состоит из пикселей, поэтому мы выполняем пороговое преобразование всех пикселей на картинке таким образом, чтобы туда попали только белые пиксели:

Imgproc.threshold(source, source, 252.0, 255.0, Imgproc.THRESH_BINARY)

далее нужно выполнять несколько формологических преобразований (размытие и растягивание), которые еще раз фильтранут все белые объекты по заданным размерам и преобразуют в белые прямоугольники путем размытия и растягивания (более подробное описание в офф.  доке)

Отфильтрованные белые прямоугольникиОтфильтрованные белые прямоугольники

private fun findPossibleTargets(rectangle: Rectangle): List {
    val capture: BufferedImage = Robot().createScreenCapture(rectangle)
    fillBlackExcess(capture, rectangle)

    val source: Mat = img2Mat(capture)

    Imgproc.cvtColor(source, source, Imgproc.COLOR_BGR2GRAY)
    Imgproc.threshold(source, source, 252.0, 255.0, Imgproc.THRESH_BINARY)
    val kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, Size(10.0, 1.0))
    Imgproc.morphologyEx(source, source, Imgproc.MORPH_CLOSE, kernel)
    Imgproc.erode(source, source, kernel)
    Imgproc.dilate(source, source, kernel)

    val points: MutableList = mutableListOf()
    Imgproc.findContours(source, points, Mat(), Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_SIMPLE)

    return points
        .sortedBy { it.toList().maxBy { it.y }.y }
        .filter {
        val maxX = it.toList().maxBy { it.x }.x
        val minX = it.toList().minBy { it.x }.x
        val width = (maxX - minX)

        val maxY = it.toList().maxBy { it.y }.y
        val minY = it.toList().minBy { it.y }.y

        val height = (maxY - minY)

        width > 30 && width < 200 && height < 30
    }
}

Сравнение объектов

Окей, наводиться мы научились, теперь нам нужно определить находится ли мышь на монстре или же на каком-то объекте флоры.

Т.к. флора и фауна мира Lineage 2 достаточно разнообразна, нам необходимо удостовериться что белый прямоугольник это наша желаемая цель в виде монстра, а не какая-то белая стена или трава. Для этого снова делаем скриншот, достаем наш шаблон, переводим обе картинки в серый и используем метод matchTemplate из OpenCV.

Работает он приблизительно следующим образом: наше шаблонное изображение последовательно накладывается на исходное изображение и между ними вычисляется корреляция, результат мы получаем на выходе в виде значения от 0.0 до 1.0. (Более подробно про метод в доке).

P.S. для тех кто будет пробовать реализацию — учтите, что на разных хрониках и серверах будут разные шаблоны у монстров, поэтому придется самостоятельно подготавливать эти изображения.

Сравнение ХП бараСравнение ХП бара

private fun isMouseSelectingAMob(rectangle: Rectangle): Boolean {
    Thread.sleep(100L)
    val minMatchThreshold = 0.8
    val capture: BufferedImage = Robot().createScreenCapture(rectangle)

    val thresholdScreen: Mat = img2Mat(capture)
    Imgproc.cvtColor(thresholdScreen, thresholdScreen, Imgproc.COLOR_BGR2GRAY)

    val template: Mat = Imgcodecs.imread("./src/main/resources/$TARGET_TEMPLATE_NAME.png")
    Imgproc.cvtColor(template, template, Imgproc.COLOR_BGR2GRAY)

    Imgproc.matchTemplate(thresholdScreen, template, thresholdScreen, Imgproc.TM_CCOEFF_NORMED)
    val value = Core.minMaxLoc(thresholdScreen).maxVal

    return value > minMatchThreshold
}

Эмуляция мыши/клавиатуры

Для начала нам нужно научиться эмулировать движение мыши и нажатие клавиатуры. К сожалению готовой, библиотеки на Java/Kotlin я не нашел, поэтому будем использовать написанную на языке С либу с названием Interception (https://github.com/oblitum/Interception). Тут я вспоминаю, что я мобильный разработчик и не умею в С, но быстро преободряюсь потому что написать обертку на Kotlin оказалось достаточно просто. Устанавливаем по гайду, закидываем файлы interception.dll и interception.h в проект. Interception работает в отдельном потоке, полностью перехватывает управление над мышью и клавиатурой, с помощью команд эмулирует движение и нажатие, однако чтобы вернуть управление обратно, нам нужно явно прописать это, задать определенную кнопку, иначе придется перезагружать весь компьютер

© Habrahabr.ru