Автоматизация рыбной ловли для World of Warcraft

в 9:31, , рубрики: opencv, python, world of warcraft, боты

Познакомился World of Warcraft очень давно и люблю его весь, но одна вещь больше всего не давала мне покоя — рыбная ловля. Это нудное повторяющееся действие, где ты просто нажимаешь на кнопку рыбной ловли и тыкаешь на поплавок раз в 5-15 секунд. Мой навык разработки рос, а ситуация с рыбной ловле так и не улучшалась с каждым годом что я играл, поэтому я решил убить двух зайцев сразу — начать осваивать python и всё же сделать бота для себя.

Я уже видел ботов, которые умеют ловить рыбу, работающие в свернутом режиме не перехватывая управления над компьютером. Также я знаю насколько беспощадны близард по вопросам банов читеров. Изменение данных в оперативной памяти легко определяется встроенным античитом. Ну и последнее — на мак я не нашёл ни одного бота.

Поэтому я решил закрыть все эти вопросы разом и сделать бота, который будет перехватывать управление мыши, кидать поплавок, и тыкать на него на экране когда нужно. Как я полагал python располагает широким выбором инструментов для автоматизации таких штук, и не ошибся.

Немножечко погуглив, я нашёл OpenCV, в котором есть поиск шаблону с не сложным гайдом. С помощью него мы и будем искать наш поплавок на экране.

Сперва мы должны получить саму картинку с поплавком. Ищем и находим библиотеку pyscreenshot с гайдом как делать скриншоты, немножечко редактируем:

import pyscreenshot as ImageGrab

screen_size = None
screen_start_point = None
screen_end_point = None

# Сперва мы проверяем размер экрана и берём начальную и конечную точку для будущих скриншотов
def check_screen_size():
	print "Checking screen size"
	img = ImageGrab.grab()
	# img.save('temp.png')
	global screen_size
	global screen_start_point
	global screen_end_point

	# я так и не смог найти упоминания о коэффициенте в методе grab с параметром bbox, но на моем макбуке коэффициент составляет 2. то есть при создании скриншота с координатами x1=100, y1=100, x2=200, y2=200), размер картинки будет 200х200 (sic!), поэтому делим на 2
	coefficient = 2
	screen_size = (img.size[0] / coefficient, img.size[1] / coefficient) 

	# берем примерно девятую часть экрана примерно посередине.
	screen_start_point = (screen_size[0] * 0.35, screen_size[1] * 0.35)
	screen_end_point = (screen_size[0] * 0.65, screen_size[1] * 0.65)
	print ("Screen size is " + str(screen_size))

def make_screenshot():
	print 'Capturing screen'
	screenshot = ImageGrab.grab(bbox=(screen_start_point[0], screen_start_point[1], screen_end_point[0], screen_end_point[1]))
	# сохраняем скриншот, чтобы потом скормить его в OpenCV
	screenshot_name = 'var/fishing_session_' + str(int(time.time())) + '.png'
	screenshot.save(screenshot_name)
	return screenshot_name

def main():
	check_screensize()
	make_screenshot()

Получаем примерно следующую картинку:

Автоматизация рыбной ловли для World of Warcraft - 1

Далее — найти поплавок. Для этого у нас должен быть сам шаблон поплавка, который мы ищем. После сотни попыток я всё таки подобрал те, которые OpenCV определяет лучше всего. Вот они:

Автоматизация рыбной ловли для World of Warcraft - 2Автоматизация рыбной ловли для World of Warcraft - 3Автоматизация рыбной ловли для World of Warcraft - 4Автоматизация рыбной ловли для World of Warcraft - 5Автоматизация рыбной ловли для World of Warcraft - 6Автоматизация рыбной ловли для World of Warcraft - 7Автоматизация рыбной ловли для World of Warcraft - 8Автоматизация рыбной ловли для World of Warcraft - 9

Берём код из ссылки выше, добавляем цикл и наши шаблоны:

import cv2
import numpy as np
from matplotlib import pyplot as plt

def find_float(screenshot_name):
	print 'Looking for a float'
	for x in range(0, 7):
		# загружаем шаблон
		template = cv2.imread('var/fishing_float_' + str(x) + '.png', 0)
		# загружаем скриншот и изменяем его на чернобелый
		src_rgb = cv2.imread(screenshot_name)
		src_gray = cv2.cvtColor(src_rgb, cv2.COLOR_BGR2GRAY)
		# берем ширину и высоту шаблона
		w, h = template.shape[::-1]
		# магия OpenCV, которая и находит наш темплейт на картинке
		res = cv2.matchTemplate(src_gray, template, cv2.TM_CCOEFF_NORMED)
		# понижаем порог соответствия нашего шаблона с 0.8 до 0.6, ибо поплавок шатается и освещение в локациях иногда изменяет его цвета, но не советую ставить ниже, а то и рыба будет похожа на поплавок
		threshold = 0.6
		# numpy фильтрует наши результаты по порогу
		loc = np.where( res >= threshold)
		# выводим результаты на картинку
		for pt in zip(*loc[::-1]):
		    cv2.rectangle(src_rgb, pt, (pt[0] + w, pt[1] + h), (0,0,255), 2)
		# и если результаты всё же есть, то возвращаем координаты и сохраняем картинку
		if loc[0].any():
			print 'Found float at ' + str(x) 
			cv2.imwrite('var/fishing_session_' + str(int(time.time())) + '_success.png', src_rgb)
			return (loc[1][0] + w / 2) / 2, (loc[0][0] + h / 2) / 2 # опять мы ведь помним, что макбук играется с разрешениями? поэтому снова приходится делить на 2

def main():
	check_screensize()
	img_name = make_screenshot()
	find_float(img_name)
	

Итак, у нас есть координаты поплавка, двигать курсор мыши умеет autopy буквально с помощью одной строки, подставляем свои координаты:

import autopy

def move_mouse(place):
	x,y = place[0], place[1]
	print("Moving cursor to " + str(place))
	autopy.mouse.smooth_move(int(screen_start_point[0]) + x , int(screen_start_point[1]) + y)

def main():
	check_screensize()
	img_name = make_screenshot()
	cords = find_float(img_name)
	move_mouse(cords)

Курсор на поплавке и теперь самое интересное — как же узнать когда нужно нажать? Ведь в самой игре поплавок подпрыгивает и издает звук будто что-то плюхается в воду. После тестов с поиском картинки я заметил, что OpenCV приходится подумать пол секунды прежде чем он возвращает результат, а поплавок прыгает даже быстрее и изменение картинки мы врядли сможем определить с помощью OpenCV, значит будем слушать звук. Для этой задачки мне пришлось поковырять разные решения и остановился вот над этим — пример использования гугл апи для распознавания голоса, оттуда мы возьмём код, который считывает звук.

import pyaudio
import wave
import audioop
from collections import deque
import time
import math

def listen():
	print 'Listening for loud sounds...'
	CHUNK = 1024
	FORMAT = pyaudio.paInt16
	CHANNELS = 2
	RATE = 18000 # битрейт звука, который мы хотим слушать
	THRESHOLD = 1200  # порог интенсивности звука, если интенсивность ниже, значит звук по нашим меркам слишком тихий
	SILENCE_LIMIT = 1  # длительность тишины, если мы не слышим ничего это время, то начинаем слушать заново

	# открываем стрим
	p = pyaudio.PyAudio()

	stream = p.open(format=FORMAT,
	                channels=CHANNELS,
	                rate=RATE,
	                # output=True, # на мак ос нет возможности слушать output, поэтому мне пришлось прибегнуть к использованию <a href="https://github.com/RogueAmoeba/Soundflower-Original">Soundflower</a>, который умеет перенаправлять канал output в input, таким образом мы перехватываем звук игры будто это микрофон 
	                input=True,
	                frames_per_buffer=CHUNK) 
	cur_data = ''
	rel = RATE/CHUNK
	slid_win = deque(maxlen=SILENCE_LIMIT * rel)
	
	# начинаем слушать и по истечении 20 секунд (столько максимум длится каждый заброс поплавка), отменяем нашу слушалку.
	success = False
	listening_start_time = time.time()
	while True:
		try:
			cur_data = stream.read(CHUNK)
			slid_win.append(math.sqrt(abs(audioop.avg(cur_data, 4))))
			if(sum([x > THRESHOLD for x in slid_win]) > 0):    
				print 'I heart something!'
				success = True
				break
			if time.time() - listening_start_time > 20:
				print 'I don't hear anything during 20 seconds!'
				break
		except IOError:
			break
	# обязательно закрываем стрим
	stream.close()
	p.terminate()
	return success

def main():
	check_screensize()
	img_name = make_screenshot()
	cords = find_float(img_name)
	move_mouse(cords)
	listen()

Последнее, что осталось — споймать саму рыбку, когда мы услышим звук, снова используем autopy:

def snatch():
	print('Snatching!')
	autopy.mouse.click(autopy.mouse.RIGHT_BUTTON)

def main():
	check_screensize()
	img_name = make_screenshot()
	cords = find_float(img_name)
	move_mouse(cords)
	if listen():
		snatch()

По моим тестам, что я оставлял бота рыбачить по ночам, за неделю такого абуза он сделал около 7000 бросков и словил около 5000 рыб. Погрешность 30% вызвана тем, что иногда не получается отловить звук или найти поплавок из-за освещения или поворотов поплавка. Но результатом я доволен — впервые попробовал python, сделал бота и сэкономил себе кучу времени.

Полный код можно посмотреть тут, но очень советую не использовать для абуза фишинга, ибо бан — страшно.

Буду рад любым комментариям.

Автор: kio_tk

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js